Skip to content

feat(store): 매장별 픽업 달력·시간 슬롯 조회(storePickupCalendar/storePickupTimeSlots) - #193

Merged
chanwoo7 merged 3 commits into
developfrom
feat/order-before-customization-figma-store-pickup-schedule
Aug 22, 2026
Merged

feat(store): 매장별 픽업 달력·시간 슬롯 조회(storePickupCalendar/storePickupTimeSlots)#193
chanwoo7 merged 3 commits into
developfrom
feat/order-before-customization-figma-store-pickup-schedule

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Aug 22, 2026

Copy link
Copy Markdown
Member

배경

상품 상세의 주문 전(커스텀 진입 전) 픽업 일정 선택 화면은 달력의 날짜별 "마감" 표기와 시간 슬롯 비활성 표시를 요구합니다.
기존 pickupCalendar/pickupTimeSlots는 홈 화면용 전역 고정 정책이라 매장별 휴무·영업시간·일일 capacity를 표현할 수 없었습니다.
그래서 매장별 조회 쿼리 storePickupCalendar(storeId, yearMonth) / storePickupTimeSlots(storeId, date) 2건을 신설했습니다.
전역 쿼리는 변경하지 않았습니다.

주요 결정 사항

  • 판정 규칙을 새로 만들지 않고 todayPickupStores 선례를 월 단위로 확장: 특별휴무·요일 휴무·영업시간 미설정은 CLOSED로 처리합니다.
    capacity는 레코드가 없으면 무제한이고, 취소·soft-delete 제외 주문 수량 합이 capacity 이상이면 CAPACITY_FULL입니다.
    슬롯은 매장 pickup_slot_interval_minutes 간격과 [open, close) 규칙으로 만들고, 당일만 min_lead_time_minutes 컷오프를 적용합니다(초 잔여 시 다음 분 올림).
  • 달력 범위는 매장 max_days_ahead 컬럼 사용: 과거는 PAST, 초과는 OUT_OF_RANGE이며, 당일에 잔여 가용 슬롯이 없으면 날짜 자체를 CLOSED 처리합니다(전역 달력 선례와 동일).
  • 위치는 store feature: 판정에 필요한 데이터(영업시간·휴무·capacity·주문 점유)와 슬롯 헬퍼가 모두 store feature에 있어 그대로 재사용했습니다.
    오전/오후 경계(12:00)는 매장 무관 UI 규칙으로 판단해 pickup feature 전역 상수를 배럴 export로 가져왔습니다(명세 외 정책 결정).
  • 연도를 1001~9998로 제한(Codex 리뷰 반영): KST -9h 경계와 익월/익일 상한 계산이 MySQL DATE/DATETIME 표현 범위를 벗어나지 않도록 방어합니다.
    0~99년을 1900년대로 매핑하는 Date.UTC 오동작도 함께 차단합니다.
  • 조회는 월 범위 벌크 4회로 고정해 날짜 루프에서 추가 쿼리를 내지 않습니다.
    비로그인 접근 가능하며, 비활성/삭제 매장은 NOT_FOUND입니다.
    스키마 변경(마이그레이션) 없음.

검증

  • real DB 회귀 테스트 20건(달력 판정 전 분기, KST 자정 경계 집계, 슬롯 간격·리드타임·오전오후 분리, 연도 범위·형식 오류·NOT_FOUND, resolver 통합 2건) 추가.
  • yarn validate 전체 통과.

…ots)

상품 상세 주문 전 단계의 픽업 일정 화면은 달력의 날짜별 '마감' 표기와
시간 슬롯 비활성(회색)을 요구한다. 기존 pickupCalendar/pickupTimeSlots는
홈 전역 고정 정책이라 매장별 휴무·영업시간·capacity를 표현할 수 없어
매장별 조회 쿼리를 신설한다.

- SDL store-pickup-schedule.graphql: storePickupCalendar(storeId, yearMonth) /
  storePickupTimeSlots(storeId, date), StorePickupDay.reason은
  PAST | OUT_OF_RANGE | CLOSED | CAPACITY_FULL
- StorePickupScheduleService: todayPickupStores와 동일한 매장 정책 판정을
  월 단위로 확장 — 특별휴무·요일 영업시간·일일 capacity(레코드 없으면 무제한,
  취소·soft-delete 주문 제외 수량 합 기준)·당일 리드타임 잔여 슬롯
- StoreRepository: 매장 픽업 정책·요일 영업시간 전체·기간 휴무/capacity·
  KST 날짜별 픽업 수량 합(raw SQL, DST 없는 고정 +9h 변환) 조회 5종 추가
- 달력 범위는 매장 max_days_ahead 컬럼 사용. 명세 외 정책 결정:
  오전/오후 경계(12:00)는 매장 무관 UI 규칙으로 보고 pickup 전역 상수 재사용
- store 테스트 팩토리에 max_days_ahead override 추가

회귀 테스트 18건: 달력 판정 전 분기(과거/범위 초과/특별휴무/요일 휴무·미설정/
capacity 소진·무제한/취소·soft-delete 제외/당일 잔여 슬롯), KST 자정 경계 집계,
슬롯 간격·리드타임 컷오프·오전오후 분리, 형식 오류·NOT_FOUND
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9f466a2-6d06-432e-98c1-d37eaf226f4f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

요약 항목 없음
전체 리포트
(knip 출력 없음 — 이슈 0이거나 실행 실패)

청소 후보(오탐 가능) · 기준 docs/guide/architecture-conventions.md

@github-actions

Copy link
Copy Markdown

🩺 NestJS Doctor — 89/100 (Good)

진단 279건 (error 0).

Category error warning info
architecture 0 0 13
correctness 0 126 0
performance 0 24 17
schema 0 0 86
security 0 13 0
architecture / security 상위 항목
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'IAuditLogRepository'.
  • warning security/security/no-exposed-env-vars: Direct 'process.env.NODE_ENV' access in 'AuthController'. Use ConfigService instead.
  • warning security/security/require-guards-on-endpoints: Endpoint 'start' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'callback' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'refresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'logout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'devIssueToken' has no @UseGuards() at class or method level.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/conversation/repositories/conversation.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'ConversationRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/order/repositories/order.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'OrderRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/product/repositories/product.repository'.

오탐 포함 가능 · 기준 docs/guide/architecture-conventions.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f09ad0bf6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

storeId: bigint,
yearMonth: string,
): Promise<StorePickupCalendar> {
const ym = parseKstYearMonth(yearMonth);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject years outside the safe database date range

The year-month parser accepts every four-digit year, but this endpoint immediately converts it into database bounds. For example, 0000-01 is silently converted by Date.UTC to January 1900 and returns dates from the wrong century, while years below MySQL's DATE minimum (such as 0100-01) and the upper bound for 9999-12 can cause Prisma/MySQL errors instead of the documented BadRequestException. Validate that the year can be represented and round-tripped through both JavaScript and the database before loading the schedule context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 연도를 10009998로 제한하고 벗어나면 BadRequest (storePickupCalendar 연월·storePickupTimeSlots 날짜 모두). 근거 — MySQL DATE 하한 1000-01-01, 9999-12(-31)는 익월/익일 상한 계산이 DATE 상한 초과, 099년은 Date.UTC의 1900년대 매핑 오동작. 회귀 테스트 2건 추가.

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.97959% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...es/store/services/store-pickup-schedule.service.ts 98.71% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.6% 4767/4884
🟢 Branches 93.27% 1525/1635
🟢 Functions 95.99% 933/972
🟢 Lines 98.21% 4334/4413

Test suite run success

1572 tests passing in 184 suites.

Report generated by 🧪jest coverage report action from 2fee27b

Codex 리뷰 반영. 연월/날짜 파서가 임의 4자리 연도를 통과시켜
0~99년은 Date.UTC의 1900년대 매핑으로 엉뚱한 세기를 반환하고,
MySQL DATE 범위(1000-01-01~9999-12-31) 밖(및 9999-12의 익월 상한
오버플로)은 BadRequest 대신 Prisma/MySQL 에러가 될 수 있었다.

- 연도 1000~9998 밖이면 INVALID_YEAR_MONTH/INVALID_DATE로 거절
- 날짜 검증을 매장 조회(DB 접근)보다 앞으로 이동
- 회귀 테스트 2건 추가 (0000-01/0999-01/9999-12, 9999-12-31)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dcf7430b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

store.id,
new Date(Date.UTC(ym.year, ym.month - 1, 1)),
new Date(Date.UTC(ym.year, ym.month, 1)),
kstMidnightUtc(ym.year, ym.month, 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject the KST boundary that underflows DATETIME

When yearMonth is 1000-01, this KST month boundary becomes 0999-12-31T15:00:00Z and is passed to sumPickupQuantitiesByKstDate for comparison with order.pickup_at, whose schema type is MySQL DATETIME(3) and whose minimum is 1000-01-01 00:00:00; the query can therefore fail instead of returning a calendar. Fresh evidence after the earlier fix is that the newly added MIN_SCHEDULE_YEAR = 1000 check still permits this UTC-underflowing boundary, so validate or clamp the constructed UTC range as well; the same issue affects storePickupTimeSlots for 1000-01-01.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영: 하한을 1001로 상향 (1000-01의 KST 월 시작 경계 -9h가 0999-12-31T15:00Z로 DATETIME 하한 미달). storePickupTimeSlots 1000-01-01 동일 차단. 회귀 테스트 2건 추가.

Codex 리뷰 반영. 1000-01의 KST 월 시작 경계(-9h)가 0999-12-31T15:00Z로
MySQL DATETIME 하한(1000-01-01)을 밑돌아 pickup_at 범위 비교가 실패할 수
있었다. storePickupTimeSlots의 1000-01-01도 동일.

- MIN_SCHEDULE_YEAR 1000 → 1001, 근거 주석 갱신
- 회귀 테스트 2건 추가 (1000-01, 1000-01-01)
@chanwoo7
chanwoo7 merged commit 7469ca4 into develop Aug 22, 2026
10 checks passed
@chanwoo7
chanwoo7 deleted the feat/order-before-customization-figma-store-pickup-schedule branch August 22, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant