Conversation
기획자 화면 명세를 로컬에서 관리하는 .figma/ 디렉터리를 ignore에 등록. 스크린샷/스펙 텍스트는 작업자 본인 로컬에서만 참고하도록 한다.
기획 명세에 별도 하한 정책이 없으나, 봇/오입력 방지를 위해 normalizeBirthDate에 1900-01-01 이전 날짜 거부 로직을 추가한다. 비교 기준은 normalize와 동일한 로컬 타임존 자정으로 둔다. - MIN_BIRTH_DATE 상수 추가 (user.constants.ts) - 회귀 테스트 2건 추가 (1899-12-31/1850 reject, 1900-01-01 통과)
기존 setHours(0,0,0,0) 로컬 자정 정규화 + 로컬 자정 MIN_BIRTH_DATE는 KST 환경에선 동작하나 음수 오프셋(예: America/New_York) 또는 UTC 환경에서는 '1900-01-01' 같은 ISO date string이 1899-12-31로 정렬되어 부당하게 reject되는 회귀가 있었음 (Codex 리뷰 P2). GraphQL DateTime은 ISO string을 UTC로 해석하고 DB 컬럼은 @db.Date(시간 무시)이므로 UTC 자정 기준으로 정규화/비교하도록 통일한다. - MIN_BIRTH_DATE = new Date(Date.UTC(1900, 0, 1)) - normalize: getUTCFullYear/getUTCMonth/getUTCDate로 UTC 자정 정렬 - 미래 비교의 today도 동일하게 UTC midnight 기준 - 회귀 테스트 입력을 ISO string으로 조정 + getUTCFullYear 검증
feat(user): 생년월일 1900-01-01 이전 입력 거부
기존 ^[0-9-]+\$ + 7~20자 정책은 '01-2-3' 같은 비정상 패턴도 통과시켜
사실상 비검증 상태였음. 기획 figma 명세에 맞춰 010만 13자 고정 규칙으로 강화한다.
- PHONE_REGEX = /^010-\\d{4}-\\d{4}\$/ 상수 추가
- PHONE_FORMAT_EXAMPLE 예시 문자열 상수 추가
- MIN_PHONE_LENGTH/MAX_PHONE_LENGTH 제거 (정규식으로 대체)
- normalizePhoneNumber: 정규식 단일 검증으로 단순화 + 명확한 에러 메시지
- 회귀 테스트: 정상 3건 + trim 1건 + 비정상 10건으로 보강
feat(user): 전화번호 정규식 010-XXXX-XXXX 고정
마이페이지 figma 명세에 따라 회원정보 수정 화면에서 이름 변경을 지원한다. '필수값' 표시에 따라 전송 시 trim 후 빈 문자열은 reject. - SDL UpdateMyProfileInput에 name: String 추가 - TS DTO 동기화 - service: hasName 분기 + normalizeName 검증 + repository 호출 - repository: updateProfile args에 name 추가, account/user_profile 양쪽 update를 transaction으로 묶어 부분 실패 방지 - 회귀 테스트 6건 (name 단독/trim/빈값/공백/동시업데이트/미지정 시 유지)
feat(user): 회원정보 수정에 name 필드 추가
기존 정책은 IMAGE/VIDEO 합산 10개였으나, 마이페이지 figma 명세에 따라 사진 10장 / 동영상 1개로 분리한다 (총 11개까지 허용). - MAX_IMAGE_COUNT(10) / MAX_VIDEO_COUNT(1) 상수 분리 - validateMedia: 단일 카운트에서 type별 카운트로 변경 - 에러 메시지 분리: TOO_MANY_MEDIA → TOO_MANY_IMAGES, TOO_MANY_VIDEOS - 회귀 테스트 4건 (총 11개 통과 / 사진 11 reject / 동영상 2 reject / 동영상 단독 통과)
feat(user): 리뷰 미디어 분리 제한 (사진 10 / 동영상 1)
마이페이지 figma 04-order-list 명세 '픽업 완료 상태일 경우 리뷰 작성 버튼 노출'을 백엔드 카드 단위로 노출 가능하도록 hasReviewableItem 필드를 추가한다. 조건: status === PICKED_UP && (active 리뷰 미작성 item이 1건이라도 존재). - SDL MyOrderSummary에 hasReviewableItem: Boolean! 추가 - TS DTO 동기화 - OrderRepository.findReviewableOrderIds 메서드 신규 (단일 IN 쿼리, N+1 회피) - user-order.service에서 list 매핑 시 set 조회 후 hasReviewableItem 계산 - 회귀 테스트 6건 (미작성/active리뷰/soft-delete/CONFIRMED/CANCELED/혼합)
orderRepository.findReviewableOrderIds의 빈 배열 early return 분기 커버. codecov patch coverage가 100%가 되도록 보강.
feat(user): 주문 카드에 hasReviewableItem 노출
마이페이지 figma 02-main-login의 찜 기능을 백엔드에 도입한다. 하트 버튼 클릭 시 추가/해제하는 멱등 mutation과 목록 조회, 그리고 최근 본 상품에 isWishlisted 매핑까지 함께 반영한다. ## 변경 사항 - SDL 신규: user-wishlist.graphql (myWishlist / addToWishlist / removeFromWishlist) - SDL 갱신: RecentViewedProductSummary.isWishlisted: Boolean! - 신규 service: UserWishlistService (UserBaseService 상속) - 신규 resolver: UserWishlistQueryResolver / UserWishlistMutationResolver - UserRepository: upsertWishlistItem / softDeleteWishlistItem / findWishlistedProductIds (단일 IN 쿼리, N+1 회피) / findWishlistItems - ProductRepository: existsActiveProduct (active+soft-delete 검증, 재사용 가능한 가벼운 헬퍼) - 카운트 일관성 버그 수정: countWishlistItems / getViewerCounts.wishlistCount에 deleted_at: null 필터 누락 → soft-delete된 위시리스트도 카운트에 포함되던 문제 수정 - user-mypage.service / user-recent-view.service: 매핑 시 isWishlisted 계산 (findWishlistedProductIds set으로 일괄 조회, N+1 회피) - 회귀 테스트 다수 - UserWishlistService spec 15건 - UserWishlistResolver spec 2건 (NotFound 전파 / 추가→목록→해제 시나리오) - mypage spec: recentViewedProducts.isWishlisted 매핑 검증 보강 - recent-view spec: list isWishlisted 매핑 검증 추가
countWishlistItems / getViewerCounts.wishlistCount 가 wishlist soft-delete만 필터링하고 비활성/삭제 product/store에 연결된 row는 카운트에 포함하던 문제 수정. findWishlistItems와 동일한 가시성 조건(product/store active+not-deleted)을 공유하도록 visibleWishlistWhere helper로 통일하여 카운트와 목록 길이가 항상 일치하도록 한다. (Codex 리뷰 P2: count badge vs. list contents 불일치 회피) - visibleWishlistWhere private helper 도입 (UserRepository) - getViewerCounts.wishlistCount / countWishlistItems / findWishlistItems 모두 동일 helper 사용 - 회귀 테스트 1건: 4건 wishlist 중 inactive product / soft-delete product / inactive store 의 product 3건은 카운트에서 제외, visible 1건만 카운트
feat(user): 찜 토글 + isWishlisted 노출 + 카운트 일관성 수정
📝 WalkthroughWalkthrough사용자 위시리스트 기능을 추가하고, 주문 리뷰 가능 판정·프로필의 이름 필드 업데이트·휴대폰·생년월일 검증 규칙 및 이미지/비디오별 미디어 제한을 도입했습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant WishlistMutationResolver as WishlistMutation<br/>Resolver
participant WishlistService as UserWishlist<br/>Service
participant ProductRepo as ProductRepository
participant UserRepo as UserRepository
Client->>WishlistMutationResolver: addToWishlist(productId)
WishlistMutationResolver->>WishlistService: addToWishlist(accountId, productId)
WishlistService->>ProductRepo: existsActiveProduct(productId)
ProductRepo-->>WishlistService: boolean
alt Product exists
WishlistService->>UserRepo: upsertWishlistItem(accountId, productId, now)
UserRepo-->>WishlistService: void
WishlistService-->>WishlistMutationResolver: true
else Product not found
WishlistService-->>WishlistMutationResolver: NotFoundException
end
WishlistMutationResolver-->>Client: boolean or error
sequenceDiagram
actor Client
participant WishlistQueryResolver as WishlistQuery<br/>Resolver
participant WishlistService as UserWishlist<br/>Service
participant UserRepo as UserRepository
Client->>WishlistQueryResolver: myWishlist(offset, limit)
WishlistQueryResolver->>WishlistService: myWishlist(accountId, input)
WishlistService->>UserRepo: findWishlistItems(accountId, offset, limit)
UserRepo-->>WishlistService: items[], totalCount
WishlistService->>WishlistService: map items -> WishlistItemSummary[] (대표이미지 선택)
WishlistService->>WishlistService: compute hasMore from offset+limit vs totalCount
WishlistService-->>WishlistQueryResolver: MyWishlistConnection
WishlistQueryResolver-->>Client: paginated wishlist response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Coverage report
Test suite run success868 tests passing in 77 suites. Report generated by 🧪jest coverage report action from 97af31f |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 911dfecc2e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| where: { | ||
| account_id: args.accountId, | ||
| deleted_at: null, | ||
| product_id: { in: args.productIds }, | ||
| }, |
There was a problem hiding this comment.
Apply visibility filters when marking wishlisted products
findWishlistedProductIds only checks account_id, deleted_at, and product_id, while this same commit made myWishlist/wishlistCount depend on stricter visibility (product.is_active and store.is_active, non-deleted). Because recent-view queries still include products from inactive stores, this can return isWishlisted=true for items that are excluded from both myWishlist and the wishlist count, creating inconsistent mypage/recent-view state for users. Reuse the same visibility predicate here to keep the flag aligned with the exposed wishlist surface.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/features/user/services/user-order.service.ts (1)
54-79: 빈 결과일 때는 리뷰 가능 조회를 생략해도 됩니다.
sliced가 비어 있으면findReviewableOrderIds를 호출할 필요가 없습니다. 결과는 동일하므로 빈Set으로 바로 처리하면 불필요한 repository 호출을 줄일 수 있습니다.♻️ 제안하는 수정
- const reviewableOrderIds = - await this.orderRepository.findReviewableOrderIds({ - accountId, - orderIds: sliced.map((o) => o.id), - }); + const reviewableOrderIds = + sliced.length === 0 + ? new Set<string>() + : await this.orderRepository.findReviewableOrderIds({ + accountId, + orderIds: sliced.map((o) => o.id), + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/services/user-order.service.ts` around lines 54 - 79, When sliced is empty, avoid calling this.orderRepository.findReviewableOrderIds and instead set reviewableOrderIds to an empty Set to save an unnecessary DB query; update the logic around orderRepository.findReviewableOrderIds (the call that assigns reviewableOrderIds) to check if sliced.length === 0 and assign new Set() in that branch, otherwise call findReviewableOrderIds with { accountId, orderIds: sliced.map(o => o.id) } so the rest of the mapping (using reviewableOrderIds.has(order.id.toString())) works unchanged.src/features/user/repositories/user.repository.ts (1)
397-415: 멱등 추가에서updated_at갱신은 다시 한번 보세요.현재
upsertWishlistItem은 이미 active인 항목을 다시 추가해도updated_at을 바꿉니다.addToWishlist를 진짜 no-op에 가깝게 유지하려면, 여기서는deleted_at만 복원하도록 두는 편이 더 안전합니다.🔧 Possible tweak
- update: { deleted_at: null, updated_at: args.now }, + update: { deleted_at: null },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/user/repositories/user.repository.ts` around lines 397 - 415, The upsertWishlistItem implementation currently updates updated_at on every upsert which refreshes timestamp for already-active items; change the prisma.wishlistItem.upsert call inside upsertWishlistItem so the update payload only restores deleted_at (e.g., update: { deleted_at: null }) and does not set updated_at, leaving timestamps untouched for already-active wishlist rows; locate the upsertWishlistItem function and modify the prisma.wishlistItem.upsert update object accordingly.
🤖 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/user/repositories/user.repository.ts`:
- Around line 478-507: The wishlist query in this.prisma.wishlistItem.findMany
uses only orderBy: { created_at: 'desc' } which can produce non-deterministic
ordering for items with identical timestamps; add a stable tie-breaker by
changing the orderBy to a compound sort (e.g., [{ created_at: 'desc' }, { id:
'desc' }] or another unique column) so the results and pagination are
deterministic — update the orderBy in the wishlistItem.findMany call inside the
method that returns { items: rows, totalCount } to include the secondary key.
In `@src/features/user/services/user-review.service.ts`:
- Around line 235-249: Change the validateMedia method signature and related
locals to use a stricter literal union for mediaType instead of string: update
the parameter type from { mediaType: string } to { mediaType: 'IMAGE' | 'VIDEO'
} (and the media array's element type accordingly) in validateMedia; then
simplify the loop inside validateMedia (the for-of over media) to use explicit
checks for 'IMAGE' and 'VIDEO' (or a switch) so the "not VIDEO => IMAGE"
assumption is enforced by the type system; keep the existing throw checks
against MAX_IMAGE_COUNT, MAX_VIDEO_COUNT and USER_REVIEW_ERRORS intact.
---
Nitpick comments:
In `@src/features/user/repositories/user.repository.ts`:
- Around line 397-415: The upsertWishlistItem implementation currently updates
updated_at on every upsert which refreshes timestamp for already-active items;
change the prisma.wishlistItem.upsert call inside upsertWishlistItem so the
update payload only restores deleted_at (e.g., update: { deleted_at: null }) and
does not set updated_at, leaving timestamps untouched for already-active
wishlist rows; locate the upsertWishlistItem function and modify the
prisma.wishlistItem.upsert update object accordingly.
In `@src/features/user/services/user-order.service.ts`:
- Around line 54-79: When sliced is empty, avoid calling
this.orderRepository.findReviewableOrderIds and instead set reviewableOrderIds
to an empty Set to save an unnecessary DB query; update the logic around
orderRepository.findReviewableOrderIds (the call that assigns
reviewableOrderIds) to check if sliced.length === 0 and assign new Set() in that
branch, otherwise call findReviewableOrderIds with { accountId, orderIds:
sliced.map(o => o.id) } so the rest of the mapping (using
reviewableOrderIds.has(order.id.toString())) works unchanged.
🪄 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
Run ID: a162ef24-44e1-43a9-8dee-3eba2b41a151
📒 Files selected for processing (34)
.gitignoresrc/features/order/repositories/order.repository.tssrc/features/product/repositories/product.repository.tssrc/features/user/constants/user-review-error-messages.tssrc/features/user/constants/user-wishlist-error-messages.tssrc/features/user/constants/user.constants.tssrc/features/user/repositories/user.repository.tssrc/features/user/resolvers/user-recent-view.resolver.spec.tssrc/features/user/resolvers/user-wishlist-mutation.resolver.tssrc/features/user/resolvers/user-wishlist-query.resolver.tssrc/features/user/resolvers/user-wishlist.resolver.spec.tssrc/features/user/services/user-base.service.spec.tssrc/features/user/services/user-base.service.tssrc/features/user/services/user-mypage.service.spec.tssrc/features/user/services/user-mypage.service.tssrc/features/user/services/user-order.service.spec.tssrc/features/user/services/user-order.service.tssrc/features/user/services/user-profile.service.spec.tssrc/features/user/services/user-profile.service.tssrc/features/user/services/user-recent-view.service.spec.tssrc/features/user/services/user-recent-view.service.tssrc/features/user/services/user-review.service.spec.tssrc/features/user/services/user-review.service.tssrc/features/user/services/user-wishlist.service.spec.tssrc/features/user/services/user-wishlist.service.tssrc/features/user/types/user-input.type.tssrc/features/user/types/user-mypage-output.type.tssrc/features/user/types/user-order-output.type.tssrc/features/user/types/user-wishlist-output.type.tssrc/features/user/user-mypage.graphqlsrc/features/user/user-order.graphqlsrc/features/user/user-profile.graphqlsrc/features/user/user-wishlist.graphqlsrc/features/user/user.module.ts
| if (!media || media.length === 0) return; | ||
|
|
||
| let imageCount = 0; | ||
| let videoCount = 0; | ||
| for (const m of media) { | ||
| if (m.mediaType === 'VIDEO') videoCount++; | ||
| else imageCount++; | ||
| } | ||
|
|
||
| if (imageCount > MAX_IMAGE_COUNT) { | ||
| throw new BadRequestException(USER_REVIEW_ERRORS.TOO_MANY_IMAGES); | ||
| } | ||
| if (videoCount > MAX_VIDEO_COUNT) { | ||
| throw new BadRequestException(USER_REVIEW_ERRORS.TOO_MANY_VIDEOS); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 '\bWriteReviewInput\b|mediaType' src/features/userRepository: CaQuick/caquick-be
Length of output: 16923
🏁 Script executed:
# Find GraphQL enum definition for ReviewMediaType
rg -n 'enum ReviewMediaType|ReviewMediaType' src/features/user --type graphqlRepository: CaQuick/caquick-be
Length of output: 94
🏁 Script executed:
# Check for validation decorators on input types
rg -n '@Is|@Validate|class.*Input' src/features/user/types/user-review-input.type.ts -A 10Repository: CaQuick/caquick-be
Length of output: 44
🏁 Script executed:
# Check resolver for any validation logic
rg -n '@Mutation|writeReview|@Body|@Validate' src/features/user/resolvers/user-review-mutation.resolver.ts -B 2 -A 10Repository: CaQuick/caquick-be
Length of output: 1063
🏁 Script executed:
cat src/features/user/user-review.graphqlRepository: CaQuick/caquick-be
Length of output: 1728
🏁 Script executed:
cat src/features/user/types/user-review-input.type.tsRepository: CaQuick/caquick-be
Length of output: 545
🏁 Script executed:
# Search for ReviewMediaType enum definition anywhere
rg -n 'enum.*ReviewMediaType|ReviewMediaType.*=' src/Repository: CaQuick/caquick-be
Length of output: 126
내부 validateMedia 메서드의 타입 안정성을 개선하세요.
GraphQL 스키마에서 enum ReviewMediaType { IMAGE VIDEO }로 정의되어 있고, Apollo Server가 API 경계에서 enum 값을 검증하기 때문에 외부 입력은 보호됩니다. 다만 service 내부 메서드는 개선이 필요합니다:
- Line 233:
validateMedia메서드의 매개변수 타입이{ mediaType: string }로 제너릭 문자열을 사용합니다. - 타입을
{ mediaType: 'IMAGE' | 'VIDEO' }로 변경하면 컴파일 타임에 타입 안정성이 강화됩니다. - Line 240-241의 "VIDEO가 아니면 IMAGE" 로직도 타입으로 보호받게 됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/user/services/user-review.service.ts` around lines 235 - 249,
Change the validateMedia method signature and related locals to use a stricter
literal union for mediaType instead of string: update the parameter type from {
mediaType: string } to { mediaType: 'IMAGE' | 'VIDEO' } (and the media array's
element type accordingly) in validateMedia; then simplify the loop inside
validateMedia (the for-of over media) to use explicit checks for 'IMAGE' and
'VIDEO' (or a switch) so the "not VIDEO => IMAGE" assumption is enforced by the
type system; keep the existing throw checks against MAX_IMAGE_COUNT,
MAX_VIDEO_COUNT and USER_REVIEW_ERRORS intact.
PR #81 develop→main 릴리즈에 달린 Codex/CodeRabbit 리뷰 반영. - Codex P2: findWishlistedProductIds도 visibleWishlistWhere 사용하여 myWishlist/wishlistCount와 동일한 가시성 기준 공유. 비활성 store에 속한 product가 recent-view에서 isWishlisted=true로 보이지만 myWishlist에는 안 보이는 모순 회피. - CodeRabbit Major: findWishlistItems orderBy에 product_id 보조 정렬키 추가. 같은 밀리초에 생성된 항목의 페이지 경계 흔들림 방지. - 회귀 테스트 1건: 비활성 store의 product에 wishlist가 있어도 recent-view list에서 isWishlisted=false로 매핑됨 (가시성 일관)
fix(user): wishlist 가시성 일관성 + 목록 정렬 안정화 (PR #81 리뷰 반영)
Summary
기획자 figma 마이페이지 명세(5화면) ↔ 백엔드 구현 정합화 8건을 6개 PR로 분해해 develop에 머지 완료. 본 릴리즈는 그 결과를 main으로 반영한다.
정합화 결과 (8건)
부수 효과
정량 결과
Breaking 여부
Test plan
Summary by CodeRabbit
릴리스 노트
새 기능
개선사항