Skip to content

feat: 프롬프트 목록 페이지 추가 기능 구현#42

Merged
ccconac merged 12 commits intodevfrom
feat/40-promptList
Jan 29, 2026
Merged

feat: 프롬프트 목록 페이지 추가 기능 구현#42
ccconac merged 12 commits intodevfrom
feat/40-promptList

Conversation

@ccconac
Copy link
Copy Markdown
Member

@ccconac ccconac commented Jan 29, 2026

✅ 체크리스트

  • 프롬프트 카드 UI 변경
  • 프롬프트 목록 페이지 API DTO 변경 대응
  • 프롬프트 목록 페이지 페이지네이션 추가
    • 페이지네이션 컴포넌트 구현

📝 작업 상세 내용

프롬프트 카드 UI 변경

프롬프트 카드 UI가 변경되었습니다. margin 속성 사용을 지양하기 위해 일부 여백 속성을 수정하고, 카드 컴포넌트의 높이를 h-28에서 h-18로 변경했습니다. 사소한 변경점으로 별도의 이미지 첨부는 하지 않았습니다.

프롬프트 목록 페이지 페이지네이션 추가

Pagination 컴포넌트 구현이 완료되었습니다. 관련한 로직을 커스텀 훅으로 분리해 두었는데, 페이지네이션 컴포넌트에만 사용되는 훅이기 때문에 과도한 분리보다는 한곳에서 관리하는 게 좋을 것 같아 고민 중에 있습니다. 추후 변경될 수 있습니다.

📸 스크린샷 (선택 사항)

pagination

✅ 셀프 체크리스트

  • 브랜치 확인하기
  • 불필요한 코드가 들어가지 않았는지 재확인하기
  • issue 닫기
  • reiewers, assignees, Lables 등록 확인하기

이슈 번호: #40

Summary by CodeRabbit

  • 새로운 기능

    • 페이지 내비게이션 UI 추가로 프롬프트를 페이지 단위로 탐색 가능해졌습니다.
    • 정렬 옵션이 확장되어 최신순/오래된순 선택이 가능합니다.
    • 프롬프트 항목에 더 풍부한 메타정보(작성일, 통계)와 태그 표시가 적용되었습니다.
  • 스타일

    • 카드 레이아웃의 간격과 시각적 계층이 개선되어 읽기 쉬워졌습니다.
  • 아이콘

    • 이전/다음 네비게이션용 Chevron 아이콘이 추가되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@ccconac ccconac self-assigned this Jan 29, 2026
@ccconac ccconac added ♻️ REFACTOR 리팩토링 (성능 개선 등) ✨ FEAT 기능 개발 labels Jan 29, 2026
@ccconac ccconac linked an issue Jan 29, 2026 that may be closed by this pull request
4 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 29, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Prompt 목록 응답 타입을 중첩 구조로 리팩토링하고, 클라이언트 페이지네이션 훅(usePagination)과 Pagination 컴포넌트를 추가했으며, usePromptList 훅을 React Query로 마이그레이션해 페이지네이션을 통합했습니다.

Changes

Cohort / File(s) Summary
Prompt API 타입 리팩토링
src/apis/prompts/prompts.types.ts
PromptListItemResponseAuthorInfo, PromptContent, PromptStats, PromptTags 등 중첩 구조로 재구성하고 GetPromptListParams.sort'oldest' 옵션을 추가했습니다.
Prompt 매퍼 및 모킹 업데이트
src/mappers/promptMapper.ts, src/mocks/prompts.ts
새로운 PromptDTO 타입 도입(및 tags: string[]), mapPromptItemDTO/mapPromptDetailDTO를 중첩된 응답 구조에 맞게 재작성했습니다.
페이지네이션 훅 및 상수
src/hooks/prompts/usePagination.ts, src/config/constants.ts
DOTS 상수 추가 및 페이지 범위 계산을 수행하는 usePagination 훅 추가(숫자 페이지와 DOTS 반환).
Pagination UI 컴포넌트 및 타입/스타일
src/components/Pagination/Pagination.tsx, src/components/Pagination/Pagination.types.ts, src/components/Pagination/Pagination.styles.ts
Pagination 컴포넌트, 관련 타입 파일 및 ButtonStyles 스타일 그룹을 추가했습니다.
데이터 페칭 훅 변경
src/hooks/prompts/usePromptList.ts
usePromptList을 React Query로 전환하고 params 인자 추가, select로 매핑하여 { prompts, meta } 반환 및 isError 포함하도록 변경했습니다.
페이지 통합 및 UI 조정
src/pages/PromptList/PromptListPage.tsx, src/components/Card/Card.tsx
PromptListPage에 클라이언트 페이지 상태와 Pagination 연결을 추가했고, Card 레이아웃을 flex 기반으로 조정했습니다.
아이콘 맵 업데이트
src/components/Icon/iconMap.ts
아이콘 맵에 chevornLeftchevronRight 항목을 추가했습니다.

Sequence Diagram

sequenceDiagram
    actor User
    participant PromptListPage
    participant usePromptList as Hook
    participant ReactQuery as ReactQuery
    participant API
    participant PaginationComponent as Pagination

    User->>PromptListPage: 페이지 변경 클릭
    PromptListPage->>PromptListPage: page 상태 업데이트
    PromptListPage->>Hook: GetPromptListParams 전달 (page, size)
    Hook->>ReactQuery: useQuery 실행 (queryKey, queryFn)
    ReactQuery->>API: getPromptList(page, size) 요청
    API-->>ReactQuery: PromptListItemResponse[] 반환
    ReactQuery->>Hook: select로 매핑(mapPromptListItemDTO 등)
    Hook-->>PromptListPage: { prompts, meta }
    PromptListPage->>Pagination: currentPage, totalSize 전달
    Pagination-->>PromptListPage: onPageChange 콜백 호출
    PromptListPage-->>User: 업데이트된 목록 및 페이지네이션 렌더링
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested labels

💄 DESIGN

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 페이지네이션 기능 추가, UI 수정, API DTO 변경 대응 등 여러 주요 변경사항을 포함하는 광범위한 변경을 적절히 요약하고 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/components/Pagination/Pagination.tsx`:
- Around line 15-58: paginationRange can be empty causing lastPage to be
undefined and enabling the Next button; ensure lastPage is derived safely (e.g.
fallback to Math.max(1, Math.ceil(totalSize / pageSize)) or 0 when
totalSize===0) and guard usePagination results: compute lastPage from totalSize
when paginationRange is falsy or empty, make onNext clamp to lastPage, and
update the Next button's disabled check to use this safe lastPage (references:
paginationRange, lastPage, usePagination, currentPage, onNext, DOTS).

In `@src/hooks/prompts/usePagination.ts`:
- Around line 15-55: The function usePagination has a missing return when
rendering all pages: in the computed paginationRnage you call range(1,
totalSize) inside the if but don't return it and the condition is reversed;
change the logic so that when totalSize is less than or equal to
totalPageNumbers you return range(1, totalSize) (i.e., replace the existing "if
(totalSize >= totalPageNumbers) range(1, totalSize);" with a proper early
return), ensuring paginationRnage yields the full page list; keep using the
existing symbols (usePagination, paginationRnage, totalPageNumbers, range, DOTS,
firstPageIndex, lastPageIndex) to locate and update the code.
🧹 Nitpick comments (3)
src/components/Card/Card.tsx (1)

18-18: className에 불필요한 trailing space가 있습니다.

gap-1 뒤에 불필요한 공백이 있습니다. 기능에는 영향이 없지만, 코드 정리 차원에서 제거하는 것이 좋습니다.

🧹 수정 제안
-      <main className="flex flex-col h-18 gap-1 ">
+      <main className="flex flex-col h-18 gap-1">
src/pages/PromptList/PromptListPage.tsx (1)

38-38: 콜백 함수를 간소화할 수 있습니다.

(page) => setPage(page)setPage로 간소화할 수 있습니다. 또한 현재 파라미터 page가 외부 state 변수 page를 shadowing하고 있어 혼란을 줄 수 있습니다.

♻️ 수정 제안
       <Pagination
         currentPage={meta.page}
         totalSize={meta.totalPages}
-        onPageChange={(page) => setPage(page)}
+        onPageChange={setPage}
       />
src/hooks/prompts/usePromptList.ts (1)

12-18: 초기 meta 값이 기본 파라미터와 불일치합니다.
초기 렌더에서 페이지네이션이 0페이지로 보일 수 있으니 기본값을 defaultParams 기준으로 맞추는 편이 안정적입니다.

♻️ 수정 제안
-const defaultMeta: PromptListMeta = {
-  page: 0,
-  size: 0,
+const defaultMeta: PromptListMeta = {
+  page: defaultParams.page ?? 1,
+  size: defaultParams.size ?? 0,
   totalElements: 0,
   totalPages: 0,
   hasNext: false,
 };

Comment on lines +15 to +58
const paginationRange = usePagination({ totalSize, siblingCount, currentPage });
const lastPage = paginationRange ? paginationRange[paginationRange.length - 1] : 0;

if (currentPage === 0) return null;

const onPrevious = () => onPageChange(currentPage - 1);
const onNext = () => onPageChange(currentPage + 1);

return (
<div className={`flex items-center justify-center gap-1 ${className}`}>
<button
onClick={onPrevious}
disabled={currentPage === 1}
aria-label="이전 페이지"
className={`${ButtonStyles.base} ${
currentPage === 1 ? ButtonStyles.disabled : ButtonStyles.inactive
}`}>
<SvgChevornleft />
</button>
{paginationRange?.map((pageNumber, index) => {
if (pageNumber === DOTS) {
return (
<span
key={`dots-${index}`}
className="px-2 text-gray-400 flex items-center select-none">
{DOTS}
</span>
);
}

return (
<button
key={pageNumber}
className={`${ButtonStyles.base} ${
pageNumber === currentPage ? ButtonStyles.active : ButtonStyles.inactive
}`}
onClick={() => onPageChange(Number(pageNumber))}>
{pageNumber}
</button>
);
})}
<button
onClick={onNext}
disabled={currentPage === lastPage}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

빈 페이지(totalSize=0)에서 다음 버튼이 활성화될 수 있습니다.
paginationRange가 빈 배열이면 lastPage가 undefined가 되어 Next가 눌릴 수 있습니다. totalSize/빈 range를 가드하고 lastPage는 totalSize로 계산하는 편이 안전합니다.

🛠️ 수정 제안
-  const paginationRange = usePagination({ totalSize, siblingCount, currentPage });
-  const lastPage = paginationRange ? paginationRange[paginationRange.length - 1] : 0;
-
-  if (currentPage === 0) return null;
+  const paginationRange = usePagination({ totalSize, siblingCount, currentPage });
+  if (currentPage === 0 || totalSize <= 0 || !paginationRange?.length) return null;
+  const lastPage = totalSize;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const paginationRange = usePagination({ totalSize, siblingCount, currentPage });
const lastPage = paginationRange ? paginationRange[paginationRange.length - 1] : 0;
if (currentPage === 0) return null;
const onPrevious = () => onPageChange(currentPage - 1);
const onNext = () => onPageChange(currentPage + 1);
return (
<div className={`flex items-center justify-center gap-1 ${className}`}>
<button
onClick={onPrevious}
disabled={currentPage === 1}
aria-label="이전 페이지"
className={`${ButtonStyles.base} ${
currentPage === 1 ? ButtonStyles.disabled : ButtonStyles.inactive
}`}>
<SvgChevornleft />
</button>
{paginationRange?.map((pageNumber, index) => {
if (pageNumber === DOTS) {
return (
<span
key={`dots-${index}`}
className="px-2 text-gray-400 flex items-center select-none">
{DOTS}
</span>
);
}
return (
<button
key={pageNumber}
className={`${ButtonStyles.base} ${
pageNumber === currentPage ? ButtonStyles.active : ButtonStyles.inactive
}`}
onClick={() => onPageChange(Number(pageNumber))}>
{pageNumber}
</button>
);
})}
<button
onClick={onNext}
disabled={currentPage === lastPage}
const paginationRange = usePagination({ totalSize, siblingCount, currentPage });
if (currentPage === 0 || totalSize <= 0 || !paginationRange?.length) return null;
const lastPage = paginationRange[paginationRange.length - 1];
const onPrevious = () => onPageChange(currentPage - 1);
const onNext = () => onPageChange(currentPage + 1);
return (
<div className={`flex items-center justify-center gap-1 ${className}`}>
<button
onClick={onPrevious}
disabled={currentPage === 1}
aria-label="이전 페이지"
className={`${ButtonStyles.base} ${
currentPage === 1 ? ButtonStyles.disabled : ButtonStyles.inactive
}`}>
<SvgChevornleft />
</button>
{paginationRange?.map((pageNumber, index) => {
if (pageNumber === DOTS) {
return (
<span
key={`dots-${index}`}
className="px-2 text-gray-400 flex items-center select-none">
{DOTS}
</span>
);
}
return (
<button
key={pageNumber}
className={`${ButtonStyles.base} ${
pageNumber === currentPage ? ButtonStyles.active : ButtonStyles.inactive
}`}
onClick={() => onPageChange(Number(pageNumber))}>
{pageNumber}
</button>
);
})}
<button
onClick={onNext}
disabled={currentPage === lastPage}
🤖 Prompt for AI Agents
In `@src/components/Pagination/Pagination.tsx` around lines 15 - 58,
paginationRange can be empty causing lastPage to be undefined and enabling the
Next button; ensure lastPage is derived safely (e.g. fallback to Math.max(1,
Math.ceil(totalSize / pageSize)) or 0 when totalSize===0) and guard
usePagination results: compute lastPage from totalSize when paginationRange is
falsy or empty, make onNext clamp to lastPage, and update the Next button's
disabled check to use this safe lastPage (references: paginationRange, lastPage,
usePagination, currentPage, onNext, DOTS).

Comment thread src/hooks/prompts/usePagination.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/hooks/prompts/usePromptList.ts`:
- Around line 6-35: The merge of params into defaultParams can leave explicit
undefined values in queryParams causing cache key/request mismatch; update
usePromptList to build a normalizedParams object using nullish coalescing for
each field (e.g., page: params?.page ?? defaultParams.page, size: params?.size
?? defaultParams.size, sort: params?.sort ?? defaultParams.sort), use
normalizedParams for the queryKey and for calling getPromptList, and derive
defaultMeta from normalizedParams (so defaultMeta.page/size come from
normalizedParams.page/size).

Comment on lines +6 to +35
const defaultParams: GetPromptListParams = {
page: 1,
size: 21,
sort: 'latest',
};

const defaultMeta: PromptListMeta = {
page: defaultParams.page ?? 1,
size: defaultParams.size ?? 0,
totalElements: 0,
totalPages: 0,
hasNext: false,
};

useEffect(() => {
(async () => {
try {
const res = await getPromptList();
const mappedData = res.data.items.map(mapPromptListItemDTO);
export const usePromptList = (params?: GetPromptListParams) => {
const queryParams = { ...defaultParams, ...params };

setPrompts(mappedData);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
})();
}, []);
const { data, isLoading, isError, error } = useQuery({
queryKey: ['prompts', 'list', queryParams],
queryFn: () => getPromptList(queryParams),
select: (response) => ({
prompts: response.data.items.map(mapPromptListItemDTO),
meta: response.data.meta,
}),
placeholderData: keepPreviousData,
});

return { prompts, loading, error };
return {
prompts: data?.prompts || [],
meta: data?.meta || defaultMeta,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and examine its content
fd -t f "usePromptList.ts"

Repository: promlog/promlog-client

Length of output: 100


🏁 Script executed:

# Look for the file in the hooks directory
find . -name "usePromptList.ts" -type f 2>/dev/null

Repository: promlog/promlog-client

Length of output: 102


🏁 Script executed:

# Check the repository structure
ls -la src/hooks/ 2>/dev/null || echo "Directory not found"

Repository: promlog/promlog-client

Length of output: 233


🏁 Script executed:

# Read the file content to verify the code snippet
cat -n src/hooks/prompts/usePromptList.ts

Repository: promlog/promlog-client

Length of output: 1475


🏁 Script executed:

# Also check for type definitions
fd -t f "\.ts" src/ | xargs grep -l "GetPromptListParams\|PromptListMeta" 2>/dev/null | head -10

Repository: promlog/promlog-client

Length of output: 162


🏁 Script executed:

# Check the type definitions
cat -n src/apis/prompts/prompts.types.ts

Repository: promlog/promlog-client

Length of output: 1934


🏁 Script executed:

# Also check the prompts.ts to understand the getPromptList function
cat -n src/apis/prompts/prompts.ts

Repository: promlog/promlog-client

Length of output: 1064


🏁 Script executed:

# Let's verify the actual behavior by analyzing the parameter handling
# Check if there are any places where usePromptList is called with undefined params
grep -r "usePromptList" src/ --include="*.ts" --include="*.tsx" -B 2 -A 2

Repository: promlog/promlog-client

Length of output: 1208


🏁 Script executed:

# Let's analyze the parameter spreading behavior and cache implications
# Check if there are any test files or other usages that might pass undefined
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "usePromptList" 2>/dev/null || echo "No test files found with usePromptList"

Repository: promlog/promlog-client

Length of output: 104


🏁 Script executed:

# Let's check the actual usage more carefully and see if partial params are passed
cat -n src/pages/PromptList/PromptListPage.tsx | head -30

Repository: promlog/promlog-client

Length of output: 1230


명시적 파라미터 정규화로 잠재적 캐시 키/기본 메타 불일치 예방

현재 코드에서 params에 명시적으로 undefined 값이 포함되면 스프레드 연산자로 인해 기본값이 덮어씌워질 수 있습니다. 예를 들어 usePromptList({page: undefined})를 호출하면 queryParams.pageundefined가 되지만, getPromptList의 기본 파라미터에서는 page = 1을 사용하므로 캐시 키와 실제 요청 값이 달라질 수 있습니다. 명시적 널 병합 연산자(??)로 각 파라미터를 정규화하고, 정규화된 queryParams에서 defaultMeta를 생성하도록 수정해 주세요.

🔧 제안 수정안
-const defaultMeta: PromptListMeta = {
-  page: defaultParams.page ?? 1,
-  size: defaultParams.size ?? 0,
-  totalElements: 0,
-  totalPages: 0,
-  hasNext: false,
-};
+const buildDefaultMeta = (params: GetPromptListParams): PromptListMeta => ({
+  page: params.page ?? 1,
+  size: params.size ?? 0,
+  totalElements: 0,
+  totalPages: 0,
+  hasNext: false,
+});

 export const usePromptList = (params?: GetPromptListParams) => {
-  const queryParams = { ...defaultParams, ...params };
+  const queryParams: GetPromptListParams = {
+    page: params?.page ?? defaultParams.page,
+    size: params?.size ?? defaultParams.size,
+    sort: params?.sort ?? defaultParams.sort,
+  };
+  const defaultMeta = buildDefaultMeta(queryParams);

   const { data, isLoading, isError, error } = useQuery({
     queryKey: ['prompts', 'list', queryParams],
     queryFn: () => getPromptList(queryParams),
@@
   return {
     prompts: data?.prompts || [],
-    meta: data?.meta || defaultMeta,
+    meta: data?.meta ?? defaultMeta,
🤖 Prompt for AI Agents
In `@src/hooks/prompts/usePromptList.ts` around lines 6 - 35, The merge of params
into defaultParams can leave explicit undefined values in queryParams causing
cache key/request mismatch; update usePromptList to build a normalizedParams
object using nullish coalescing for each field (e.g., page: params?.page ??
defaultParams.page, size: params?.size ?? defaultParams.size, sort: params?.sort
?? defaultParams.sort), use normalizedParams for the queryKey and for calling
getPromptList, and derive defaultMeta from normalizedParams (so
defaultMeta.page/size come from normalizedParams.page/size).

@ccconac ccconac merged commit 05edb08 into dev Jan 29, 2026
1 check was pending
@ccconac ccconac deleted the feat/40-promptList branch January 29, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEAT 기능 개발 ♻️ REFACTOR 리팩토링 (성능 개선 등)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 프롬프트 목록 페이지 추가 기능 구현

1 participant