[fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영#104
Conversation
… fix/book-api-spec-59
… fix/book-api-spec-59
… fix/book-api-spec-59
… fix/book-api-spec-59
… fix/book-api-spec-59
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
Walkthrough북 기능의 API 엔드포인트를 중앙화하고 모의(mock) 데이터를 별도 모듈로 분리했습니다. 레코드 기반 API를 타임라인 기반 무한 스크롤로 전환하고, 타입·컴포넌트·훅을 이에 맞춰 광범위하게 갱신했습니다. (타입·엔드포인트·모의·UI·훅 변경 포함) Changes
Sequence DiagramsequenceDiagram
participant User as 사용자
participant UI as BookLogList
participant Hook as useBookRecords
participant Scroll as useInfiniteScroll
participant API as Book API
participant Timeline as Timeline Service
User->>UI: 페이지 진입
UI->>Hook: 초기 데이터 요청
Hook->>API: getBookTimeline(personalBookId, params)
API->>Timeline: 첫 페이지 요청
Timeline-->>API: { items, nextCursor, hasNext }
API-->>Hook: 응답 전송
Hook-->>UI: 데이터 제공 + hasNextPage
UI->>Scroll: 스크롤 감지
Scroll-->>Hook: loadMore 트리거
Hook->>API: getBookTimeline(..., { cursorEventAt, cursorSourceId })
API->>Timeline: 다음 페이지 요청
Timeline-->>API: { items, nextCursor, hasNext }
API-->>Hook: 다음 페이지 응답
Hook-->>UI: 페이지 병합 및 렌더
UI->>UI: 항목 렌더링
User->>UI: 편집/삭제 요청
UI->>Hook: 뮤테이션 실행 (update/delete)
Hook->>API: updateBookRecord/deleteBookRecord
API-->>Hook: 완료
Hook->>Hook: 쿼리 무효화
Hook-->>UI: 갱신된 데이터 반영
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (10)
src/features/keywords/keywords.mock.ts (1)
728-731: 목데이터 원본을 참조 그대로 반환하고 있습니다.문제:
mockKeywordsResponse를 그대로 반환합니다. 영향: 호출부에서 값을 수정하면 이후 호출까지 데이터가 오염될 수 있습니다. 대안: 반환 시 복제본을 만들어 불변성을 보장해 주세요.변경 제안
export const getMockKeywords = async (): Promise<GetKeywordsResponse> => { await delay(MOCK_DELAY) - return mockKeywordsResponse + return { + keywords: mockKeywordsResponse.keywords.map((keyword) => ({ ...keyword })), + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/keywords/keywords.mock.ts` around lines 728 - 731, getMockKeywords currently returns the shared mockKeywordsResponse object directly, which can lead to downstream mutation and data contamination; update getMockKeywords to return a cloned copy of mockKeywordsResponse (e.g., use a deep clone mechanism such as structuredClone or JSON.parse(JSON.stringify(...)) depending on environment) so callers receive an immutable copy and the original mockKeywordsResponse stays unchanged.src/shared/constants/routes.ts (1)
52-53: 동일 경로 라우트 상수 중복은 정리 권장합니다.문제: Line 52-53의
MEETING_RETROSPECTIVE_PERSONAL이 기존PERSONAL_RETROSPECTIVE와 동일 경로입니다.
영향: 호출부마다 상수 사용이 갈려 검색/변경 시 누락 위험이 커집니다.
대안: 하나로 통일하고, 호환 목적이면 deprecated alias 주석으로 의도를 명시해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/constants/routes.ts` around lines 52 - 53, MEETING_RETROSPECTIVE_PERSONAL duplicates the existing PERSONAL_RETROSPECTIVE route constant; consolidate to a single source of truth by removing the duplicate constant and updating all usages to reference PERSONAL_RETROSPECTIVE, or keep MEETING_RETROSPECTIVE_PERSONAL as a deprecated alias (add a clear comment like "DEPRECATED: use PERSONAL_RETROSPECTIVE") that forwards to PERSONAL_RETROSPECTIVE to preserve compatibility; update any imports/usages to the unified constant (references: MEETING_RETROSPECTIVE_PERSONAL, PERSONAL_RETROSPECTIVE).src/pages/Books/BookDetailPage.tsx (1)
21-21: 헤더 이동 경로는 라우트 상수 사용을 권장합니다.문제: Line 21의
to="/books"하드코딩은 라우트 상수 체계와 분리됩니다.
영향: 경로 변경 시 일부 화면만 누락되어 라우팅 불일치가 생길 수 있습니다.
대안:ROUTES.BOOKS를 사용해 경로 정의를 단일화해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Books/BookDetailPage.tsx` at line 21, Replace the hardcoded route string in the SubPageHeader call with the route constant to centralize route definitions: locate the SubPageHeader component usage (SubPageHeader label="내 책장" to="/books" disableShadow={isBookLogSticky}) and change the to prop to use ROUTES.BOOKS so the page follows the app's ROUTES constant system and avoids mismatched paths when routes change.src/features/book/components/BookLogActionMenu.tsx (1)
52-54: 아이콘 버튼에 접근 이름을 추가해 주세요.문제: Line 52 아이콘 전용 버튼에
aria-label이 없습니다.
영향: 스크린리더 사용자는 버튼 목적을 파악하기 어렵습니다.
대안:aria-label(필요 시aria-haspopup)을 추가해 의미를 명확히 하세요.접근성 속성 추가 예시
- <button type="button" className="cursor-pointer p-xsmall text-grey-600 hover:text-grey-800"> + <button + type="button" + aria-label="감상 기록 메뉴 열기" + aria-haspopup="menu" + className="cursor-pointer p-xsmall text-grey-600 hover:text-grey-800" + > <EllipsisVertical className="size-5" /> </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/components/BookLogActionMenu.tsx` around lines 52 - 54, BookLogActionMenu의 아이콘 전용 버튼(<button> wrapping <EllipsisVertical />)에 스크린리더용 의미가 없으니 버튼에 명확한 aria-label을 추가하고, 버튼이 팝업 메뉴를 열면 aria-haspopup="true"와 aria-expanded 상태 관리를 추가하도록 수정하세요; 예: BookLogActionMenu 컴포넌트에서 해당 <button> 요소를 찾아 적절한 설명(예: "더보기 옵션")을 aria-label로 넣고, 메뉴 열림/닫힘 상태가 있으면 aria-expanded를 반영하도록 연결하세요.src/features/book/components/BookLogList.tsx (2)
254-256: PRE_OPINION key 안정성 검토 필요
gatheringName + sharedAt조합은 데이터 변경 시 불안정할 수 있습니다.→ 가능하다면 고유 ID 기반 key 사용 권장 (예:
preOpinionId또는meetingId)♻️ 더 안정적인 key 예시
- key={`pre-opinion-${item.preOpinion.gatheringName}-${item.preOpinion.sharedAt}`} + key={`pre-opinion-${item.preOpinion.gatheringId}-${item.preOpinion.meetingId}`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/components/BookLogList.tsx` around lines 254 - 256, The key for MeetingPreOpinionItem is currently built from gatheringName+sharedAt which can change and cause React reconciliation bugs; update the key to use a stable unique identifier from the preOpinion object (e.g., item.preOpinion.preOpinionId or item.preOpinion.meetingId) inside the BookLogList component where MeetingPreOpinionItem is rendered, and if that ID field might be missing ensure you fall back to another immutable identifier (e.g., a stable UUID or a concatenation of immutable IDs like gatheringId with sharedAt) so the key is reliably stable across updates.
47-67: mutation 에러 핸들링 누락
onSuccess만 정의되어 있고onError가 없습니다.→ 삭제 실패 시 사용자에게 피드백이 없어 UX 저하 가능
→ 토스트 알림 등으로 에러 상태 표시 권장♻️ 에러 핸들링 추가 예시
const { mutate: deletePersonalRecord } = useMutation({ mutationFn: (recordId: number) => deleteBookRecord(bookId, recordId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: bookRecordsKeys.all }) }, + onError: () => { + // toast.error('기록 삭제에 실패했습니다.') + }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/components/BookLogList.tsx` around lines 47 - 67, The three useMutation hooks (mutate: deletePersonalRecord, deletePreOpinion, deleteRetrospective) lack onError handlers; add onError callbacks to each to show user feedback (e.g., a toast or alert) and optionally log the error, and ensure queryClient.invalidateQueries({ queryKey: bookRecordsKeys.all }) is still called or rollback state if needed; implement the onError for the mutationFn wrappers (deleteBookRecord, deleteMyPreOpinionAnswer, deletePersonalRetrospective) to display a clear error message and include the error object for debugging while keeping the existing onSuccess logic.src/features/book/book.mock.ts (1)
333-338: mock 상태 mutation 주의
mockBookDetail.bookReadingStatus를 직접 변경하므로 테스트 간 상태가 공유됩니다.→ 테스트 환경에서 예상치 못한 상태 오염 가능
→ 필요시 상태 리셋 유틸리티 추가 고려🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/book.mock.ts` around lines 333 - 338, The function getMockToggleBookReadingStatus mutates the shared module-level mockBookDetail (symbol mockBookDetail), causing test state leakage; change it to avoid mutating mockBookDetail by computing the toggled status locally and returning a new BookDetail object (e.g., derive newStatus = mockBookDetail.bookReadingStatus === 'READING' ? 'COMPLETED' : 'READING' and return { ...mockBookDetail, bookReadingStatus: newStatus, bookId }) rather than assigning to mockBookDetail; if shared mutation is required elsewhere, add and export a clear/reset utility (e.g., resetMockBookDetail) to reinitialize mockBookDetail for tests.src/features/book/book.api.ts (3)
186-188:getBookTimelinemock 호출에서 책 식별자가 누락됩니다.문제: mock 분기가
personalBookId없이getMockBookRecords(params)만 호출합니다.
영향: mock 모드에서 책별 타임라인 분리가 깨져 실제 API와 다른 결과로 검증될 수 있습니다.
대안: mock helper 시그니처를 실제 API와 맞춰personalBookId를 전달하세요.변경 예시
export async function getBookTimeline( personalBookId: number, params: GetBookTimelineParams = {} ): Promise<GetBookTimelineResponse> { if (USE_MOCK) { - return getMockBookRecords(params) + return getMockBookRecords(personalBookId, params) }// src/features/book/book.mock.ts export const getMockBookRecords = async ( personalBookId: number, params: GetBookTimelineParams = {} ): Promise<GetBookTimelineResponse> => { // personalBookId 기준 필터 반영 }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/book.api.ts` around lines 186 - 188, getBookTimeline의 mock 분기에서 책 식별자(personalBookId)를 빼먹어 mock 동작이 실제 API와 다릅니다; 수정 방법은 USE_MOCK 분기에서 getMockBookRecords를 호출할 때 함수 시그니처에 맞춰 personalBookId를 첫번째 인수로 전달하고 필요하면 GetBookTimelineParams 타입을 두번째 인수로 넘기도록 변경하되, getMockBookRecords 정의(또는 호출 지점)와 getBookTimeline 함수 시그니처(personalBookId, params)를 일치시키세요.
150-152: 페이지네이션 API의 mock 분기에서 파라미터 계약이 유실됩니다.문제:
getMyGatherings,getBookReviewHistorymock 호출이params(및bookId)를 전달하지 않습니다.
영향: 커서/필터 기반 시나리오를 mock 환경에서 정확히 재현하지 못해 QA 신뢰도가 떨어집니다.
대안: mock helper에도 동일 인자를 전달하고 커서/필터 로직을 반영하세요.변경 예시
export async function getMyGatherings( params: GetGatheringsParams = {} ): Promise<GetGatheringsResponse> { if (USE_MOCK) { - return getMockMyGatherings() + return getMockMyGatherings(params) }export async function getBookReviewHistory( bookId: number, params: GetBookReviewHistoryParams = {} ): Promise<GetBookReviewHistoryResponse> { if (USE_MOCK) { - return getMockBookReviewHistory() + return getMockBookReviewHistory(bookId, params) }Also applies to: 218-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/book.api.ts` around lines 150 - 152, The mock branch currently returns getMockMyGatherings() without forwarding the original request parameters, so cursor/filter semantics are lost; update the USE_MOCK branches to call getMockMyGatherings(params) and getMockBookReviewHistory(params, bookId) (or equivalent mock helpers) and ensure the mock helpers accept and honor params (cursor, limit, filters) and bookId so mock behavior mirrors getMyGatherings and getBookReviewHistory cursor/filter logic; update the mock helper signatures and any call sites (references: USE_MOCK, getMockMyGatherings, getMyGatherings, getBookReviewHistory, bookId, params) accordingly.
316-322: 빈bookIds요청은 사전에 차단하는 편이 안전합니다.문제:
deleteBook([])호출 시 그대로 DELETE 요청이 전송됩니다.
영향: 불필요한 네트워크 호출과 서버 4xx 응답 가능성이 생깁니다.
대안: 빈 배열은 early return(또는 명시적 에러)로 처리하세요.변경 예시
export async function deleteBook(bookIds: number[]): Promise<void> { + if (bookIds.length === 0) return + if (USE_MOCK) { return getMockDeleteResponse() } return api.delete(BOOK_ENDPOINTS.DELETE, { data: { bookIds } }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/book.api.ts` around lines 316 - 322, In deleteBook, add an early-guard for empty or missing bookIds before any network/mock behavior: check if (!bookIds || bookIds.length === 0) and return immediately (e.g., return Promise.resolve() or void) to avoid calling getMockDeleteResponse or api.delete with an empty array; update the function so the emptiness check occurs before the USE_MOCK branch and reference the deleteBook function, bookIds variable, getMockDeleteResponse, api.delete, and BOOK_ENDPOINTS.DELETE when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/Books/BookListPage.tsx`:
- Around line 103-106: handleEnterEditMode currently only clears
selectedBookIds, leaving filteredBookIds stale which can cause a transient
mismatch for "select all"; update handleEnterEditMode to also reset
filteredBookIds (call the setter for filteredBookIds alongside
setSelectedBookIds) before navigating to `${ROUTES.BOOKS}?edit=true` so both
selection state and filter state are cleared when entering edit mode.
---
Nitpick comments:
In `@src/features/book/book.api.ts`:
- Around line 186-188: getBookTimeline의 mock 분기에서 책 식별자(personalBookId)를 빼먹어
mock 동작이 실제 API와 다릅니다; 수정 방법은 USE_MOCK 분기에서 getMockBookRecords를 호출할 때 함수 시그니처에
맞춰 personalBookId를 첫번째 인수로 전달하고 필요하면 GetBookTimelineParams 타입을 두번째 인수로 넘기도록
변경하되, getMockBookRecords 정의(또는 호출 지점)와 getBookTimeline 함수 시그니처(personalBookId,
params)를 일치시키세요.
- Around line 150-152: The mock branch currently returns getMockMyGatherings()
without forwarding the original request parameters, so cursor/filter semantics
are lost; update the USE_MOCK branches to call getMockMyGatherings(params) and
getMockBookReviewHistory(params, bookId) (or equivalent mock helpers) and ensure
the mock helpers accept and honor params (cursor, limit, filters) and bookId so
mock behavior mirrors getMyGatherings and getBookReviewHistory cursor/filter
logic; update the mock helper signatures and any call sites (references:
USE_MOCK, getMockMyGatherings, getMyGatherings, getBookReviewHistory, bookId,
params) accordingly.
- Around line 316-322: In deleteBook, add an early-guard for empty or missing
bookIds before any network/mock behavior: check if (!bookIds || bookIds.length
=== 0) and return immediately (e.g., return Promise.resolve() or void) to avoid
calling getMockDeleteResponse or api.delete with an empty array; update the
function so the emptiness check occurs before the USE_MOCK branch and reference
the deleteBook function, bookIds variable, getMockDeleteResponse, api.delete,
and BOOK_ENDPOINTS.DELETE when making the change.
In `@src/features/book/book.mock.ts`:
- Around line 333-338: The function getMockToggleBookReadingStatus mutates the
shared module-level mockBookDetail (symbol mockBookDetail), causing test state
leakage; change it to avoid mutating mockBookDetail by computing the toggled
status locally and returning a new BookDetail object (e.g., derive newStatus =
mockBookDetail.bookReadingStatus === 'READING' ? 'COMPLETED' : 'READING' and
return { ...mockBookDetail, bookReadingStatus: newStatus, bookId }) rather than
assigning to mockBookDetail; if shared mutation is required elsewhere, add and
export a clear/reset utility (e.g., resetMockBookDetail) to reinitialize
mockBookDetail for tests.
In `@src/features/book/components/BookLogActionMenu.tsx`:
- Around line 52-54: BookLogActionMenu의 아이콘 전용 버튼(<button> wrapping
<EllipsisVertical />)에 스크린리더용 의미가 없으니 버튼에 명확한 aria-label을 추가하고, 버튼이 팝업 메뉴를 열면
aria-haspopup="true"와 aria-expanded 상태 관리를 추가하도록 수정하세요; 예: BookLogActionMenu
컴포넌트에서 해당 <button> 요소를 찾아 적절한 설명(예: "더보기 옵션")을 aria-label로 넣고, 메뉴 열림/닫힘 상태가 있으면
aria-expanded를 반영하도록 연결하세요.
In `@src/features/book/components/BookLogList.tsx`:
- Around line 254-256: The key for MeetingPreOpinionItem is currently built from
gatheringName+sharedAt which can change and cause React reconciliation bugs;
update the key to use a stable unique identifier from the preOpinion object
(e.g., item.preOpinion.preOpinionId or item.preOpinion.meetingId) inside the
BookLogList component where MeetingPreOpinionItem is rendered, and if that ID
field might be missing ensure you fall back to another immutable identifier
(e.g., a stable UUID or a concatenation of immutable IDs like gatheringId with
sharedAt) so the key is reliably stable across updates.
- Around line 47-67: The three useMutation hooks (mutate: deletePersonalRecord,
deletePreOpinion, deleteRetrospective) lack onError handlers; add onError
callbacks to each to show user feedback (e.g., a toast or alert) and optionally
log the error, and ensure queryClient.invalidateQueries({ queryKey:
bookRecordsKeys.all }) is still called or rollback state if needed; implement
the onError for the mutationFn wrappers (deleteBookRecord,
deleteMyPreOpinionAnswer, deletePersonalRetrospective) to display a clear error
message and include the error object for debugging while keeping the existing
onSuccess logic.
In `@src/features/keywords/keywords.mock.ts`:
- Around line 728-731: getMockKeywords currently returns the shared
mockKeywordsResponse object directly, which can lead to downstream mutation and
data contamination; update getMockKeywords to return a cloned copy of
mockKeywordsResponse (e.g., use a deep clone mechanism such as structuredClone
or JSON.parse(JSON.stringify(...)) depending on environment) so callers receive
an immutable copy and the original mockKeywordsResponse stays unchanged.
In `@src/pages/Books/BookDetailPage.tsx`:
- Line 21: Replace the hardcoded route string in the SubPageHeader call with the
route constant to centralize route definitions: locate the SubPageHeader
component usage (SubPageHeader label="내 책장" to="/books"
disableShadow={isBookLogSticky}) and change the to prop to use ROUTES.BOOKS so
the page follows the app's ROUTES constant system and avoids mismatched paths
when routes change.
In `@src/shared/constants/routes.ts`:
- Around line 52-53: MEETING_RETROSPECTIVE_PERSONAL duplicates the existing
PERSONAL_RETROSPECTIVE route constant; consolidate to a single source of truth
by removing the duplicate constant and updating all usages to reference
PERSONAL_RETROSPECTIVE, or keep MEETING_RETROSPECTIVE_PERSONAL as a deprecated
alias (add a clear comment like "DEPRECATED: use PERSONAL_RETROSPECTIVE") that
forwards to PERSONAL_RETROSPECTIVE to preserve compatibility; update any
imports/usages to the unified constant (references:
MEETING_RETROSPECTIVE_PERSONAL, PERSONAL_RETROSPECTIVE).
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
src/features/book/book.api.tssrc/features/book/book.endpoints.tssrc/features/book/book.mock.tssrc/features/book/book.types.tssrc/features/book/components/BookCard.tsxsrc/features/book/components/BookInfo.tsxsrc/features/book/components/BookList.tsxsrc/features/book/components/BookLogActionMenu.tsxsrc/features/book/components/BookLogList.tsxsrc/features/book/components/BookLogListSkeleton.tsxsrc/features/book/components/MeetingGroupRecordItem.tsxsrc/features/book/components/MeetingPreOpinionItem.tsxsrc/features/book/components/MeetingRetrospectiveItem.tsxsrc/features/book/components/PersonalRecordItem.tsxsrc/features/book/components/PersonalRecordModal.tsxsrc/features/book/components/index.tssrc/features/book/hooks/useBookDetail.tssrc/features/book/hooks/useBookRecords.tssrc/features/book/hooks/useBooks.tssrc/features/book/hooks/useDeleteBook.tssrc/features/book/index.tssrc/features/keywords/keywords.api.tssrc/features/keywords/keywords.mock.tssrc/pages/Books/BookDetailPage.tsxsrc/pages/Books/BookListPage.tsxsrc/shared/constants/routes.tssrc/shared/layout/RootLayout.tsxsrc/shared/ui/StarRatingFilter.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/features/book/book.mock.ts (1)
335-340: 공유 목데이터 직접 변경으로 인한 상태 누수 위험문제:
mockBookDetail.bookReadingStatus를 직접 변경하면 모듈 수준 상태가 호출 간에 유지됩니다.
영향: 테스트 간 격리가 깨지고, 개발 중 예기치 않은 토글 상태가 발생할 수 있습니다.
대안: 반환 시 새 객체를 생성하고 원본은 유지하세요.수정 예시
export const getMockToggleBookReadingStatus = async (bookId: number): Promise<BookDetail> => { await delay(MOCK_DELAY) - mockBookDetail.bookReadingStatus = - mockBookDetail.bookReadingStatus === 'READING' ? 'COMPLETED' : 'READING' - return { ...mockBookDetail, bookId } + const newStatus = mockBookDetail.bookReadingStatus === 'READING' ? 'COMPLETED' : 'READING' + return { ...mockBookDetail, bookId, bookReadingStatus: newStatus } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/book.mock.ts` around lines 335 - 340, The function getMockToggleBookReadingStatus currently mutates the module-level mockBookDetail causing state leakage across calls; instead compute the toggled status locally, construct and return a new BookDetail object (e.g., using spread of mockBookDetail with bookReadingStatus set to the toggled value and bookId) without assigning back to mockBookDetail, preserving the original mockBookDetail; keep the existing delay(MOCK_DELAY) and return the new object.src/pages/Books/BookListPage.tsx (1)
92-99: 삭제 성공 시 사용자 피드백 누락문제: 삭제 성공 후
navigate(ROUTES.BOOKS)만 실행되고 성공 메시지가 없습니다.
영향: 사용자가 삭제 완료 여부를 명확히 인지하기 어렵습니다.
대안: 책 등록 성공 시처럼showToast로 성공 메시지를 추가하세요.수정 예시
try { await deleteBook(bookIds) + showToast('책이 삭제되었습니다.') navigate(ROUTES.BOOKS) } catch {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Books/BookListPage.tsx` around lines 92 - 99, The delete flow currently calls deleteBook(bookIds) then navigate(ROUTES.BOOKS) without user feedback; update the success path in the try block (the block containing deleteBook and navigate) to call showToast with a success title/message (matching the app's toast style) before or immediately after navigate so users see a "삭제 완료" success message; keep the catch block using openConfirm as-is for failures. Ensure you reference the existing deleteBook, navigate, ROUTES.BOOKS, openConfirm and showToast symbols when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/features/book/components/BookLogList.tsx`:
- Around line 260-275: The key for the PRE_OPINION case can collide because
gatheringName+sharedAt is not guaranteed unique; update the React key used in
the MeetingPreOpinionItem render to include the unique meeting identifier
(meetingId) so the key uses meetingId along with gatheringName and sharedAt
(reference: MeetingPreOpinionItem render, item.preOpinion.gatheringName,
item.preOpinion.sharedAt, item.preOpinion.meetingId); ensure the onDelete logic
that calls deletePreOpinion remains unchanged.
---
Nitpick comments:
In `@src/features/book/book.mock.ts`:
- Around line 335-340: The function getMockToggleBookReadingStatus currently
mutates the module-level mockBookDetail causing state leakage across calls;
instead compute the toggled status locally, construct and return a new
BookDetail object (e.g., using spread of mockBookDetail with bookReadingStatus
set to the toggled value and bookId) without assigning back to mockBookDetail,
preserving the original mockBookDetail; keep the existing delay(MOCK_DELAY) and
return the new object.
In `@src/pages/Books/BookListPage.tsx`:
- Around line 92-99: The delete flow currently calls deleteBook(bookIds) then
navigate(ROUTES.BOOKS) without user feedback; update the success path in the try
block (the block containing deleteBook and navigate) to call showToast with a
success title/message (matching the app's toast style) before or immediately
after navigate so users see a "삭제 완료" success message; keep the catch block
using openConfirm as-is for failures. Ensure you reference the existing
deleteBook, navigate, ROUTES.BOOKS, openConfirm and showToast symbols when
making the change.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/features/book/book.api.tssrc/features/book/book.mock.tssrc/features/book/components/BookLogActionMenu.tsxsrc/features/book/components/BookLogList.tsxsrc/pages/Books/BookDetailPage.tsxsrc/pages/Books/BookListPage.tsxsrc/shared/constants/routes.ts
💤 Files with no reviewable changes (1)
- src/shared/constants/routes.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/book/components/BookLogActionMenu.tsx
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/features/book/components/BookLogList.tsx (1)
48-77: 삭제 mutation의isPending상태 미사용문제: 세 개의 삭제 mutation 모두
isPending상태를 destructure하지 않음.
영향: 사용자가 삭제 버튼을 연속 클릭하면 중복 요청이 발생할 수 있음.
대안:isPending상태를 활용해 버튼 비활성화 또는 로딩 피드백 제공.수정 예시
- const { mutate: deletePersonalRecord } = useMutation({ + const { mutate: deletePersonalRecord, isPending: isDeletingRecord } = useMutation({ mutationFn: (recordId: number) => deleteBookRecord(bookId, recordId), ... })이후
onDelete핸들러에서isPending상태를 자식 컴포넌트에 전달하거나, 삭제 진행 중일 때 추가 호출을 방지하는 로직 추가 권장.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/book/components/BookLogList.tsx` around lines 48 - 77, The three deletion mutations (deletePersonalRecord, deletePreOpinion, deleteRetrospective) don’t destructure or expose their isPending state, so rapid repeated clicks can trigger duplicate calls; update each useMutation call to destructure isPending (e.g., const { mutate, isPending } = useMutation(...)) and then use those isPending flags in the corresponding onDelete handlers or pass them into child components to disable the delete button or short-circuit additional mutate calls while a request is in flight.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/features/book/components/BookLogList.tsx`:
- Around line 48-77: The three deletion mutations (deletePersonalRecord,
deletePreOpinion, deleteRetrospective) don’t destructure or expose their
isPending state, so rapid repeated clicks can trigger duplicate calls; update
each useMutation call to destructure isPending (e.g., const { mutate, isPending
} = useMutation(...)) and then use those isPending flags in the corresponding
onDelete handlers or pass them into child components to disable the delete
button or short-circuit additional mutate calls while a request is in flight.
There was a problem hiding this comment.
이 컴포넌트에서는 다른 곳과 다르게 mutation 훅을 분리하지 않으신 이유가 있나요?
There was a problem hiding this comment.
놓쳤던 것 같습니다. 분리했습니다!
* [refactor] 사전의견 폴더 병합 (#92) * refactor: 사전의견 관련 폴더 병합 (#91) * fix: SubPageHeader 그림자 prop 추가 (#91) * refactor: 코드 포매팅 (#91) * fix: 빌드 에러 수정 (#91) * fix: 개인 회고 nullable 필드 처리 및 목 모드 조기 반환 추가 (#91) * chore: 개인회고 관련 파일 삭제 (#91) * feat: 약속 회고 API 레이어 구축 (#94) (#95) * feat: 약속 회고 API 레이어 구축 (#94) STT Job 생성, 요약 조회/수정/발행 API 함수 및 React Query 훅 구현. AbortController 기반 STT 요청 취소 지원, setQueryData 캐시 전략 적용. * style: prettier 포맷 수정 (#94) * refactor: 코드 리뷰 반영 (#94) - api 함수 api.* 헬퍼로 통일 - useCreateSttJob race condition 수정 - useCreateSttJob 언마운트 시 자동 취소 - KeyPointUpdateRequest 타입 alias로 변경 * feat: 에러 코드 추가 및 사전 의견 버튼 disabled 처리 (#97) (#98) 신규 에러 코드 5개(E000, M016, M017, R106, R108) 추가. MyMeetingListItem에 preOpinionTemplateConfirmed 필드 추가 후, 메인페이지 예정 약속의 사전 의견 작성 버튼 disabled 처리 반영. * [feat] 약속회고 생성 전 플로우 UI 및 기능 개발 (#100) * feat: 약속 완료 시 약속 상세 페이지 UI 변경(#87) * feat: 약속 회고 상세 페이지 추가 (#87) * feat:아코디언 컴포넌트 추가(#87) * design: 아코디언 컴포넌트css 수정(#87) * feat: 수집된 사전의견 조회 api 개발(#87) * feat: Dropzone 공통컴포넌트 생성 및 파일 업로드 기능구현(#87) * design: AI요약 버튼 디자인 수정(#87) * style: 프리티어(#87) * refactor: 파일 업로드 검증 강화 및 의존성 정리 (#87) Dropzone 파일 타입 검증 기능 추가 및 radix-ui 의존성 정리로 코드 품질 개선. - Dropzone 파일 타입 검증 로직 추가 - 파일 타입 불일치 시 onTypeRejected 콜백 호출 - alert를 toast로 변경하여 일관된 에러 처리 - radix-ui 제거 후 @radix-ui/react-accordion 추가 - Accordion, AlertIcon 컴포넌트 코드 정리 - Import/export 경로 정리 및 불필요한 mock export 제거 * refactor: 에러처리 개선 (#87) * design: 아코디언css 수정 (#87) * refactor: GetCollectedAnswersParams 배럴 추가(#87) * style: 주석제거(#87) * refactor: AlertIcon 색 커스텀 기능 추가(#87) * refactor: 회고 아이콘을 SVG 파일로 전환 (#87) - MeetingRetroIcon, PersonalRetroIcon 컴포넌트를 SVG 파일로 대체 - useCollectedAnswers 훅의 유효성 검사 강화 (정수 체크, pageSize 검증) - MeetingDetailPage 에러 처리 중복 실행 방지 * refactor: AlertIcon적용 (#87) * refactor: AlertIcon size prop추가(#87) * refactor: 주제 목록 에러 처리 개선 (#87) * fix: 버튼 프로퍼티수정(#87) * feat: 회고 상태 기반 라우트 분기 처리 추가 (#87) * feat: 목데이터에 회고상태 추가(#87) * refactor: params검증로직 페이지에서 처리(#87) * [feat] 개인회고 작성 및 조회 화면 구현 (#101) * feat: 개인 회고 작성 화면 구현 (#90) * feat: 개인 회고 전송 기능 구현 (#90) * feat: 개인회고 조회 화면 구현 (#90) * feat: 개인회고 삭제, 수정 구현 (#90) * chore 미사용 유틸리티 제거 (#90) * refactor: 코드 포매팅 (#90) * fix: 코드리뷰 반영 (#90) * fix: 코드 리뷰 반영 (#90) * refactor: 코드 포매팅 (#90) * refactor: 개인회고 폴더 정리 (#90) * fix: 코드 리뷰 반영 (#90) * feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93) (#102) * feat: 약속 회고 결과 페이지 UI 및 기능 개발 (#93) - AI 요약 결과 조회, 수정, 발행 기능 구현 - TopicSummaryCard: 구조화된 개별 input으로 주요포인트 편집 - FormPageHeader: children, onBack prop 추가 - 발행 성공 시 DetailPage로 이동 - AiLoadingOverlay SVG 직접 import 방식으로 변경 - summary 관련 목데이터 추가 * fix: 코드 리뷰 반영 (#93) - TopicSummaryCard 편집 모드 keyPoints map key를 인덱스에서 stable id로 교체 (EditableKeyPoint 타입 추가, crypto.randomUUID()로 id 생성) - mock abort 에러를 DOMException에서 Error('canceled')로 변경해 axios 취소 동작 일치 - 회고 요약 목데이터를 전역 단일 객체에서 meetingId별 Record로 리팩터링 - MeetingRetrospectiveResultPage에 isError 처리 및 다시 시도 버튼 추가 - window.history.replaceState를 navigate(location.pathname, replace)로 교체 - FormPageHeader props를 discriminated union으로 타입 강화 * style: prettier 포맷 수정 (#93) * fix: 코드 리뷰 반영 2차 (#93) - 상세 항목(details)에도 EditableDetail 타입으로 stable id 적용 (추가/삭제 시 input 포커스 혼동 방지) - handleSaveEdit에서 EditableDetail[] → string[]으로 직렬화 처리 - mock STT abort: aborted 즉시 감지, once 옵션으로 리스너 중복 방지, resolve 시 리스너 정리 - meetingId 유효성 검사 강화 (Number.isInteger && > 0) * fix: 코드 리뷰 반영 3차 (#93) - guard를 핸들러 정의 이전으로 이동해 gatheringId/meetingId non-null assertion 제거 - topic prop을 항상 SummaryTopic[]으로 유지해 union 타입 불일치 해결 - EditableDetail/EditableKeyPoint를 components/index.ts에서 함께 re-export * [fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영 (#104) * refactor: 감상 기록 액션 메뉴 분리 (#59) * refactor: Book/Keywords API 목데이터와 엔드포인트 분리 (#59) * design: 감상 기록 액션 메뉴 관련 z-index 조정 (#59) * feat: 별점 필터링에 0점 추가 (#59) * refactor: 내 책장 무한스크롤 시 useInfiniteScroll 활용 (#59) * feat: 내 책장에 등록된 책이 없을 경우 툴팁 추가 (#59) * feat: 기록 타임라인의 사전의견에 답변이 없는 주제 처리 추가 (#59) * refactor: sticky한 헤더에 대해 drop-shadow-bottom 및 useScrollCollapse 활용 (#59) * fix: 책 타임라인 api 반영 (#59) * fix: 책 리스트 조회 api 반영 (#59) * fix: 책 리스트 삭제 api 반영 및 디자인 수정 (#59) * fix: 도서 API 스펙 변경에 따른 타입 및 컴포넌트 업데이트 (#59) * fix: 약속 회고 아이템에서 미연동 액션 메뉴 제거 (#59) * feat: 도서 읽기 상태 토글에 낙관적 업데이트 적용 (#59) * refactor: 코드 포매팅 (#59) * fix: 메인페이지 ReadingBooksSection API 파라미터 및 응답 필드 타입 오류 수정 (#59) * fix: 내책장 편집 모드 진입 시 filteredBookIds 초기화 (#59) * fix: mock API 파라미터 계약 및 라우트 상수 관련 수정 (#59) * fix: PRE_OPINION 아이템 React key 중복 가능성 수정 (#59) * refactor: RootLayout에서 불필요한 overflow-x-clip 제거 (#59) * refactor: BookLogList 삭제 로직을 useBookLogDeleteActions 훅으로 분리 (#59) * [feat] 약속회고 조회 및 댓글 기능 구현 (#103) * feat: 약속 회고상세 UI개발(#96) * refactor: 회고 관련 파일 구조 meeting 폴더로 재구성 (#96) * feat: 댓글 Textarea 높이조절 기능구현(#96) * feat: 약속회고 댓글 기능 구현 (#96) - 약속회고 상세 페이지에 댓글 작성/삭제 기능 추가. - 무한 스크롤로 댓글 목록 조회하며, 약속장 및 댓글 작성자만 삭제 가능. * fix: 약속회고 댓글 에러 처리 및 안정성 개선 (#96) - 댓글 조회 실패 시 에러 토스트 및 안내 메시지 표시. - currentUserId null 체크로 삭제 권한 확인 로직 개선. - 스켈레톤 count 음수값 방어 처리. - Textarea format별 height 기본값 적용. * fix: 약속회고 댓글/상세 조회 mock 처리 개선 (#96) * refactor: 약속회고 댓글 삭제 확인 모달 개선 및 코드 정리 (#96) - window.confirm을 글로벌 모달로 변경 - Mock 함수에서 불필요한 meetingId 파라미터 제거 - Textarea 컴포넌트에서 사용하지 않는 ref 제거. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * [feat] 페이지 접근 권한 전역 관리 구현 (#105) * feat: 페이지 접근 권한 전역 관리 구현 (#99) 인터셉터에서 권한 에러 감지 시 커스텀 DOM 이벤트를 dispatch하고, RootLayout의 usePermissionRedirect 훅에서 토스트와 홈 리다이렉트를 처리하여 한 곳에서 통합 관리. - errors.ts: PAGE_ACCESS_ERROR_CODES Set 추가 (6개 에러 코드) - interceptors.ts: permission-denied 커스텀 이벤트 dispatch - usePermissionRedirect: 이벤트 → 토스트 + navigate(HOME) - RootLayout: usePermissionRedirect 훅 연결 - MeetingRetrospectiveCreatePage: 이중 내비게이션 방지 * fix: 권한 에러 중복 이벤트 억제 및 핸들러 방어 코드 추가 (#99) * fix: 권한 에러 시 중복 토스트/모달 방지 (#99) * fix: 약속상세 페이지의 사전의견 버튼에 링크 연동(#106) (#107) --------- Co-authored-by: choiyoungae <109134495+choiyoungae@users.noreply.github.com> Co-authored-by: haeun <110523397+haruyam15@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
🚀 풀 리퀘스트 제안
📋 작업 내용
도서 관련 API 스펙 변경에 따라 타입, API 연동, 컴포넌트를 전반적으로 업데이트하였습니다. 아울러 감상 기록 UI/UX 개선 및 코드 구조 리팩토링을 함께 진행하였습니다.
🔧 변경 사항
API 연동 및 타입 업데이트
리팩토링
기능 추가 및 수정
📸 스크린샷 (선택 사항)
수정된 화면 또는 기능을 시연할 수 있는 스크린샷을 첨부해 주세요.
📄 기타
추가적으로 전달하고 싶은 내용이나 특별한 요구 사항이 있으면 작성해 주세요.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항