docs(gift): 선물 요구사항 변경을 기록한다

This commit is contained in:
2026-09-30 17:37:09 +09:00
parent 631c985a24
commit 38941d9fe9
2 changed files with 159 additions and 22 deletions
@@ -6,7 +6,7 @@
|---|---|
| 문서 상태 | 구현 중 |
| 작성일 | 2026-09-29 |
| 최종 수정일 | 2026-09-29 |
| 최종 수정일 | 2026-09-30 |
| 대상 제품 | 크리에이터 선물하기 |
| 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 |
@@ -25,6 +25,7 @@
- 선물 가격은 사이즈별로 다르고 운영자가 변경해야 하므로 코드 상수만으로는 운영 요구를 충족할 수 없다.
- 운송장 미등록, 크리에이터 배송지 미입력, 검수 실패 같은 종료 상태와 환불/푸시 정책이 명확히 정의되어야 한다.
- 팬과 크리에이터가 서로 다른 시점에 약관과 개인정보 처리에 동의해야 하므로 신청 건 기준의 동의 스냅샷이 필요하다.
- 클라이언트가 날짜/시간 응답을 안정적으로 해석할 수 있도록 선물 API의 응답 시각은 timezone 없는 `LocalDateTime` JSON이 아니라 UTC ISO 문자열이어야 한다.
문제를 해결했다는 판단은 선물 신청부터 전달 완료 또는 종료 상태까지 모든 상태 전이가 API/스케줄링/운영 API로 추적되고, 캔 사용내역과 푸시가 상태별 정책대로 처리되는 것으로 한다.
@@ -97,10 +98,12 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
### 6.3 신청번호 정책
- 신청번호 `applicationNo`는 내부 기본키와 별도로 발급한다.
- 형식은 `G{yyyyMMdd}{dailySequence6}`로 한다.
- `yyyyMMdd`는 Asia/Seoul 기준 신청일이다.
- `dailySequence6`는 해당 날짜 안에서 1부터 증가하는 6자리 숫자다.
- 예시는 `G20260929000001`이다.
- 형식은 `{receiptCode}-{classificationNumber}{YYMMDD}{SequenceNo}`로 한다.
- `receiptCode`는 선택한 카테고리의 접수코드이고, `classificationNumber`는 선택한 카테고리의 분류번호다.
- `YYMMDD`는 Asia/Seoul 기준 신청일이다.
- `SequenceNo`는 선택한 카테고리와 신청일 조합 안에서 1부터 증가한다.
- `SequenceNo`는 최소 4자리로 왼쪽을 0으로 채우고, `9999`를 초과하면 자리수를 늘린다.
- 예시는 `A-1002609300001`, `C-20026093010000`이다.
- `applicationNo`는 unique이며 생성 후 변경하지 않는다.
- 동시에 여러 선물 신청이 발생해도 같은 `applicationNo`가 발급되지 않아야 한다.
- 구현은 DB unique 제약, 원자적 채번, 충돌 시 재시도 중 하나 이상의 방식으로 경합을 처리해야 한다.
@@ -129,12 +132,16 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
| 필드 | 타입 | 제약 | 설명 |
|---|---|---|---|
| `categoryCode` | string | 50자 이하, unique | 내부 분류번호. 사용자에게 표시하지 않음 |
| `name` | string | 50자 이하 | 사용자 표시명 |
| `classificationNumber` | string | 숫자 3자리, unique | 신청번호에 사용하는 분류번호. 사용자에게 별도 필드로 표시하지 않음 |
| `categoryCode` | string | 50자 이하, unique | 내부 범용코드. 사용자에게 표시하지 않음 |
| `name` | string | 50자 이하 | 분류명 |
| `receiptCode` | string | 영문 대문자 1~9자, unique | 신청번호에 사용하는 접수코드 |
| `representativeItem` | string | 100자 이하 | 대표품목. 사용자 폼 옵션의 `name` 조합에 사용 |
| `requiresDamageWaiver` | boolean | DDL은 `tinyint(1)` | 파손면책 동의 필요 여부 |
| `isActive` | boolean | DDL은 `tinyint(1)` | 활성 여부 |
`requiresDamageWaiver == true`인 카테고리를 선택한 팬은 선물 신청 시 `damageWaiverAgreed=true`를 추가로 보내야 한다.
사용자 선물 신청 폼 옵션에서는 카테고리 응답의 `name`을 `분류명/대표품목` 형태로 조합해 내려준다. 관리자 API에서는 `name`과 `representativeItem`을 각각 내려주고 각각 수정할 수 있어야 한다.
### 6.6 약관 동의
@@ -189,6 +196,8 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
- 현재 로그인 회원이 처리할 일이 아니거나 이미 처리할 수 없는 상태이면 해당 flag는 `false`다.
- 상세 조회는 정상 진행 상태 5개(`RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`, `DELIVERED`)의 날짜/시간을 순서대로 내려준다.
- `CANCELED`, `UNDELIVERABLE`은 정상 진행 타임라인 상태가 아니며, 각각 별도 종료 정보로 `canceledAt`, `undeliverableAt`, `undeliverableReason`을 내려준다.
- 선물 사용자/관리자 API의 모든 응답 시각 필드는 UTC ISO 문자열로 내려준다. 예: `2026-09-30T12:00:00Z`.
- 응답 시각 필드는 `Z` 또는 offset 없이 내려가는 `LocalDateTime` 기본 직렬화 형식을 사용하지 않는다.
### 6.9 리뷰
@@ -272,14 +281,14 @@ Response:
"categories": [
{
"categoryId": 1,
"name": "인형",
"name": "인형/피규어",
"requiresDamageWaiver": true
}
]
}
```
사용자 API는 `categoryCode`를 응답하지 않고 활성 카테고리만 내려준다.
사용자 API는 `categoryCode`, `classificationNumber`, `receiptCode`, `representativeItem`을 별도 필드로 응답하지 않고 활성 카테고리만 내려준다.
### 8.3 선물 보내기 등록
@@ -307,7 +316,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"status": "RECEIVED",
"statusName": "접수 완료",
"priceCan": 80,
@@ -342,7 +351,7 @@ Response:
{
"items": [
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"direction": "SENT",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
@@ -372,7 +381,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"direction": "SENT",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
@@ -381,7 +390,7 @@ Response:
"senderNickname": null,
"sizeName": "소형",
"categoryName": "인형",
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"paidCan": 80,
"tracking": "CJ대한통운_1234567890",
"shippingRequestedAt": null
@@ -440,7 +449,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"direction": "RECEIVED",
"status": "INSPECTION_COMPLETED",
"statusName": "검수완료",
@@ -534,7 +543,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"status": "TRACKING_REGISTERED",
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z"
}
@@ -557,7 +566,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"status": "CANCELED",
"refundedCan": 80
}
@@ -591,7 +600,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"recipientAddressRegisteredAt": "2026-10-03T03:00:00Z"
}
```
@@ -613,7 +622,7 @@ Response:
```json
{
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"status": "DELIVERED",
"deliveredAt": "2026-10-10T03:00:00Z"
}
@@ -645,7 +654,7 @@ Response:
```json
{
"reviewId": 1,
"applicationNo": "G20260929000001",
"applicationNo": "A-1002609300001",
"rating": 5,
"keywords": ["배송이 빨라요", "안내가 친절해요"],
"comment": "선물 전달 과정이 만족스러웠습니다.",
@@ -675,8 +684,11 @@ Response:
```json
{
"classificationNumber": "100",
"categoryCode": "DOLL",
"name": "인형",
"receiptCode": "A",
"representativeItem": "피규어",
"requiresDamageWaiver": true,
"isActive": true
}
@@ -804,8 +816,11 @@ Response:
| 필드 | 설명 |
|---|---|
| `id` | 내부 기본키 |
| `categoryCode` | 50자 이하 내부 분류번호 |
| `name` | 50자 이하 카테고리명 |
| `classificationNumber` | 숫자 3자리 unique 분류번호 |
| `categoryCode` | 50자 이하 unique 내부 범용코드 |
| `name` | 50자 이하 분류명 |
| `receiptCode` | 영문 대문자 1~9자 unique 접수코드 |
| `representativeItem` | 100자 이하 대표품목 |
| `requiresDamageWaiver` | 파손면책 동의 필요 여부. DDL은 `tinyint(1)` |
| `isActive` | 활성 여부. DDL은 `tinyint(1)` |
| `createdAt`, `updatedAt` | 생성/수정 시각 |
@@ -854,6 +869,9 @@ Response:
- 리뷰는 작성 권한, 상태, 중복 작성, 글자수 제한을 테스트한다.
- 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다.
- 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다.
- 선물 사용자/관리자 API response DTO의 날짜/시간 필드는 UTC ISO 문자열(`Z` 포함)로 직렬화되는지 controller test로 고정한다.
- 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다.
- 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다.
## 13. 성공 기준
@@ -865,6 +883,8 @@ Response:
- [ ] 팬은 `DELIVERED` 이후 리뷰를 1회 작성할 수 있다.
- [ ] 관리자 카테고리 논리삭제와 사이즈별 가격 변경이 새 신청에만 반영된다.
- [ ] 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발송된다.
- [ ] 선물 사용자/관리자 API의 날짜/시간 응답을 클라이언트가 UTC ISO 문자열로 해석할 수 있다.
- [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다.
## 14. Open Questions
@@ -880,6 +900,7 @@ Response:
| `GIFT-009` | 4 | 운영 상태변경 API 테스트 | 운영 상태 전이 확인 |
| `GIFT-010` | 5 | 리뷰 API 테스트 | 전달완료 후 리뷰 작성 확인 |
| `GIFT-011` | 6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 |
| `GIFT-014` | 8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 |
## 16. Decision Log
@@ -897,3 +918,5 @@ Response:
| 2026-09-29 | `DEC-010` | 확정 | 가격은 `basePriceCan`과 실제 결제 금액인 `salePriceCan`으로 표현한다 | 사용자 정정 | `GIFT-001`, `GIFT-002`, `GIFT-013` |
| 2026-09-29 | `DEC-011` | 확정 | 크리에이터 배송지 입력 시에도 약관 2개 동의와 동의시각을 별도 저장한다 | 사용자 정정 | `GIFT-007` |
| 2026-09-29 | `DEC-012` | 확정 | 파손면책 동의 필요 카테고리는 팬 신청 시 `damageWaiverAgreed=true`를 요구한다 | 사용자 선택 A | `GIFT-003` |
| 2026-09-30 | `DEC-013` | 확정 | 선물 사용자/관리자 API의 response datetime 필드는 `LocalDateTime` 기본 JSON 대신 UTC ISO 문자열로 내려준다 | 클라이언트가 offset 없는 `LocalDateTime` 문자열을 안전하게 해석하지 못함 | `GIFT-014` |
| 2026-09-30 | `DEC-014` | 확정 | 카테고리는 분류번호, 내부 범용 카테고리 코드, 분류명, 접수코드, 대표품목, 파손면책 조건을 관리하고 신청번호는 `receiptCode-classificationNumberYYMMDDSequenceNo`로 발급한다 | 관리자 운영 필드와 사용자 표시 요구가 분리됨 | `GIFT-001`, `GIFT-002`, `GIFT-012` |