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'); + }); });