diff --git a/prisma/seed/idempotent.ts b/prisma/seed/idempotent.ts index c7b729b1..464d8a9b 100644 --- a/prisma/seed/idempotent.ts +++ b/prisma/seed/idempotent.ts @@ -67,20 +67,38 @@ export async function resetSeedScope(prisma: PrismaClient): Promise { }); } - // 1) 유저 종속 (주문, 리뷰, 찜, 최근본, 알림, 드래프트, 검색기록, 카트, 인증세션) - if (userIds.length > 0) { + // 1) 주문 — 사용자 조건과 분리한다. 재시드가 2) 상품 삭제에서 실패하면 시드 사용자는 이미 지워져 + // 시드 매장과 막는 주문만 남으므로, 그 상태에서 다시 돌려도 복구돼야 한다 + if (userIds.length > 0 || storeIds.length > 0) { // 주문 종속들 → 주문 본체 (FK depth가 깊으므로 안에서 다시 처리) - const userOrders = await prisma.order.findMany({ - where: { account_id: { in: userIds } }, + // 시드 밖 계정이 시드 매장 상품으로 넣은 주문도 지운다 — 품목·리뷰가 시드 상품·매장 FK(RESTRICT)에 걸린다. + // 주문은 상품 하나로 만들어져 매장도 하나라, 시드 밖 매장 품목이 함께 지워지지 않는다 + const seedOrders = await prisma.order.findMany({ + where: { + OR: [ + { account_id: { in: userIds } }, + { items: { some: { store_id: { in: storeIds } } } }, + ], + }, select: { id: true }, }); - const orderIds = userOrders.map((o) => o.id); + const orderIds = seedOrders.map((o) => o.id); if (orderIds.length > 0) { const orderItems = await prisma.orderItem.findMany({ where: { order_id: { in: orderIds } }, select: { id: true }, }); const orderItemIds = orderItems.map((i) => i.id); + // 시드 밖 계정이 받은 이 주문·리뷰 알림 — 남기면 SET NULL로 대상 없는 알림이 된다 + await prisma.notification.deleteMany({ + where: { + OR: [ + { order_id: { in: orderIds } }, + { order_item_id: { in: orderItemIds } }, + { review: { order_item_id: { in: orderItemIds } } }, + ], + }, + }); if (orderItemIds.length > 0) { // OrderItemCustomFreeEdit -> attachments -> deletes const freeEdits = await prisma.orderItemCustomFreeEdit.findMany({ @@ -125,7 +143,10 @@ export async function resetSeedScope(prisma: PrismaClient): Promise { }); await prisma.order.deleteMany({ where: { id: { in: orderIds } } }); } + } + // 1-2) 유저 종속 (리뷰, 찜, 최근본, 알림, 드래프트, 검색기록, 카트, 인증세션) + if (userIds.length > 0) { // 리뷰: 주문 종속에서 이미 처리되었지만, 다른 리뷰가 있을 수 있음 → 사용자 기준으로도 한 번 더 await prisma.reviewLike.deleteMany({ where: { account_id: { in: userIds } }, diff --git a/src/test/seed-reset-scope.spec.ts b/src/test/seed-reset-scope.spec.ts index e025f9a6..fa48cf97 100644 --- a/src/test/seed-reset-scope.spec.ts +++ b/src/test/seed-reset-scope.spec.ts @@ -1,5 +1,8 @@ import { seedCategories } from '../../prisma/seed/categories'; -import { resetSeedScope } from '../../prisma/seed/idempotent'; +import { + SEED_STORE_NAME_PREFIX, + resetSeedScope, +} from '../../prisma/seed/idempotent'; import { seedOrders } from '../../prisma/seed/orders'; import { seedRegions } from '../../prisma/seed/regions'; import { seedReviews } from '../../prisma/seed/reviews'; @@ -11,6 +14,8 @@ import { disconnectTestPrismaClient } from '@/test/db/prisma-test-client'; import { closeTruncateConnection, truncateAll } from '@/test/db/truncate'; import { createAccount, + createNotification, + createOrder, createOrderItem, createReview, createReviewReport, @@ -51,6 +56,86 @@ describe('resetSeedScope (real DB)', () => { }); } + /** 시드 밖 계정의 주문 1건(품목 1개)을 만든다. productId를 안 주면 시드 밖 매장 상품으로 */ + async function outsiderOrder(productId?: bigint) { + const outsider = await createAccount(prisma, { account_type: 'USER' }); + const order = await createOrder(prisma, { account_id: outsider.id }); + const item = await createOrderItem(prisma, { + order_id: order.id, + ...(productId ? { product_id: productId } : {}), + }); + return { outsider, order, item }; + } + + it('시드 사용자 없이 시드 매장과 시드 밖 주문만 남은 상태(이전 재시드 실패)도 정리한다', async () => { + // 이전 재시드가 상품 삭제에서 실패하면 시드 사용자는 이미 지워져 이 상태가 남는다 + await seedRegions(prisma); + const categories = await seedCategories(prisma); + const { products } = await seedStores(prisma, { categories }); + const { order } = await outsiderOrder(products[0].id); + + await resetSeedScope(prisma); + + expect( + await prisma.order.findUnique({ where: { id: order.id } }), + ).toBeNull(); + expect( + await prisma.store.count({ + where: { store_name: { startsWith: SEED_STORE_NAME_PREFIX } }, + }), + ).toBe(0); + }); + + it('시드 밖 계정이 시드 상품을 주문하고 리뷰를 남겨도 정리하고 다시 시드한다', async () => { + await seedThroughReviews(); + const seedProduct = await prisma.product.findFirstOrThrow({ + where: { store: { store_name: { startsWith: SEED_STORE_NAME_PREFIX } } }, + }); + const { outsider, order, item } = await outsiderOrder(seedProduct.id); + const review = await createReview(prisma, { order_item_id: item.id }); + const notification = await createNotification(prisma, { + account_id: outsider.id, + order_id: order.id, + order_item_id: item.id, + review_id: review.id, + }); + + await resetSeedScope(prisma); + await seedThroughReviews(); + + expect( + await prisma.order.findUnique({ where: { id: order.id } }), + ).toBeNull(); + expect( + await prisma.review.findUnique({ where: { id: review.id } }), + ).toBeNull(); + expect( + await prisma.notification.findUnique({ where: { id: notification.id } }), + ).toBeNull(); + expect( + await prisma.account.findUnique({ where: { id: outsider.id } }), + ).not.toBeNull(); + }); + + it('시드 밖 계정이 시드 밖 매장에 넣은 주문은 남긴다', async () => { + await seedThroughReviews(); + const { order, item } = await outsiderOrder(); + const notification = await createNotification(prisma, { + account_id: order.account_id, + order_id: order.id, + order_item_id: item.id, + }); + + await resetSeedScope(prisma); + + expect( + await prisma.order.findUnique({ where: { id: order.id } }), + ).not.toBeNull(); + expect( + await prisma.notification.findUnique({ where: { id: notification.id } }), + ).not.toBeNull(); + }); + it('시드 리뷰에 다른 계정이 단 댓글이 있어도 정리하고 다시 시드한다', async () => { const users = await seedThroughReviews(); const seedReview = await prisma.review.findFirstOrThrow({