From 334e8e64ccaa26d73a32313c7270843ccd72b765 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 18 Jun 2026 20:58:50 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(pickup=20=EB=8B=B9=EC=9D=BC?= =?UTF-8?q?=20=EB=A7=88=EA=B0=90=20CLOSED,=20region=20=EC=8B=9C=EB=93=9C?= =?UTF-8?q?=20upsert=20=EC=A0=95=EA=B7=9C=ED=99=94,=20keyword=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/seed/regions.ts | 3 +++ .../pickup/constants/pickup-error-messages.ts | 1 + .../pickup/services/pickup-slot.service.spec.ts | 10 ++++++++++ src/features/pickup/services/pickup-slot.service.ts | 13 +++++++++++++ .../region/dto/inputs/search-regions.input.spec.ts | 6 ++++++ 5 files changed, 33 insertions(+) diff --git a/prisma/seed/regions.ts b/prisma/seed/regions.ts index 1e63d04..42139d4 100644 --- a/prisma/seed/regions.ts +++ b/prisma/seed/regions.ts @@ -39,6 +39,8 @@ export async function seedRegions(prisma: PrismaClient): Promise { sort_order: group.sortOrder, }, update: { + level: 1, + parent_id: null, name: group.name, sort_order: group.sortOrder, is_active: true, @@ -65,6 +67,7 @@ export async function seedRegions(prisma: PrismaClient): Promise { parent_id: parentId, }, update: { + level: 2, name: child.name, sort_order: child.sortOrder, parent_id: parentId, diff --git a/src/features/pickup/constants/pickup-error-messages.ts b/src/features/pickup/constants/pickup-error-messages.ts index 0f22905..71c2efa 100644 --- a/src/features/pickup/constants/pickup-error-messages.ts +++ b/src/features/pickup/constants/pickup-error-messages.ts @@ -7,4 +7,5 @@ export const PICKUP_ERRORS = { export const PICKUP_DAY_REASON = { PAST: 'PAST', OUT_OF_RANGE: 'OUT_OF_RANGE', + CLOSED: 'CLOSED', // 당일이지만 현재시각+리드타임으로 가용 슬롯이 없음 } as const; diff --git a/src/features/pickup/services/pickup-slot.service.spec.ts b/src/features/pickup/services/pickup-slot.service.spec.ts index 7a44cb7..094ce07 100644 --- a/src/features/pickup/services/pickup-slot.service.spec.ts +++ b/src/features/pickup/services/pickup-slot.service.spec.ts @@ -42,6 +42,16 @@ describe('PickupSlotService', () => { reason: 'OUT_OF_RANGE', }); }); + + it('오늘이지만 가용 슬롯이 없으면 CLOSED로 막는다', () => { + // KST 19:00 + 리드 60분 = cutoff 20:00 > 마지막 슬롯 19:30 → 당일 슬롯 없음 + const now = new Date('2026-06-18T10:00:00.000Z'); // KST 06-18 19:00 + const calendar = service.pickupCalendar('2026-06', now); + expect(calendar.days.find((d) => d.date === '2026-06-18')).toMatchObject({ + selectable: false, + reason: 'CLOSED', + }); + }); }); describe('pickupTimeSlots', () => { diff --git a/src/features/pickup/services/pickup-slot.service.ts b/src/features/pickup/services/pickup-slot.service.ts index dfa148f..0fa7157 100644 --- a/src/features/pickup/services/pickup-slot.service.ts +++ b/src/features/pickup/services/pickup-slot.service.ts @@ -41,6 +41,12 @@ export class PickupSlotService { const daysInMonth = new Date(Date.UTC(ym.year, ym.month, 0)).getUTCDate(); + // 오늘은 현재시각+리드타임 이후 가용 슬롯이 하나라도 있어야 선택 가능 + // (없으면 pickupTimeSlots가 전부 마감 → 캘린더도 선택 불가로 일치시킨다) + const cutoffMinutes = kstMinutesOfDay(now) + PICKUP_MIN_LEAD_MINUTES; + const lastSlotMinutes = PICKUP_CLOSE_MINUTES - PICKUP_SLOT_INTERVAL_MINUTES; + const todayHasSlot = cutoffMinutes <= lastSlotMinutes; + const days = Array.from({ length: daysInMonth }, (_, index) => { const day = index + 1; const date = kstMidnightUtc(ym.year, ym.month, day); @@ -60,6 +66,13 @@ export class PickupSlotService { reason: PICKUP_DAY_REASON.OUT_OF_RANGE, }; } + if (diff === 0 && !todayHasSlot) { + return { + date: formatKstDate(date), + selectable: false, + reason: PICKUP_DAY_REASON.CLOSED, + }; + } return { date: formatKstDate(date), selectable: true, reason: null }; }); diff --git a/src/features/region/dto/inputs/search-regions.input.spec.ts b/src/features/region/dto/inputs/search-regions.input.spec.ts index 0e0625f..b40aeae 100644 --- a/src/features/region/dto/inputs/search-regions.input.spec.ts +++ b/src/features/region/dto/inputs/search-regions.input.spec.ts @@ -45,4 +45,10 @@ describe('SearchRegionsInput', () => { const errors = await validate(build({ keyword: 'a', limit: 1.5 })); expect(errors[0].property).toBe('limit'); }); + + it('keyword 최대 길이 경계(80 통과, 81 거절)', async () => { + expect(await validate(build({ keyword: 'a'.repeat(80) }))).toHaveLength(0); + const errors = await validate(build({ keyword: 'a'.repeat(81) })); + expect(errors[0].property).toBe('keyword'); + }); });