diff --git a/src/features/product/product-home.graphql b/src/features/product/product-home.graphql index 2ee41be..62f9035 100644 --- a/src/features/product/product-home.graphql +++ b/src/features/product/product-home.graphql @@ -24,6 +24,8 @@ type RandomCakesResult { """랜덤 케이크 그리드 셀(클릭 시 케이크 상세 이동).""" type RandomCake { id: ID! + """소속 매장 ID(상세 URL 구성용: /store/{storeId}/products/{id}).""" + storeId: ID! """대표 이미지(sort_order 최소).""" thumbnailUrl: String! } @@ -79,6 +81,8 @@ input CustomCakeShowcaseInput { type CustomCakeShowcaseItem { """후기 보러가기(reviewDetail) 이동용.""" reviewId: ID! + """후기 소속 매장 ID(상세 URL 구성용: /store/{storeId}/reviews/{reviewId}).""" + storeId: ID! """좋아요순 순위(1부터).""" rank: Int! """작성자 닉네임. 탈퇴 작성자는 null(FE 익명 표기).""" @@ -95,6 +99,8 @@ type CustomCakeShowcaseItem { """인기 케이크 카드(지역명·매장명·케이크명·가격·할인율).""" type PopularCake { id: ID! + """소속 매장 ID(상세 URL 구성용: /store/{storeId}/products/{id}).""" + storeId: ID! rank: Int! name: String! """대표 이미지(sort_order 최소). 없으면 null.""" diff --git a/src/features/product/repositories/product-review.repository.ts b/src/features/product/repositories/product-review.repository.ts index 820572a..660bf13 100644 --- a/src/features/product/repositories/product-review.repository.ts +++ b/src/features/product/repositories/product-review.repository.ts @@ -201,6 +201,7 @@ export class ProductReviewRepository { async findShowcaseReviewRowsByIds(reviewIds: bigint[]): Promise< { id: bigint; + store_id: bigint; content: string | null; account: { user_profile: { nickname: string; deleted_at: Date | null } | null; @@ -216,6 +217,7 @@ export class ProductReviewRepository { where: { id: { in: reviewIds }, deleted_at: null }, select: { id: true, + store_id: true, content: true, account: { // soft-delete extension은 nested relation에 deleted_at을 주입하지 않으므로 diff --git a/src/features/product/repositories/product.repository.ts b/src/features/product/repositories/product.repository.ts index 1c1637f..363294f 100644 --- a/src/features/product/repositories/product.repository.ts +++ b/src/features/product/repositories/product.repository.ts @@ -28,6 +28,7 @@ export interface StoreProductCategoryRow { /** 인기 케이크 랭킹 후보 row. product-home 매퍼 입력. */ export interface CakeCandidateRow { id: bigint; + store_id: bigint; name: string; regular_price: number; sale_price: number | null; @@ -1000,6 +1001,7 @@ export class ProductRepository { }, select: { id: true, + store_id: true, name: true, regular_price: true, sale_price: true, @@ -1200,7 +1202,9 @@ export class ProductRepository { async findRandomCakeRows(args: { productIds: bigint[]; categoryId?: bigint; - }): Promise<{ id: bigint; images: { image_url: string }[] }[]> { + }): Promise< + { id: bigint; store_id: bigint; images: { image_url: string }[] }[] + > { if (args.productIds.length === 0) return []; return this.prisma.product.findMany({ where: { @@ -1227,6 +1231,7 @@ export class ProductRepository { }, select: { id: true, + store_id: true, images: { where: { deleted_at: null }, orderBy: { sort_order: 'asc' }, diff --git a/src/features/product/resolvers/product-home-query.resolver.spec.ts b/src/features/product/resolvers/product-home-query.resolver.spec.ts index a86e777..007ab35 100644 --- a/src/features/product/resolvers/product-home-query.resolver.spec.ts +++ b/src/features/product/resolvers/product-home-query.resolver.spec.ts @@ -81,6 +81,7 @@ describe('ProductHome Query Resolver (real DB)', () => { expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ + storeId: orderItem.store_id.toString(), reviewText: '제작 후기', beforeImageUrl: 'https://img/before.png', afterImageUrl: 'https://img/after.png', @@ -97,7 +98,11 @@ describe('ProductHome Query Resolver (real DB)', () => { const result = await resolver.randomCakes(); expect(result.items).toEqual([ - { id: cake.id.toString(), thumbnailUrl: 'https://img/grid.png' }, + { + id: cake.id.toString(), + storeId: store.id.toString(), + thumbnailUrl: 'https://img/grid.png', + }, ]); }); }); diff --git a/src/features/product/services/product-home-mappers.helper.ts b/src/features/product/services/product-home-mappers.helper.ts index 27b181f..08032d0 100644 --- a/src/features/product/services/product-home-mappers.helper.ts +++ b/src/features/product/services/product-home-mappers.helper.ts @@ -28,6 +28,7 @@ export function toPopularCake( ): PopularCake { return { id: row.id.toString(), + storeId: row.store_id.toString(), rank, name: row.name, thumbnailUrl: row.images[0]?.image_url ?? null, diff --git a/src/features/product/services/product-home.service.spec.ts b/src/features/product/services/product-home.service.spec.ts index 6691b4c..c82edd5 100644 --- a/src/features/product/services/product-home.service.spec.ts +++ b/src/features/product/services/product-home.service.spec.ts @@ -219,6 +219,7 @@ describe('ProductHomeService (real DB)', () => { expect(item).toMatchObject({ name: '레터링 케이크', + storeId: store.id.toString(), storeName: '청담 케이크샵', regionLabel: '서울 청담동', regularPrice: 40000, @@ -619,6 +620,7 @@ describe('ProductHomeService (real DB)', () => { expect(new Set(result.items.map((i) => i.id)).size).toBe(9); for (const item of result.items) { expect(item.thumbnailUrl).toBe(`https://img/random-${item.id}.png`); + expect(item.storeId).toBe(store.id.toString()); } }); diff --git a/src/features/product/services/product-home.service.ts b/src/features/product/services/product-home.service.ts index e88c8c7..3f6c1d4 100644 --- a/src/features/product/services/product-home.service.ts +++ b/src/features/product/services/product-home.service.ts @@ -133,6 +133,7 @@ export class ProductHomeService { items.push({ reviewId: row.id.toString(), + storeId: row.store_id.toString(), rank: items.length + 1, // 탈퇴(soft-delete) 작성자는 닉네임을 노출하지 않는다(익명화 정책) authorNickname: @@ -175,7 +176,9 @@ export class ProductHomeService { const thumbnailUrl = row?.images[0]?.image_url; // 후보 조회가 이미지 보유를 보장하지만, 조회 사이의 삭제 경합에 대비해 방어 if (!thumbnailUrl) return []; - return [{ id: id.toString(), thumbnailUrl }]; + return [ + { id: id.toString(), storeId: row.store_id.toString(), thumbnailUrl }, + ]; }); return { items }; diff --git a/src/features/product/types/product-home-output.type.ts b/src/features/product/types/product-home-output.type.ts index 60b395b..454d0b3 100644 --- a/src/features/product/types/product-home-output.type.ts +++ b/src/features/product/types/product-home-output.type.ts @@ -16,6 +16,7 @@ export interface HomeBanner { export interface PopularCake { id: string; + storeId: string; rank: number; name: string; thumbnailUrl: string | null; @@ -34,6 +35,7 @@ export interface PopularCakesResult { export interface RandomCake { id: string; + storeId: string; thumbnailUrl: string; } @@ -43,6 +45,7 @@ export interface RandomCakesResult { export interface CustomCakeShowcaseItem { reviewId: string; + storeId: string; rank: number; authorNickname: string | null; reviewText: string | null;