FE 반영 시점은 별도로 정한다. 이 이슈는 반영해야 할 변경을 누락 없이 모으는 용도 다.
[feat(storage): 판매자·관리자 업로드 presign 신설 + 미디어 URL 소유권 검증 11건 #347 ] ProfileImageUploadUrl → UploadUrl (타입명, 필드 동일). createProfileImageUploadUrl 반환 타입
[feat(storage): 판매자·관리자 업로드 presign 신설 + 미디어 URL 소유권 검증 11건 #347 ] ReviewMediaUploadUrl → UploadUrl (타입명, 필드 동일). createReviewMediaUploadUrl 반환 타입
[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349 ] MyPageCounts.customDraftCount 삭제
[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349 ] MyPageCounts.couponCount 삭제 (항상 0이었음)
[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349 ] ViewerCounts.cartItemCount 삭제 — FE viewerCounts·마이페이지 코드 참조 중
[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353 ] 커서형 *Connection의 nextCursor: ID → String (22곳). 커서는 불투명 토큰 — 받은 문자열을 그대로 되돌려 보내면 무영향
[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353 ] 목록 입력의 cursor: ID → String (21개 input)
[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353 ] MyConversationsInput·ConversationMessagesInput 삭제 → myConversations(input: CursorInput)·conversationMessages(conversationId, input: CursorInput). CursorInput { cursor: String, limit: Int } 필드명 동일, limit 기본 30→20·상한 50→100
[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353 ] StoreProductConnection.totalCount 추가 (비파괴)
[refactor(store): 전역 pickup feature 삭제, 매장 픽업 스케줄을 pickupCalendar/pickupTimeSlots로 개명 #357 ] 전역 pickupCalendar(yearMonth)·pickupTimeSlots(date) 삭제 — BE 대체 없음. FE 3흐름(홈 MainPickupBottomSheet·픽업매장 필터·매장 상세): 홈·필터는 고정 정책(10:00~20:00 / 30분 간격 / 오늘부터 30일 / 당일 리드타임 60분)을 FE에서 계산, 매장 상세는 아래 매장판으로
[refactor(store): 전역 pickup feature 삭제, 매장 픽업 스케줄을 pickupCalendar/pickupTimeSlots로 개명 #357 ] 매장판 개명: storePickupCalendar(storeId, yearMonth) → pickupCalendar(storeId: ID!, yearMonth), storePickupTimeSlots(storeId, date) → pickupTimeSlots(storeId: ID!, date); 타입 StorePickupCalendar·StorePickupDay·StorePickupTimeSlots·StorePickupSlot → PickupCalendar·PickupDay·PickupTimeSlots·PickupSlot(필드 동일)
[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358 ] 리뷰 미디어 타입 3 → 1: ProductReviewMedia·StoreReviewMedia·MyReviewMedia → ReviewMedia(필드 동일: mediaType·mediaUrl·thumbnailUrl·sortOrder). ProductReview.media·StoreReview.media·MyReview.media의 타입명만 변경
[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358 ] AdminReview.rating: String! → Float!(관리자 리뷰 모더레이션)
[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358 ] ReviewMediaType·ReviewSort enum 정의 위치 이동(값 동일). productReviews·storeReviews 시그니처 불변
[refactor(store): 매장 카드 StoreCard 1개로 합성 (인기·오늘 픽업·검색·찜 목록) #360 ] 매장 카드 합성(StoreCard): PopularStore { rank, store: StoreCard }, TodayPickupStore { store: StoreCard, slots }, WishlistedStoreSummary → WishlistedStore { store: StoreCard, addedAt }(storeId→store.id, imageUrls→store.cakeImageUrls), SearchStoreConnection.items: [StoreCard!]!(SearchStore 삭제). StoreCard 필드: id·storeName·profileImageUrl·ratingAverage·reviewCount·regionLabel·cakeImageUrls·isWishlisted. FE 쿼리는 카드 필드를 store { ... } 아래로 중첩
[refactor(product): 상품 카드 ProductCard 1개로 합성 (인기·판매 Best·검색·매장 상품·찜·최근 본 상품) #361 ] 상품 카드 합성(ProductCard): PopularCake { rank, product: ProductCard }(홈 popularCakes·검색 진입 realtimeBestCakes), StoreProduct { product: ProductCard, description, currency, categoryIds }, WishlistItemSummary → WishlistItem { product: ProductCard, addedAt }(productId→product.id·productName→product.name·representativeImageUrl→product.thumbnailUrl), RecentViewedProductSummary → RecentViewedProduct { product: ProductCard, viewedAt }(myRecentViewedProducts·myPageOverview.recentViewedProducts), SearchProductConnection.items: [ProductCard!]!(SearchProduct 삭제). ProductCard 필드: id·storeId·name·thumbnailUrl·regularPrice·salePrice·discountRate·storeName·regionLabel·ratingAverage·reviewCount·isWishlisted(popularCakes·realtimeBestCakes·storeProducts도 로그인 시 찜 여부를 채움). RandomCake는 그대로
[refactor(order): 주문 품목·상태 이력 출력을 공용 트리 1벌로 통합 (OrderItemDetail·OrderStatusHistory) #362 ] 주문 상세 출력 공용 트리(D31): MyOrderDetail.items: [MyOrderItem!]!(← MyOrderItemDetail) — MyOrderItem { item: OrderItemDetail!, representativeImageUrl, hasMyReview, canWriteReview }, 품목 필드는 item 아래로(orderItemId→item.id, productName·quantity·regularPrice·salePrice·itemSubtotalPrice는 item.*, selectedOptions→item.optionItems{id,groupName,optionTitle,priceDelta}, customTexts→item.customTexts{id,tokenKey,defaultText,valueText,sortOrder}, customFreeEdits→item.freeEdits{id,cropImageUrl,descriptionText,sortOrder,attachments{id,imageUrl,sortOrder}} — attachmentImageUrls 문자열 배열이 객체 배열로). MyOrderDetail.statusHistories: [OrderStatusHistory!]!(← MyOrderStatusHistory, id 추가·나머지 동일). FE 현재 myOrderDetail 미사용 — 기록만. 판매자·관리자 주문 상세는 SellerOrder*/AdminOrder* 6타입씩 → 공용 OrderItemDetail·OrderStatusHistory(*Snapshot 접미 제거: productNameSnapshot→productName, regularPriceSnapshot→regularPrice, salePriceSnapshot→salePrice, groupNameSnapshot→groupName, optionTitleSnapshot→optionTitle, optionPriceDeltaSnapshot→priceDelta, tokenKeySnapshot→tokenKey, defaultTextSnapshot→defaultText)
배경
MSA 전환 Phase 0(정리)에서 SDL 파괴적 변경을 전부 허용하기로 결정했다(분석 문서 D11). FE(
caquick-fe-v2)는 BE SDL을 codegen으로 직접 참조하므로, 머지 시점부터 타입이 어긋난다.FE 반영 시점은 별도로 정한다. 이 이슈는 반영해야 할 변경을 누락 없이 모으는 용도다.
2026-09-15 첫 시도(PR #327~#343)는 브랜치째 원복됐고, 2026-09-18 플랜 v2로 재시작했다. 아래 목록은 재시작 이후 머지분만 담는다.
규칙
- [#PR번호] <변경 요약>pnpm codegen을 돌린다:GRAPHQL_SCHEMA_URL='<caquick-be>/src/features/**/*.graphql'목록
타입·필드 변경 (codegen 재생성 필요)
[feat(storage): 판매자·관리자 업로드 presign 신설 + 미디어 URL 소유권 검증 11건 #347]
ProfileImageUploadUrl→UploadUrl(타입명, 필드 동일).createProfileImageUploadUrl반환 타입[feat(storage): 판매자·관리자 업로드 presign 신설 + 미디어 URL 소유권 검증 11건 #347]
ReviewMediaUploadUrl→UploadUrl(타입명, 필드 동일).createReviewMediaUploadUrl반환 타입[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349]
MyPageCounts.customDraftCount삭제[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349]
MyPageCounts.couponCount삭제 (항상 0이었음)[chore(prisma): 미사용 모델·컬럼 삭제 (Cart*·CustomDraft*·SearchEvent 잔재·couponCount) #349]
ViewerCounts.cartItemCount삭제 — FEviewerCounts·마이페이지 코드 참조 중[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353] 커서형
*Connection의nextCursor: ID→String(22곳). 커서는 불투명 토큰 — 받은 문자열을 그대로 되돌려 보내면 무영향[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353] 목록 입력의
cursor: ID→String(21개 input)[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353]
MyConversationsInput·ConversationMessagesInput삭제 →myConversations(input: CursorInput)·conversationMessages(conversationId, input: CursorInput).CursorInput { cursor: String, limit: Int }필드명 동일, limit 기본 30→20·상한 50→100[refactor(common): 커서 페이지네이션 구현 5벌 → 1벌 통합, 커서 String·CursorInput 통일 #353]
StoreProductConnection.totalCount추가 (비파괴)[refactor(store): 전역 pickup feature 삭제, 매장 픽업 스케줄을 pickupCalendar/pickupTimeSlots로 개명 #357] 전역
pickupCalendar(yearMonth)·pickupTimeSlots(date)삭제 — BE 대체 없음. FE 3흐름(홈 MainPickupBottomSheet·픽업매장 필터·매장 상세): 홈·필터는 고정 정책(10:00~20:00 / 30분 간격 / 오늘부터 30일 / 당일 리드타임 60분)을 FE에서 계산, 매장 상세는 아래 매장판으로[refactor(store): 전역 pickup feature 삭제, 매장 픽업 스케줄을 pickupCalendar/pickupTimeSlots로 개명 #357] 매장판 개명:
storePickupCalendar(storeId, yearMonth)→pickupCalendar(storeId: ID!, yearMonth),storePickupTimeSlots(storeId, date)→pickupTimeSlots(storeId: ID!, date); 타입StorePickupCalendar·StorePickupDay·StorePickupTimeSlots·StorePickupSlot→PickupCalendar·PickupDay·PickupTimeSlots·PickupSlot(필드 동일)[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358] 리뷰 미디어 타입 3 → 1:
ProductReviewMedia·StoreReviewMedia·MyReviewMedia→ReviewMedia(필드 동일: mediaType·mediaUrl·thumbnailUrl·sortOrder).ProductReview.media·StoreReview.media·MyReview.media의 타입명만 변경[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358]
AdminReview.rating: String!→Float!(관리자 리뷰 모더레이션)[refactor(review): 상품·매장 리뷰 목록 파이프라인 공용화 (features/review 신설) #358]
ReviewMediaType·ReviewSortenum 정의 위치 이동(값 동일).productReviews·storeReviews시그니처 불변[refactor(store): 매장 카드 StoreCard 1개로 합성 (인기·오늘 픽업·검색·찜 목록) #360] 매장 카드 합성(
StoreCard):PopularStore { rank, store: StoreCard },TodayPickupStore { store: StoreCard, slots },WishlistedStoreSummary→WishlistedStore { store: StoreCard, addedAt }(storeId→store.id,imageUrls→store.cakeImageUrls),SearchStoreConnection.items: [StoreCard!]!(SearchStore삭제).StoreCard필드: id·storeName·profileImageUrl·ratingAverage·reviewCount·regionLabel·cakeImageUrls·isWishlisted. FE 쿼리는 카드 필드를store { ... }아래로 중첩[refactor(product): 상품 카드 ProductCard 1개로 합성 (인기·판매 Best·검색·매장 상품·찜·최근 본 상품) #361] 상품 카드 합성(
ProductCard):PopularCake { rank, product: ProductCard }(홈popularCakes·검색 진입realtimeBestCakes),StoreProduct { product: ProductCard, description, currency, categoryIds },WishlistItemSummary→WishlistItem { product: ProductCard, addedAt }(productId→product.id·productName→product.name·representativeImageUrl→product.thumbnailUrl),RecentViewedProductSummary→RecentViewedProduct { product: ProductCard, viewedAt }(myRecentViewedProducts·myPageOverview.recentViewedProducts),SearchProductConnection.items: [ProductCard!]!(SearchProduct삭제).ProductCard필드: id·storeId·name·thumbnailUrl·regularPrice·salePrice·discountRate·storeName·regionLabel·ratingAverage·reviewCount·isWishlisted(popularCakes·realtimeBestCakes·storeProducts도 로그인 시 찜 여부를 채움).RandomCake는 그대로[refactor(order): 주문 품목·상태 이력 출력을 공용 트리 1벌로 통합 (OrderItemDetail·OrderStatusHistory) #362] 주문 상세 출력 공용 트리(D31):
MyOrderDetail.items: [MyOrderItem!]!(←MyOrderItemDetail) —MyOrderItem { item: OrderItemDetail!, representativeImageUrl, hasMyReview, canWriteReview }, 품목 필드는item아래로(orderItemId→item.id,productName·quantity·regularPrice·salePrice·itemSubtotalPrice는item.*,selectedOptions→item.optionItems{id,groupName,optionTitle,priceDelta},customTexts→item.customTexts{id,tokenKey,defaultText,valueText,sortOrder},customFreeEdits→item.freeEdits{id,cropImageUrl,descriptionText,sortOrder,attachments{id,imageUrl,sortOrder}}—attachmentImageUrls문자열 배열이 객체 배열로).MyOrderDetail.statusHistories: [OrderStatusHistory!]!(←MyOrderStatusHistory,id추가·나머지 동일). FE 현재myOrderDetail미사용 — 기록만. 판매자·관리자 주문 상세는SellerOrder*/AdminOrder*6타입씩 → 공용OrderItemDetail·OrderStatusHistory(*Snapshot접미 제거:productNameSnapshot→productName,regularPriceSnapshot→regularPrice,salePriceSnapshot→salePrice,groupNameSnapshot→groupName,optionTitleSnapshot→optionTitle,optionPriceDeltaSnapshot→priceDelta,tokenKeySnapshot→tokenKey,defaultTextSnapshot→defaultText)런타임 동작 변경 (타입 변경 없음)
[fix(user): 리뷰 미디어 URL 버킷·소유권 검증 추가 #346]
writeReview미디어 URL 소유권 검증 —mediaUrl·thumbnailUrl이createReviewMediaUploadUrl로 이 계정에 발급된 publicUrl이 아니면 BAD_USER_INPUT(""썸네일 포함). 썸네일은mediaType: IMAGE로 발급받아야 한다. 발급 URL을 그대로 넘기면 무영향[refactor(common): 에러 코드 카탈로그 도입 + extensions.code 도메인 코드 노출 (common·global·user·product·store·order·pickup·region) #354]
errors[].extensions.code값 전면 변경 — 기존BAD_USER_INPUT/UNAUTHENTICATED/FORBIDDEN/NOT_FOUND대신 카탈로그 코드(STORE_NOT_FOUND·AUTHENTICATION_REQUIRED·INVALID_CURSOR등). 기존 값은extensions.classification으로 이동(FE 분기 12곳: UNAUTHENTICATED 6·NOT_FOUND 4·FORBIDDEN 1·BAD_USER_INPUT 1).classification에CONFLICT(409) 추가. 판매자·관리자·인증·대화 경로의 미이관 예외는 07b·07c 전까지code에 분류값이 그대로 실림[refactor(common): 에러 코드 카탈로그 도입 + extensions.code 도메인 코드 노출 (common·global·user·product·store·order·pickup·region) #354] 구매자 노출 에러 메시지 영→한(프로필·닉네임·생년월일·검색어·주문 상태 전이·좋아요 등)
[refactor(common): 에러 코드 카탈로그 도입 + extensions.code 도메인 코드 노출 (common·global·user·product·store·order·pickup·region) #354] REST 봉투에
errorCode: string | null추가(비파괴,code: number유지)[refactor(auth,conversation): 에러 메시지를 카탈로그로 이관 + 인라인 예외 메시지 가드 #356] REST
/auth/seller/login·/auth/admin/login·/auth/refresh·/auth/*/change-password·OIDC 콜백 실패 메시지 영→한,errorCode에 카탈로그 코드(INVALID_CREDENTIALS·MISSING_REFRESH_TOKEN·INVALID_REFRESH_TOKEN등)[refactor(auth,conversation): 에러 메시지를 카탈로그로 이관 + 인라인 예외 메시지 가드 #356] 토큰 없음/만료 401: 메시지
Unauthorized→로그인이 필요합니다./액세스 토큰이 유효하지 않습니다.,extensions.codeUNAUTHENTICATED→AUTHENTICATION_REQUIRED/INVALID_ACCESS_TOKEN(classification은UNAUTHENTICATED유지)[fix: ValidationPipe 예외 메시지를 카탈로그 VALIDATION_FAILED 문구로 통일 #365] 검증 실패(VALIDATION_FAILED)
message'Bad Request Exception'(GraphQL)/'Validation Error'(REST) →'입력값이 올바르지 않습니다.'. FE는 메시지를 그대로 토스트에 노출(caquick-fe-v2 chore(deps-dev): bump jest and @types/jest #43)[refactor(auth,conversation): 에러 메시지를 카탈로그로 이관 + 인라인 예외 메시지 가드 #356] 대화(myConversations·conversationMessages·문의 뮤테이션)
extensions.code→ 카탈로그 코드, 메시지 영→한. 미등록 REST 경로 404는errorCode: ROUTE_NOT_FOUND영향 없음으로 확인한 머지
[fix(auth): JWT 시크릿 해석을 authConfig 단일 소스로 통일 #345] JWT 시크릿 해석을 authConfig로 통일 — SDL·REST 불변
[chore(prisma): Prisma 6.19 → 7.10 마이그레이션 (prisma-client generator·mariadb adapter·prisma.config.ts) #350] Prisma 6.19 → 7.10 마이그레이션 — SDL·REST 불변
[chore: 소형 정리 일괄 (knip 진양성·pass-through·인터페이스 토큰·상수 중복) #351] 소형 정리 일괄(토큰·pass-through·상수·매퍼) — SDL·REST 불변
[chore: 주석 간결화 전수 — "왜"만 남기고 요약·이력·출처 라벨 삭제 #364] 주석 간결화 전수(339파일) — 주석만 변경, SDL·REST·런타임 불변
[refactor(auth): 판매자·관리자 자격증명 REST 문서 데코레이터 공용화 #363] 판매자·관리자 자격증명 REST Swagger 데코레이터 공용화 — 문서 출력(스냅샷)·라우트·동작 불변
[fix(conversation): 읽음 마커 spec의 시계 의존 제거(DB 시계 사용) #352] 읽음 마커 spec 시계 의존 제거 — 테스트 전용
[refactor(seller,admin): 에러 메시지를 카탈로그로 이관 (영어 → 한국어) #355] 판매자·관리자 에러 메시지 카탈로그 이관(영→한) — 구매자 SDL·REST 불변
[refactor(store,product): 통계 집계를 ReviewReadRepository·StoreStatsRepository 1벌로 통합 #359] 리뷰·주문 통계 집계를 ReviewReadRepository·StoreStatsRepository로 통합 — SDL·REST 불변