-
Notifications
You must be signed in to change notification settings - Fork 0
chore: 릴리즈 — 주문 생성 API (createOrder) #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d46cfe3
feat(order): 주문 생성 mutation(createOrder) — 정식 API의 확정 부분집합
chanwoo7 bcb59ec
fix(order): 주문 금액 범위 검증 + capacity 원자 예약 (Codex P1 반영)
chanwoo7 bc8ec27
fix(order): 제작 소요시간 강제 + 비 KRW 통화 거절 (Codex P1 반영)
chanwoo7 bb21992
fix(order): 활성 USER 계정 강제 + 주문자 전화번호 형식 검증 (Codex 반영)
chanwoo7 dee7b8f
fix(order): 공백만 있는 주문자 이름은 미입력으로 취급 (Codex P2 반영)
chanwoo7 f5966d0
Merge pull request #208 from CaQuick/feat/create-order-mutation
chanwoo7 1f4be4c
fix: 릴리즈 리뷰 반영 (설명/이미지 필수 옵션 선택 시 주문 거절)
chanwoo7 2acfcc6
Merge pull request #210 from CaQuick/fix/release-review-checkout-opti…
chanwoo7 8211999
fix: 릴리즈 리뷰 반영 (하루 넘는 리드타임을 미래 날짜에도 적용)
chanwoo7 9181128
Merge pull request #211 from CaQuick/fix/release-review-multiday-lead…
chanwoo7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /** 주문 생성(체크아웃) 에러 메시지. */ | ||
| export const ORDER_CHECKOUT_ERRORS = { | ||
| BUYER_ACCOUNT_NOT_ACTIVE: '유효한 사용자 계정이 아닙니다.', | ||
| BUYER_NOT_USER: 'USER 계정만 주문할 수 있습니다.', | ||
| PRODUCT_NOT_FOUND: '상품을 찾을 수 없습니다.', | ||
| DUPLICATE_OPTION_ITEM: '중복된 옵션 선택입니다.', | ||
| INVALID_OPTION_ITEM: '해당 상품의 옵션이 아닙니다.', | ||
| OPTION_GROUP_RULE_VIOLATION: '옵션 그룹의 선택 규칙을 충족하지 않습니다.', | ||
| OPTION_CUSTOMIZATION_REQUIRED: | ||
| '커스텀 정보가 필요한 옵션은 아직 주문할 수 없습니다.', | ||
| PICKUP_NOT_AVAILABLE: '선택한 픽업 일시는 예약할 수 없습니다.', | ||
| ORDER_AMOUNT_OUT_OF_RANGE: '주문 금액이 처리 가능한 범위를 벗어났습니다.', | ||
| UNSUPPORTED_CURRENCY: 'KRW 상품만 주문할 수 있습니다.', | ||
| BUYER_PHONE_REQUIRED: | ||
| '주문자 연락처가 필요합니다. 프로필에 전화번호를 등록하거나 입력해 주세요.', | ||
| ORDER_NUMBER_GENERATION_FAILED: | ||
| '주문번호 생성에 실패했습니다. 잠시 후 다시 시도해 주세요.', | ||
| } as const; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /** | ||
| * 주문자 전화번호 형식. 프로필 전화번호 정책(user feature `PHONE_REGEX`, | ||
| * 010-XXXX-XXXX 고정 13자)과 동일해야 한다 — user는 배럴 없는 feature라 | ||
| * cross-feature import 대신 정책을 복제하고 출처를 명시한다. | ||
| */ | ||
| export const ORDER_BUYER_PHONE_REGEX = /^010-\d{4}-\d{4}$/; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import 'reflect-metadata'; | ||
|
|
||
| import { plainToInstance } from 'class-transformer'; | ||
| import { validate } from 'class-validator'; | ||
|
|
||
| import { CreateOrderInput } from '@/features/order/dto/inputs/create-order.input'; | ||
|
|
||
| function build(plain: object): CreateOrderInput { | ||
| return plainToInstance(CreateOrderInput, plain); | ||
| } | ||
|
|
||
| const VALID = { | ||
| productId: '1', | ||
| optionItemIds: ['10', '11'], | ||
| pickupAt: new Date('2026-09-18T05:00:00.000Z'), | ||
| }; | ||
|
|
||
| describe('CreateOrderInput', () => { | ||
| it('필수 필드만으로 통과한다 (quantity/buyer는 optional)', async () => { | ||
| expect(await validate(build(VALID))).toHaveLength(0); | ||
| }); | ||
|
|
||
| it('buyer 필드·quantity 포함 통과', async () => { | ||
| const errors = await validate( | ||
| build({ | ||
| ...VALID, | ||
| quantity: 3, | ||
| buyerName: '차차', | ||
| buyerPhone: '010-0000-1111', | ||
| }), | ||
| ); | ||
| expect(errors).toHaveLength(0); | ||
| }); | ||
|
|
||
| it('optionItemIds가 배열이 아니면 거절한다', async () => { | ||
| const errors = await validate(build({ ...VALID, optionItemIds: '10' })); | ||
| expect(errors[0].property).toBe('optionItemIds'); | ||
| }); | ||
|
|
||
| it('pickupAt이 Date가 아니면 거절한다', async () => { | ||
| const errors = await validate(build({ ...VALID, pickupAt: 'not-a-date' })); | ||
| expect(errors[0].property).toBe('pickupAt'); | ||
| }); | ||
|
|
||
| it('quantity 0·100은 범위 위반으로 거절한다', async () => { | ||
| expect((await validate(build({ ...VALID, quantity: 0 })))[0].property).toBe( | ||
| 'quantity', | ||
| ); | ||
| expect( | ||
| (await validate(build({ ...VALID, quantity: 100 })))[0].property, | ||
| ).toBe('quantity'); | ||
| }); | ||
|
|
||
| it('buyerName 빈 문자열은 거절한다', async () => { | ||
| const errors = await validate(build({ ...VALID, buyerName: '' })); | ||
| expect(errors[0].property).toBe('buyerName'); | ||
| }); | ||
|
|
||
| it('buyerPhone은 010-XXXX-XXXX 형식만 허용한다', async () => { | ||
| expect( | ||
| (await validate(build({ ...VALID, buyerPhone: 'abc' })))[0].property, | ||
| ).toBe('buyerPhone'); | ||
| expect( | ||
| (await validate(build({ ...VALID, buyerPhone: '01000001111' })))[0] | ||
| .property, | ||
| ).toBe('buyerPhone'); | ||
| expect( | ||
| await validate(build({ ...VALID, buyerPhone: '010-0000-1111' })), | ||
| ).toHaveLength(0); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { | ||
| IsArray, | ||
| IsDate, | ||
| IsInt, | ||
| IsNotEmpty, | ||
| IsOptional, | ||
| IsString, | ||
| Matches, | ||
| Max, | ||
| MaxLength, | ||
| Min, | ||
| } from 'class-validator'; | ||
|
|
||
| import { ORDER_BUYER_PHONE_REGEX } from '@/features/order/constants/order.constants'; | ||
|
|
||
| export class CreateOrderInput { | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| productId!: string; | ||
|
|
||
| @IsArray() | ||
| @IsString({ each: true }) | ||
| optionItemIds!: string[]; | ||
|
|
||
| @IsDate() | ||
| pickupAt!: Date; | ||
|
|
||
| @IsOptional() | ||
| @IsInt() | ||
| @Min(1) | ||
| @Max(99) | ||
| quantity?: number; | ||
|
|
||
| @IsOptional() | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| @MaxLength(100) | ||
| buyerName?: string; | ||
|
|
||
| @IsOptional() | ||
| @IsString() | ||
| // 프로필 전화번호와 동일 정책(010-XXXX-XXXX 고정) — 임의 문자열이 | ||
| // 검증된 프로필 값을 덮어쓰지 못하게 형식을 강제한다 | ||
| @Matches(ORDER_BUYER_PHONE_REGEX) | ||
| buyerPhone?: string; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| extend type Mutation { | ||
| """주문 생성(구매자). 옵션·픽업 일시를 서버가 재검증하고 가격을 스냅샷한다. 로그인 필수.""" | ||
| createOrder(input: CreateOrderInput!): CreateOrderOutput! | ||
| } | ||
|
|
||
| """주문 생성 입력. 커스텀 필드는 커스텀 스펙 확정 후 optional로 확장 예정.""" | ||
| input CreateOrderInput { | ||
| productId: ID! | ||
| """선택한 옵션 아이템 ID 목록. 그룹 규칙(필수/min/max)을 서버가 검증한다.""" | ||
| optionItemIds: [ID!]! | ||
| """픽업 일시. 매장 정책(영업시간·휴무·capacity·리드타임·슬롯 정렬) 재검증.""" | ||
| pickupAt: DateTime! | ||
| quantity: Int = 1 | ||
| """미입력 시 프로필 닉네임 사용.""" | ||
| buyerName: String | ||
| """미입력 시 프로필 전화번호 사용. 둘 다 없으면 거절.""" | ||
| buyerPhone: String | ||
| } | ||
|
|
||
| """주문 생성 결과 요약. 상세는 myOrder로 재조회.""" | ||
| type CreateOrderOutput { | ||
| orderId: ID! | ||
| orderNumber: String! | ||
| status: OrderStatusType! | ||
| pickupAt: DateTime! | ||
| totalPrice: Int! | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a client retries after a timeout or submits twice, this mutation has no idempotency key or equivalent lookup, and every invocation generates a fresh random order number. The same checkout therefore creates multiple independent
SUBMITTEDorders and consumes capacity multiple times; accept a client request key, enforce uniqueness per account, and return the existing result on replay.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
미반영(후속 추적): 멱등 키는 주문 요청 키 컬럼+unique 등 스키마 변경 동반이라 릴리즈 리뷰 fix 범위 초과. 결제 없는 현 단계 위험도가 낮아, 커스텀 확장 시 입력 계약과 함께 설계하기로 하고 후속 이슈로 등록 예정.