From 471bd85a5dc110f2a5f8335680e62e2cc8df26a2 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Sun, 4 Oct 2026 23:52:36 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=8C=90=EB=A7=A4=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EC=83=81=ED=92=88=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=97=B4=20=EC=88=98=20=EC=9E=88=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sellerProducts는 숨김(is_active=false) 상품도 보여주는데 sellerProduct만 is_active: true인 findProductById를 써서 목록에서 연 숨김 상품이 PRODUCT_NOT_FOUND - sellerProduct를 findProductByIdIncludingInactive로 전환(lifecycle·option·taxonomy 판매자 경로와 같은 메서드, store_id 소유 범위 그대로, soft-delete는 루트 findFirst가 제외) - 두 메서드 include가 동일해 toProductOutput 매핑은 변화 없음 - 호출처가 없어진 findProductById 삭제, repository spec의 store_id 불일치·카테고리/태그 삭제 가드 케이스는 남은 메서드로 이전 - 회귀: 숨김 본인 상품 상세는 isActive false로 반환(수정 전 PRODUCT_NOT_FOUND로 실패 확인), soft-delete 상품은 PRODUCT_NOT_FOUND 유지 --- .../repositories/product.repository.spec.ts | 27 +++------- .../repositories/product.repository.ts | 49 ------------------- .../product-seller-query.service.spec.ts | 21 ++++++++ .../services/product-seller-query.service.ts | 3 +- 4 files changed, 29 insertions(+), 71 deletions(-) diff --git a/src/features/product/repositories/product.repository.spec.ts b/src/features/product/repositories/product.repository.spec.ts index 89d29aa3..32d00e8f 100644 --- a/src/features/product/repositories/product.repository.spec.ts +++ b/src/features/product/repositories/product.repository.spec.ts @@ -235,18 +235,18 @@ describe('ProductRepository (real DB)', () => { }); }); - describe('findProductById (active만)', () => { - it('is_active: false면 반환 안함', async () => { + describe('findProductByIdIncludingInactive', () => { + it('inactive 상품도 반환', async () => { const store = await createStore(prisma); const inactive = await createProduct(prisma, { store_id: store.id, is_active: false, }); - const result = await repo.findProductById({ + const result = await repo.findProductByIdIncludingInactive({ productId: inactive.id, storeId: store.id, }); - expect(result).toBeNull(); + expect(result?.id).toBe(inactive.id); }); it('store_id 불일치면 null', async () => { @@ -254,7 +254,7 @@ describe('ProductRepository (real DB)', () => { const storeB = await createStore(prisma); const product = await createProduct(prisma, { store_id: storeA.id }); - const result = await repo.findProductById({ + const result = await repo.findProductByIdIncludingInactive({ productId: product.id, storeId: storeB.id, }); @@ -303,7 +303,7 @@ describe('ProductRepository (real DB)', () => { data: { deleted_at: new Date() }, }); - const result = await repo.findProductById({ + const result = await repo.findProductByIdIncludingInactive({ productId: product.id, storeId: store.id, }); @@ -314,21 +314,6 @@ describe('ProductRepository (real DB)', () => { }); }); - describe('findProductByIdIncludingInactive', () => { - it('inactive 상품도 반환', async () => { - const store = await createStore(prisma); - const inactive = await createProduct(prisma, { - store_id: store.id, - is_active: false, - }); - const result = await repo.findProductByIdIncludingInactive({ - productId: inactive.id, - storeId: store.id, - }); - expect(result?.id).toBe(inactive.id); - }); - }); - describe('createProduct / updateProduct / softDeleteProduct', () => { it('createProduct는 store_id를 결합하여 생성', async () => { const store = await createStore(prisma); diff --git a/src/features/product/repositories/product.repository.ts b/src/features/product/repositories/product.repository.ts index 89ad152f..24da236a 100644 --- a/src/features/product/repositories/product.repository.ts +++ b/src/features/product/repositories/product.repository.ts @@ -264,55 +264,6 @@ export class ProductRepository { return Boolean(found); } - async findProductById(args: { productId: bigint; storeId: bigint }) { - return this.prisma.product.findFirst({ - where: { - id: args.productId, - store_id: args.storeId, - is_active: true, - }, - // soft-delete extension은 root만 patch하므로 nested relation에 가드를 명시한다 - include: { - images: { - where: activeWhere, - orderBy: { sort_order: 'asc' }, - }, - product_categories: { - // 링크·대상 카테고리의 soft-delete 가드. is_active는 셀러 화면에서 - // 기존 지정을 계속 보여줘야 하므로 걸지 않는다. - where: { ...activeWhere, category: activeWhere }, - include: { - category: true, - }, - }, - product_tags: { - where: { ...activeWhere, tag: activeWhere }, - include: { - tag: true, - }, - }, - option_groups: { - where: activeWhere, - orderBy: { sort_order: 'asc' }, - include: { - option_items: { - where: activeWhere, - orderBy: { sort_order: 'asc' }, - }, - }, - }, - custom_template: { - include: { - text_tokens: { - where: activeWhere, - orderBy: { sort_order: 'asc' }, - }, - }, - }, - }, - }); - } - async findProductByIdIncludingInactive(args: { productId: bigint; storeId: bigint; diff --git a/src/features/product/services/product-seller-query.service.spec.ts b/src/features/product/services/product-seller-query.service.spec.ts index a7ebc615..45c0a4fb 100644 --- a/src/features/product/services/product-seller-query.service.spec.ts +++ b/src/features/product/services/product-seller-query.service.spec.ts @@ -175,6 +175,27 @@ describe('SellerProductQueryService (real DB)', () => { expect(result.images).toHaveLength(1); }); + it('soft-delete된 상품이면 404', async () => { + const { account, store } = await setupSellerWithStore(prisma); + const deleted = await createSellerProduct(store.id, { + deleted_at: new Date(), + }); + + await expect( + service.sellerProduct(account.id, deleted.id), + ).rejects.toThrowDomain('PRODUCT_NOT_FOUND'); + }); + + it('숨김(is_active=false) 본인 상품도 상세를 반환한다', async () => { + const { account, store } = await setupSellerWithStore(prisma); + const hidden = await createSellerProduct(store.id, { is_active: false }); + + const result = await service.sellerProduct(account.id, hidden.id); + + expect(result.id).toBe(hidden.id.toString()); + expect(result.isActive).toBe(false); + }); + it('soft-delete된 이미지·옵션 그룹·옵션 아이템은 상세에서 제외한다', async () => { const { account, store } = await setupSellerWithStore(prisma); const product = await createSellerProduct(store.id); diff --git a/src/features/product/services/product-seller-query.service.ts b/src/features/product/services/product-seller-query.service.ts index 2d76889e..34216c31 100644 --- a/src/features/product/services/product-seller-query.service.ts +++ b/src/features/product/services/product-seller-query.service.ts @@ -70,7 +70,8 @@ export class SellerProductQueryService extends SellerBaseService { productId: bigint, ): Promise { const ctx = await this.requireSellerContext(accountId); - const row = await this.productRepository.findProductById({ + // 목록이 숨김 상품도 보여주므로 상세도 is_active와 무관하게 연다 + const row = await this.productRepository.findProductByIdIncludingInactive({ productId, storeId: ctx.storeId, });