Skip to content

[fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영#104

Merged
choiyoungae merged 26 commits intodevelopfrom
fix/book-api-spec-59
Mar 5, 2026
Merged

[fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영#104
choiyoungae merged 26 commits intodevelopfrom
fix/book-api-spec-59

Conversation

@choiyoungae
Copy link
Contributor

@choiyoungae choiyoungae commented Mar 2, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

도서 관련 API 스펙 변경에 따라 타입, API 연동, 컴포넌트를 전반적으로 업데이트하였습니다. 아울러 감상 기록 UI/UX 개선 및 코드 구조 리팩토링을 함께 진행하였습니다.

🔧 변경 사항

API 연동 및 타입 업데이트

  • 책 타임라인 API 스펙 변경 반영: book.types.ts 타입 수정, book.api.ts 및 book.endpoints.ts 업데이트, useBookRecords 훅 수정
  • 책 리스트 조회 API 스펙 변경 반영: book.types.ts, book.api.ts, useBooks 훅 수정, BookCard·BookInfo·BookList 컴포넌트 업데이트
  • 책 리스트 삭제 API 연동: book.api.ts, book.endpoints.ts, useDeleteBook 훅 수정, BookListPage 디자인 수정
  • 도서 API 스펙 변경에 따른 book.types.ts, BookCard, BookList, BookLogList, MeetingGroupRecordItem 컴포넌트 전반 업데이트

리팩토링

  • Book/Keywords API 파일에서 목데이터와 엔드포인트를 별도 파일(book.mock.ts, book.endpoints.ts, keywords.mock.ts, keywords.endpoints.ts)로 분리
  • BookLogActionMenu 컴포넌트를 별도 파일로 분리하고, MeetingGroupRecordItem · MeetingPreOpinionItem · MeetingRetrospectiveItem · PersonalRecordItem에서 공통으로 활용하도록 통합
  • BookLogModal을 PersonalRecordModal로 이름 변경
  • 내 책장 무한스크롤 처리를 공통 useInfiniteScroll 훅으로 교체
  • sticky 헤더에 drop-shadow-bottom 및 useScrollCollapse 훅 활용, SubPageHeader 개선

기능 추가 및 수정

  • 도서 읽기 상태 토글에 낙관적 업데이트 적용 (useBookDetail)
  • 별점 필터링에 0점 옵션 추가 (StarRatingFilter)
  • 내 책장에 등록된 책이 없을 경우 빈 상태 안내 툴팁 추가
  • 기록 타임라인의 사전의견 항목에서 답변이 없는 주제에 대한 예외 처리 추가
  • 약속 회고 아이템(MeetingGroupRecordItem)에서 미연동 상태의 액션 메뉴 제거
  • 감상 기록 액션 메뉴 및 모달 z-index 조정

📸 스크린샷 (선택 사항)

수정된 화면 또는 기능을 시연할 수 있는 스크린샷을 첨부해 주세요.

📄 기타

추가적으로 전달하고 싶은 내용이나 특별한 요구 사항이 있으면 작성해 주세요.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 도서 기록의 타임라인 기반 조회 기능 추가
    • 도서 로그 액션 메뉴 추가 (수정·삭제)
    • 개인 기록 편집 모달 이름/흐름 개선 (PersonalRecordModal)
    • 도서 로그 항목 직접 삭제 기능 추가
  • 개선 사항

    • 일괄 도서 삭제 지원
    • 별점 필터에 0점 옵션 추가
    • 스크롤 기반 동적 헤더 그림자 적용
    • 로딩 스켈레톤 UI 및 무한 스크롤 개선
    • 별점이 null인 경우 표시 보완
    • 읽기 상태 토글 시 즉시 반영(낙관적 업데이트)

@choiyoungae choiyoungae linked an issue Mar 2, 2026 that may be closed by this pull request
@choiyoungae choiyoungae self-assigned this Mar 2, 2026
@choiyoungae choiyoungae added the bug 버그 수정 label Mar 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dedafe3d-fda4-443d-b5b0-c19c7470fb7c

📥 Commits

Reviewing files that changed from the base of the PR and between af3d7fe and 2caf553.

📒 Files selected for processing (3)
  • src/features/book/components/BookLogList.tsx
  • src/features/book/hooks/index.ts
  • src/features/book/hooks/useBookLogDeleteActions.ts

Walkthrough

북 기능의 API 엔드포인트를 중앙화하고 모의(mock) 데이터를 별도 모듈로 분리했습니다. 레코드 기반 API를 타임라인 기반 무한 스크롤로 전환하고, 타입·컴포넌트·훅을 이에 맞춰 광범위하게 갱신했습니다. (타입·엔드포인트·모의·UI·훅 변경 포함)

Changes

Cohort / File(s) Summary
API 엔드포인트 & API 표면
src/features/book/book.endpoints.ts, src/features/book/book.api.ts
엔드포인트 문자열을 BOOK_ENDPOINTS로 중앙화. API 함수들이 실서버 호출은 BOOK_ENDPOINTS.*로, 모의는 getMockX 헬퍼로 분기하도록 변경. 일부 함수 시그니처(예: deleteBook → 배열 인자, getBookRecords → getBookTimeline) 업데이트.
모의 데이터 모듈
src/features/book/book.mock.ts, src/features/keywords/keywords.mock.ts
인메모리 모의 응답과 지연/페이징 로직을 book.mock.ts로 추출·정리. keywords.mock를 비동기화하고 delay 도입.
타입 리팩터링
src/features/book/book.types.ts
records → timeline 타입 전환(예: GetBookTimelineParams/Response, TimelineItem 등), BookListItem 구조 변경(gatherings 추가), BookReadingStatus에 PENDING 추가, GetBooksParams/응답과 상태 카운트 구조(statusCounts) 변경. 여러 이전 meeting 관련 타입 제거/조정.
무한 스크롤·타임라인 훅
src/features/book/hooks/useBookRecords.ts, src/features/book/hooks/useBooks.ts, src/features/book/hooks/useBookLogDeleteActions.ts, src/features/book/hooks/useDeleteBook.ts, src/features/book/hooks/index.ts
useQueryuseInfiniteQuery 전환, 페이지 기반 cursor 페이징( cursorEventAt/cursorSourceId/cursorRating ) 지원. 삭제 훅들 배치 및 무효화 패턴 추가. 훅 재익스포트 추가.
리스트·아이템·모달 UI 변경
src/features/book/components/... (여러 파일, 하단 주요 항목 참조)
BookCard: gatherings 구조 반영(selectedGatheringId). BookList: useInfiniteScroll 도입·서버/클라이언트 필터 조정. BookLogList/BookLogListSkeleton/PersonalRecordModal/BookLogActionMenu 등 추가·교체. 여러 아이템 컴포넌트(PersonalRecordItem, MeetingRetrospectiveItem, MeetingPreOpinionItem, MeetingGroupRecordItem)에서 액션 메뉴와 삭제 지원 추가 및 데이터 형태 변경 반영.
페이지/라우팅/레이아웃
src/pages/Books/BookListPage.tsx, src/pages/Books/BookDetailPage.tsx, src/shared/constants/routes.ts
BookListPage: 편집모드 URL 쿼리로 전환·대량 삭제 단순화. BookDetailPage: useScrollCollapse 도입. ROUTES에서 PERSONAL_RETROSPECTIVE 제거.
기타 UI·유틸 변경
src/features/book/components/BookInfo.tsx, src/shared/ui/StarRatingFilter.tsx, src/features/keywords/keywords.api.ts
커버 이미지 크기 조정, 별점 필터에 0 추가, keywords.api가 실제 엔드포인트 사용으로 전환 및 상대경로화.

Sequence Diagram

sequenceDiagram
    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: 갱신된 데이터 반영
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • mgYang53
  • haruyam15
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly summarizes the main changes: updating the book detail and my bookshelf pages to reflect new API specifications. The title is specific and directly relates to the core refactoring work across API types, endpoints, and components.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/book-api-spec-59

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

@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

🧹 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: getBookTimeline mock 호출에서 책 식별자가 누락됩니다.

문제: 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, getBookReviewHistory mock 호출이 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

📥 Commits

Reviewing files that changed from the base of the PR and between a773eb2 and 9c068a6.

📒 Files selected for processing (28)
  • src/features/book/book.api.ts
  • src/features/book/book.endpoints.ts
  • src/features/book/book.mock.ts
  • src/features/book/book.types.ts
  • src/features/book/components/BookCard.tsx
  • src/features/book/components/BookInfo.tsx
  • src/features/book/components/BookList.tsx
  • src/features/book/components/BookLogActionMenu.tsx
  • src/features/book/components/BookLogList.tsx
  • src/features/book/components/BookLogListSkeleton.tsx
  • src/features/book/components/MeetingGroupRecordItem.tsx
  • src/features/book/components/MeetingPreOpinionItem.tsx
  • src/features/book/components/MeetingRetrospectiveItem.tsx
  • src/features/book/components/PersonalRecordItem.tsx
  • src/features/book/components/PersonalRecordModal.tsx
  • src/features/book/components/index.ts
  • src/features/book/hooks/useBookDetail.ts
  • src/features/book/hooks/useBookRecords.ts
  • src/features/book/hooks/useBooks.ts
  • src/features/book/hooks/useDeleteBook.ts
  • src/features/book/index.ts
  • src/features/keywords/keywords.api.ts
  • src/features/keywords/keywords.mock.ts
  • src/pages/Books/BookDetailPage.tsx
  • src/pages/Books/BookListPage.tsx
  • src/shared/constants/routes.ts
  • src/shared/layout/RootLayout.tsx
  • src/shared/ui/StarRatingFilter.tsx

Copy link

@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

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 82da350 and 42f6b3e.

📒 Files selected for processing (7)
  • src/features/book/book.api.ts
  • src/features/book/book.mock.ts
  • src/features/book/components/BookLogActionMenu.tsx
  • src/features/book/components/BookLogList.tsx
  • src/pages/Books/BookDetailPage.tsx
  • src/pages/Books/BookListPage.tsx
  • src/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

Copy link

@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.

🧹 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.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42f6b3e and af3d7fe.

📒 Files selected for processing (1)
  • src/features/book/components/BookLogList.tsx

Copy link
Contributor

@mgYang53 mgYang53 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

이 컴포넌트에서는 다른 곳과 다르게 mutation 훅을 분리하지 않으신 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

놓쳤던 것 같습니다. 분리했습니다!

@choiyoungae choiyoungae merged commit 5a817d3 into develop Mar 5, 2026
1 check passed
@choiyoungae choiyoungae deleted the fix/book-api-spec-59 branch March 5, 2026 14:42
mgYang53 added a commit that referenced this pull request Mar 7, 2026
* [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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 책 상세 페이지, 내 책장 페이지 API 스펙 반영

3 participants