feat: 고객 리뷰 관리 (작성 가능/내 리뷰) - #22
Conversation
작성 가능/내 리뷰 조회와 리뷰 작성 흐름을 mock 우선으로 연결할 수 있게 데이터 레이어를 구성합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
작성 가능 리뷰·내가 작성한 리뷰 목록과 작성 모달을 견적 페이지 패턴에 맞춰 구성합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
프로필 드롭다운에서 작성 가능/내 리뷰로 이동할 수 있게 하고, 인증 세션 구독과 앱 라우트를 함께 연결합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
리뷰 카드 클릭 시 해당 기사님 상세 페이지로 연결되도록 라우트를 추가합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
axios 대신 공통 fetch 클라이언트를 쓰도록 맞추고, 페이지네이션 응답용 getPaginated를 추가합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough리뷰 관리 경로, 탭과 공통 셸, 작성 가능 리뷰·내 리뷰 목록, 리뷰 작성 모달과 mock API가 추가되었다. 헤더는 인증 세션 구독과 접근성 프로필 메뉴를 지원하며, 페이지네이션과 리뷰 생성 후 쿼리 갱신 흐름이 연결되었다. Changes리뷰 관리 기능
인증 세션 기반 헤더
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Customer
participant WritableReviewsPageClient
participant ReviewWriteModal
participant useCreateReview
participant createReview
Customer->>WritableReviewsPageClient: 리뷰 작성 선택
WritableReviewsPageClient->>ReviewWriteModal: 선택 견적 전달
Customer->>ReviewWriteModal: 별점·내용 제출
ReviewWriteModal->>useCreateReview: 리뷰 생성 실행
useCreateReview->>createReview: 리뷰 데이터 전달
createReview-->>useCreateReview: 생성 결과 반환
useCreateReview-->>ReviewWriteModal: 성공 또는 오류 전달
ReviewWriteModal-->>WritableReviewsPageClient: 닫기 또는 토스트 표시
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (7)
src/components/review/ReviewWriteModal.tsx (1)
53-80: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win검증 분기가 도달 불가능한 죽은 코드입니다.
isSubmitDisabled가rating < 1 || trimmedContent.length < MIN_CONTENT_LENGTH를 이미 포함하므로, 그 조건에서는 버튼이 눌리지 않아handleSubmit이 호출되지 않습니다. 결과적으로ratingError/contentError는 절대 화면에 나타나지 않고, 사용자는 버튼이 왜 비활성인지 알 수 없습니다.둘 중 하나로 정리하는 편이 좋습니다.
- 버튼은
isPending일 때만 비활성화하고 검증은handleSubmit에서 수행 (에러 메시지가 실제로 노출됨, 권장)- 또는 검증 분기를 제거하고 비활성 사유를 정적 안내 문구로 대체
♻️ 제안 (에러 메시지를 살리는 방향)
- const isSubmitDisabled = - createMutation.isPending || rating < 1 || trimmedContent.length < MIN_CONTENT_LENGTH; + const isSubmitDisabled = createMutation.isPending;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/review/ReviewWriteModal.tsx` around lines 53 - 80, Update isSubmitDisabled so it only reflects createMutation.isPending, allowing handleSubmit to run for invalid rating or content and display ratingError/contentError. Keep the existing validation and mutation behavior in handleSubmit unchanged.src/components/review/WritableReviewsPageClient.tsx (2)
41-44: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
pagestate 는 클램핑되지 않아 목록이 줄어들면 값이 남습니다.
currentPage는 파생값으로 보정되지만page자체는 그대로입니다. 리뷰 작성 후 목록이 줄어 2페이지가 사라진 상태에서 다시 항목이 늘어나면, 사용자가 1페이지를 보고 있었는데 2페이지로 점프합니다. 목록 갱신 흐름이 잦은 화면이라 정리해 두면 좋습니다.♻️ 제안
const handlePageChange = (nextPage: number) => { - setPage(nextPage); + setPage(Math.min(Math.max(1, nextPage), totalPages)); window.scrollTo({ top: 0, behavior: "smooth" }); };또는
currentPage !== page일 때setPage(currentPage)로 동기화하는 방법도 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/review/WritableReviewsPageClient.tsx` around lines 41 - 44, Synchronize the writable reviews page state with the clamped currentPage value so page does not retain an invalid page after the review list shrinks. Update the pagination logic around handlePageChange and currentPage to setPage(currentPage) whenever they differ, while preserving normal navigation behavior and avoiding unnecessary updates.
26-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
totalPages가 두 곳에서 따로 계산됩니다 — 단일 소스로 정리해 주세요.Line 27에서
totalPages를 직접 계산해currentPage클램핑에 쓰고, Line 30-33에서buildMockPagination이 같은 값을 다시 계산해 Line 79/83에서 사용합니다. 두 계산이 어긋나면 클램핑 범위와Pagination의pageCount가 불일치합니다. 또한buildMockPagination은 이름 그대로 mock 유틸인데 여기서는totalPages하나만 쓰고 있어, 실 API 전환 시 잔재로 남기 쉽습니다.♻️ 제안
const totalCount = data?.length ?? 0; - const totalPages = Math.max(1, Math.ceil(totalCount / REVIEWABLE_PAGE_LIMIT) || 1); + const totalPages = Math.max(1, Math.ceil(totalCount / REVIEWABLE_PAGE_LIMIT)); const currentPage = totalCount === 0 ? 1 : Math.min(Math.max(1, page), totalPages); - const pagination = useMemo( - () => buildMockPagination(totalCount, currentPage, REVIEWABLE_PAGE_LIMIT), - [totalCount, currentPage], - ); -그리고 Line 79/83의
pagination.totalPages를totalPages로 교체하고, 미사용이 된buildMockPaginationimport를 제거하면 됩니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/review/WritableReviewsPageClient.tsx` around lines 26 - 39, Use the locally computed totalPages as the single pagination source: replace pagination.totalPages references in the page controls with totalPages, remove the buildMockPagination use and its import, and retain currentPage clamping and pageItems behavior based on the shared totalPages value.src/components/common/Header/Header.tsx (1)
174-208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
min-w-[200px],top-[calc(100%+8px)]임의값 대신 spacing 토큰 사용을 검토해 주세요.
src/styles/tokens.theme.css의 spacing 토큰으로 대체 가능하면 다른 드롭다운과 값이 어긋나는 것을 막을 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/common/Header/Header.tsx` around lines 174 - 208, Update the profile menu container in the isProfileMenuOpen rendering to replace the arbitrary min-w-[200px] and top-[calc(100%+8px)] values with the corresponding spacing tokens from tokens.theme.css, matching the established dropdown spacing conventions.Source: Path instructions
src/lib/dev-auth.ts (1)
47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win세션 변경 알림 블록이 두 함수에 복제되어 있고 rejection 처리가 없습니다. 동일한 동적 import +
.then()패턴이 두 곳에 그대로 반복되어, import 실패 시 양쪽 모두 unhandled rejection 이 됩니다. 파일 내 헬퍼 하나로 통합하고.catch()를 붙이세요.
src/lib/dev-auth.ts#L47-L50:notifySessionChangeAsync()헬퍼를 정의하고 이 블록을 호출로 대체.src/lib/dev-auth.ts#L60-L62: 같은 헬퍼 호출로 대체.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/dev-auth.ts` around lines 47 - 50, In src/lib/dev-auth.ts lines 47-50, define a shared notifySessionChangeAsync() helper that performs the dynamic session import, calls notifyAuthSessionChange, and handles failures with catch; replace the duplicated block with this helper call. Apply the same helper replacement at lines 60-62, with no separate direct change beyond using the shared helper.src/components/review/ReviewStarRating.tsx (1)
62-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winradiogroup인데 방향키 탐색(roving tabindex)이 없습니다.
각 별이
role="radio"이지만 기본tabIndex를 그대로 사용해 Tab으로 5개를 모두 개별 이동합니다. WAI-ARIA radiogroup 패턴은 화살표 키로 옵션을 이동하고 그룹 내 하나만 tab-stop이어야 하는데, 현재는 이 규약과 어긋나 스크린리더 사용자의 기대 동작과 다릅니다.♿ roving tabindex 적용 제안 (개념)
<button key={starValue} type="button" role="radio" aria-checked={starValue === clamped} aria-label={`${label} ${starValue}점`} disabled={disabled} + tabIndex={starValue === (clamped || 1) ? 0 : -1} + onKeyDown={(e) => { + if (e.key === "ArrowRight" || e.key === "ArrowLeft") { + e.preventDefault(); + const next = e.key === "ArrowRight" + ? Math.min(5, starValue + 1) + : Math.max(1, starValue - 1); + onChange(next); + } + }} className="focus-visible:ring-border-brand rounded-4 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed" onClick={() => onChange(starValue)} >as per path instructions, "키보드 접근성, focus·disabled·hover·active 상태, aria 속성, 모달 focus 관리, 색상 대비, 모바일 터치 영역을 확인한다."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/review/ReviewStarRating.tsx` around lines 62 - 81, Review the star buttons rendered in ReviewStarRating and implement the WAI-ARIA radiogroup pattern with roving tabindex: keep exactly one enabled star at tabIndex 0 and assign -1 to the others, then support ArrowLeft/ArrowRight navigation by moving focus and updating the selected value. Preserve disabled behavior, aria-checked state, and existing click handling while ensuring the focused option remains within the available stars.Source: Path instructions
src/components/review/ReviewsShell.tsx (1)
1-1: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win불필요한
use client를 제거해 주세요.
ReviewsShell은children과ReviewTabs를 조합할 뿐 클라이언트 기능을 사용하지 않습니다. 이 파일은 Server Component로 유지하고ReviewTabs만 클라이언트 경계로 두어 불필요한 클라이언트 번들을 줄이는 편이 좋습니다.As per path instructions, 불필요한 Client Component와 클라이언트 경계를 추가하지 않아야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/review/ReviewsShell.tsx` at line 1, Remove the "use client" directive from ReviewsShell so it remains a Server Component. Keep the existing children and ReviewTabs composition unchanged, allowing only ReviewTabs to remain the client boundary.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/common/Header/Header.tsx`:
- Around line 168-172: Header의 사용자 표시 영역에서 하드코딩된 "닉네임"을 세션 또는 프로필의 실제 사용자 이름으로
연결하세요. 현재 구조상 사용자 이름을 조회할 수 없다면 해당 텍스트가 임시 값임을 명시하는 TODO 주석을 추가하고, 기존 버튼 및 Text
렌더링은 유지하세요.
- Around line 56-103: Update the keydown handler in the useEffect around
profileMenuRef so ArrowDown, ArrowUp, Home, and End are handled only when
event.target is within the profile menu container; return before preventDefault
for events outside it. Keep Escape handling global while preserving the existing
menu navigation behavior and dependency array.
In `@src/components/review/MyReviewCard.tsx`:
- Around line 27-30: Update the Link element in MyReviewCard by removing
aria-labelledby and retaining the existing aria-label based on displayName, so
the link has a single accessible name.
In `@src/components/review/ReviewTabs.tsx`:
- Around line 20-22: Review the nav element in ReviewTabs and replace the
arbitrary shadow utility shadow-[0_2px_5px_0_rgba(248,248,248,0.1)] with the
matching existing shadow token from tokens.theme.css; if no equivalent token
exists, add that token there first and then reference it through the standard
utility.
In `@src/components/review/ReviewWriteModal.tsx`:
- Line 91: Update the Modal.Close usage in ReviewWriteModal so it remains
mounted while createMutation.isPending, using its supported disabled prop to
prevent closing during submission. Preserve the existing onClose behavior when
the mutation is not pending and keep focus trapped in the modal.
- Around line 84-88: Update ReviewWriteModal so Modal always receives the
onClose function, removing the conditional undefined value. Preserve the
pending-state close prevention by wrapping or guarding ReviewWriteModal’s close
handler when createMutation.isPending, while satisfying Modal’s onClose: () =>
void contract.
In `@src/components/review/WritableReviewCard.tsx`:
- Line 141: Update the move-date rendering in WritableReviewCard around
formatMoveDateLabel so invalid or empty moveDate values cannot throw during
render. Catch the formatter failure or use a safe formatter variant such as
formatMoveDateLabelSafe, and render "-" when formatting fails while preserving
the existing label for valid dates.
In `@src/lib/api/fetchInstance.ts`:
- Around line 165-197: Update requestPaginated to handle res.status === 204
before parsing or returning paginated fields, matching the existing request
behavior and preserving a safe empty-response result. Also reuse or extract the
shared fetch, header, refresh, and error-handling logic between request and
requestPaginated so both paths apply the same 204 handling consistently.
In `@src/lib/api/reviews.ts`:
- Around line 25-30: Update fetchReviewableEstimates and the related review
조회/등록 functions in reviews.ts to use the appropriate API_ROUTES endpoints with
fetchInstance or getPaginated instead of mock helpers and Promise.resolve.
Remove the mock-only branches and preserve each function’s existing return types
while wiring both server retrieval and registration to the real API.
---
Nitpick comments:
In `@src/components/common/Header/Header.tsx`:
- Around line 174-208: Update the profile menu container in the
isProfileMenuOpen rendering to replace the arbitrary min-w-[200px] and
top-[calc(100%+8px)] values with the corresponding spacing tokens from
tokens.theme.css, matching the established dropdown spacing conventions.
In `@src/components/review/ReviewsShell.tsx`:
- Line 1: Remove the "use client" directive from ReviewsShell so it remains a
Server Component. Keep the existing children and ReviewTabs composition
unchanged, allowing only ReviewTabs to remain the client boundary.
In `@src/components/review/ReviewStarRating.tsx`:
- Around line 62-81: Review the star buttons rendered in ReviewStarRating and
implement the WAI-ARIA radiogroup pattern with roving tabindex: keep exactly one
enabled star at tabIndex 0 and assign -1 to the others, then support
ArrowLeft/ArrowRight navigation by moving focus and updating the selected value.
Preserve disabled behavior, aria-checked state, and existing click handling
while ensuring the focused option remains within the available stars.
In `@src/components/review/ReviewWriteModal.tsx`:
- Around line 53-80: Update isSubmitDisabled so it only reflects
createMutation.isPending, allowing handleSubmit to run for invalid rating or
content and display ratingError/contentError. Keep the existing validation and
mutation behavior in handleSubmit unchanged.
In `@src/components/review/WritableReviewsPageClient.tsx`:
- Around line 41-44: Synchronize the writable reviews page state with the
clamped currentPage value so page does not retain an invalid page after the
review list shrinks. Update the pagination logic around handlePageChange and
currentPage to setPage(currentPage) whenever they differ, while preserving
normal navigation behavior and avoiding unnecessary updates.
- Around line 26-39: Use the locally computed totalPages as the single
pagination source: replace pagination.totalPages references in the page controls
with totalPages, remove the buildMockPagination use and its import, and retain
currentPage clamping and pageItems behavior based on the shared totalPages
value.
In `@src/lib/dev-auth.ts`:
- Around line 47-50: In src/lib/dev-auth.ts lines 47-50, define a shared
notifySessionChangeAsync() helper that performs the dynamic session import,
calls notifyAuthSessionChange, and handles failures with catch; replace the
duplicated block with this helper call. Apply the same helper replacement at
lines 60-62, with no separate direct change beyond using the shared helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f320aa8d-ebc7-47bb-b8a0-a2b6512d0d46
📒 Files selected for processing (25)
src/app/reviews/layout.tsxsrc/app/reviews/me/page.tsxsrc/app/reviews/page.tsxsrc/app/reviews/writable/page.tsxsrc/components/common/Header/Header.tsxsrc/components/review/MyReviewCard.tsxsrc/components/review/MyReviewsPageClient.tsxsrc/components/review/ReviewEmptyState.tsxsrc/components/review/ReviewPageFrame.tsxsrc/components/review/ReviewStarRating.tsxsrc/components/review/ReviewTabs.tsxsrc/components/review/ReviewWriteModal.tsxsrc/components/review/ReviewsShell.tsxsrc/components/review/WritableReviewCard.tsxsrc/components/review/WritableReviewsPageClient.tsxsrc/hooks/useCreateReview.tssrc/hooks/useMyReviews.tssrc/hooks/useReviewableEstimates.tssrc/lib/api/fetchInstance.tssrc/lib/api/reviews.tssrc/lib/auth/session.tssrc/lib/constants/appRoutes.tssrc/lib/dev-auth.tssrc/lib/mocks/reviews.mock.tssrc/types/review.ts
프로필 메뉴 키 핸들링, 모달 닫기 가드, aria, 탭 shadow 토큰, 날짜/204 방어를 반영합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
yooseohyeon
left a comment
There was a problem hiding this comment.
확인했습니다! 리뷰가 좀 오래 걸려 죄송합니다!
등록 실패는 모달 인라인 에러로 표시하고, 별점 a11y role과 헤더 메뉴 타이포를 맞춥니다. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/review/ReviewWriteModal.tsx`:
- Line 85: Reset submitError whenever the user edits the review inputs: add
setSubmitError(undefined) to both the rating and content onChange handlers in
ReviewWriteModal, while preserving their existing update behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a677e31-2846-46ad-bb80-4ea1629eaaed
📒 Files selected for processing (4)
src/components/common/Header/Header.tsxsrc/components/review/ReviewStarRating.tsxsrc/components/review/ReviewWriteModal.tsxsrc/components/review/WritableReviewsPageClient.tsx
💤 Files with no reviewable changes (1)
- src/components/review/WritableReviewsPageClient.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/review/ReviewStarRating.tsx
- src/components/common/Header/Header.tsx
제출 실패 후 별점·내용 수정 시 이전 오류 메시지가 남지 않도록 초기화합니다. Co-authored-by: Cursor <cursoragent@cursor.com>
📋 작업 내용
🔥 변경 사항
/reviews/reviewable,/reviews/me,POST /reviews)/reviews/writable,/reviews/me페이지 및 리뷰 UI 컴포넌트 추가 (목록, 작성 모달, 별점, 탭, 빈 상태)/movers/{moverId}상세 이동✅ 체크리스트
📷 스크린샷 (선택)
🔗 관련 이슈
Closes #
💬 To Reviewer
src/lib/api/reviews.ts의 주석을 해제하면 됩니다./movers/[id])가 아직 없으면 리뷰 카드 클릭 시 404가 날 수 있습니다.Summary by CodeRabbit
/reviews접속 시 작성 가능 리뷰로 자동 이동