Compare commits

..
2 Commits
3 changed files with 1007 additions and 0 deletions
@@ -0,0 +1,470 @@
# 선물 관리자 페이지 구현 프롬프트
이 문서는 관리자 페이지의 선물 관련 3개 화면을 프론트엔드 구현 에이전트에게 전달하기 위한 프롬프트 모음이다.
공통 전제:
- 관리자 메뉴에는 parent 메뉴 `선물함 관리` 아래에 다음 3개 하위 메뉴가 있다.
- `선물함 리스트` → `/gift/list`
- `선물 카테고리` → `/gift/category`
- `선물 사이즈` → `/gift/size`
- 모든 API 응답은 기존 공통 envelope를 사용한다. 화면에서는 `response.data`를 실제 payload로 사용한다.
- 인증/권한 처리는 기존 관리자 페이지의 API 클라이언트, 토큰 저장 방식, 에러 처리 방식을 따른다.
- 새 디자인 시스템을 만들지 말고 기존 관리자 페이지의 테이블, 필터, 버튼, 모달, 폼, 토스트, 확인창 패턴을 재사용한다.
- 화면 문구는 한국어로 작성한다.
- API 실패 시 기존 관리자 페이지의 공통 에러 표시 방식을 따른다.
---
## 1. 선물함 리스트 페이지 프롬프트
```text
관리자 선물함 리스트 페이지를 구현해줘.
목표:
- 관리자 메뉴 `선물함 관리 > 선물함 리스트`에서 전체 선물 신청 목록을 조회한다.
- 상태, 신청번호, 닉네임으로 필터링할 수 있다.
- 목록 row에서 선물 상세를 확인하고, 현재 상태에서 가능한 운영 액션을 실행할 수 있다.
- 기존 관리자 페이지의 테이블/필터/페이지네이션/모달/확인창 스타일을 그대로 따른다.
라우트:
- `/gift/list`
필수 화면 구성:
- 상단 제목: `선물함 리스트`
- 필터 영역:
- 상태 select: 전체, 접수 완료, 발송 확인, 사서함 도착, 검수 완료, 전달 완료, 전달 불가, 신청 취소
- 신청번호 검색 input: `applicationNo` 부분 검색
- 닉네임 검색 input: 발송인 또는 수취인 닉네임 부분 검색
- 검색 버튼, 초기화 버튼
- 목록 테이블 컬럼:
- 신청번호 `applicationNo`
- 발송인 닉네임 `senderNickname`
- 수취인 닉네임 `recipientNickname`
- 사이즈 `sizeName` / `sizeCode`
- 카테고리 `categoryName`
- 분류번호 `classificationNumber`
- 택배사 `courierCompanyName`
- 운송장번호 `trackingNumber`
- 상태 `statusName`
- 액션 버튼
- 페이지네이션:
- 서버의 `page`, `size`, `totalCount`, `hasNext` 기준으로 기존 관리자 페이지 패턴에 맞춘다.
목록 조회 API:
- Method: `GET`
- URL: `/api/v2/admin/gifts`
- Query:
- `status?: GiftStatus`
- `applicationNo?: string`
- `nickname?: string`
- `page?: number` 기본 `0`
- `size?: number` 기본 `20`
- 필터 조합:
- 선택 필터는 AND 조건이다.
- `nickname`은 발송인/수취인 닉네임 중 하나에 포함되면 매칭된다.
목록 조회 response `data`:
```ts
type AdminGiftListResponse = {
totalCount: number;
items: AdminGiftListItemResponse[];
page: number;
size: number;
hasNext: boolean;
};
type AdminGiftListItemResponse = {
applicationNo: string;
senderNickname: string;
recipientNickname: string;
sizeCode: string;
sizeName: string;
categoryName: string;
classificationNumber: string;
courierCompanyName: string | null;
trackingNumber: string | null;
status: GiftStatus;
statusName: string;
availableActions: AdminGiftAction[];
};
```
상태 enum:
```ts
type GiftStatus =
| "RECEIVED"
| "TRACKING_REGISTERED"
| "ARRIVED_AT_MAILBOX"
| "INSPECTION_COMPLETED"
| "DELIVERED"
| "UNDELIVERABLE"
| "CANCELED";
```
상태 표시명:
- `RECEIVED`: `접수 완료`
- `TRACKING_REGISTERED`: `발송 확인`
- `ARRIVED_AT_MAILBOX`: `사서함 도착`
- `INSPECTION_COMPLETED`: `검수 완료`
- `DELIVERED`: `전달 완료`
- `UNDELIVERABLE`: `전달 불가`
- `CANCELED`: `신청 취소`
상세 조회 API:
- Method: `GET`
- URL: `/api/v2/admin/gifts/{applicationNo}`
- 용도:
- 목록 row 클릭 또는 `상세` 버튼 클릭 시 상세 모달/상세 패널을 연다.
상세 response `data`:
```ts
type AdminGiftDetailResponse = {
applicationNo: string;
senderInfo: AdminGiftMemberInfoResponse;
recipientInfo: AdminGiftMemberInfoResponse;
productInfo: AdminGiftProductInfoResponse;
inboundDeliveryInfo: AdminGiftInboundDeliveryInfoResponse;
status: GiftStatus;
statusName: string;
availableActions: AdminGiftAction[];
};
type AdminGiftMemberInfoResponse = {
nickname: string;
name: string;
phoneNumber: string;
address: string;
};
type AdminGiftProductInfoResponse = {
sizeCode: string;
sizeName: string;
categoryName: string;
classificationNumber: string;
};
type AdminGiftInboundDeliveryInfoResponse = {
courierCompanyName: string | null;
trackingNumber: string | null;
};
```
상세 표시 요구사항:
- 발송인 정보: 닉네임, 이름, 전화번호, 주소
- 수취인 정보: 닉네임, 이름, 전화번호, 주소
- 상품 정보: 사이즈, 카테고리, 분류번호
- 입고 배송 정보: 택배사, 운송장번호
- 현재 상태와 가능한 액션
- 수취인이 배송지를 아직 입력하지 않은 경우 `recipientInfo.nickname`은 표시하고 `name`, `phoneNumber`, `address`는 빈 문자열로 온다. 화면에서는 `미입력`으로 표시해도 된다.
운영 액션 enum:
```ts
type AdminGiftAction =
| "ARRIVE_MAILBOX"
| "COMPLETE_INSPECTION"
| "COMPLETE_DELIVERY"
| "MARK_UNDELIVERABLE";
```
상태별 `availableActions`:
- `RECEIVED`: 없음
- `TRACKING_REGISTERED`: `ARRIVE_MAILBOX`, `MARK_UNDELIVERABLE`
- `ARRIVED_AT_MAILBOX`: `COMPLETE_INSPECTION`, `MARK_UNDELIVERABLE`
- `INSPECTION_COMPLETED`: `COMPLETE_DELIVERY`, `MARK_UNDELIVERABLE`
- `DELIVERED`: 없음
- `UNDELIVERABLE`: 없음
- `CANCELED`: 없음
액션 버튼 문구:
- `ARRIVE_MAILBOX`: `사서함 도착 처리`
- `COMPLETE_INSPECTION`: `검수 완료 처리`
- `COMPLETE_DELIVERY`: `전달 완료 처리`
- `MARK_UNDELIVERABLE`: `전달 불가 처리`
액션 API:
- 사서함 도착 처리
- Method: `POST`
- URL: `/api/v2/admin/gifts/{applicationNo}/arrive-mailbox`
- Body: 없음
- 검수 완료 처리
- Method: `POST`
- URL: `/api/v2/admin/gifts/{applicationNo}/complete-inspection`
- Body: 없음
- 전달 완료 처리
- Method: `POST`
- URL: `/api/v2/admin/gifts/{applicationNo}/complete-delivery`
- Body: 없음
- 전달 불가 처리
- Method: `POST`
- URL: `/api/v2/admin/gifts/{applicationNo}/mark-undeliverable`
- Body:
```ts
type AdminGiftMarkUndeliverableRequest = {
reason: string;
};
```
액션 response `data`:
```ts
type AdminGiftOperationStatusResponse = {
applicationNo: string;
status: GiftStatus;
statusName: string;
occurredAt: string; // UTC ISO string, e.g. 2026-09-30T12:00:00Z
};
```
액션 UX:
- 모든 상태 변경 액션은 확인창을 띄운다.
- `MARK_UNDELIVERABLE`은 사유 입력 모달을 띄운다.
- 전달 불가 사유는 공백만 입력할 수 없고 255자 이내로 제한한다.
- 액션 성공 후:
- 상세 모달이 열려 있으면 상세를 다시 조회한다.
- 목록도 현재 필터/페이지 기준으로 다시 조회한다.
- 성공 토스트를 표시한다.
- 액션 실패 시 기존 관리자 페이지 에러 토스트/알림 패턴을 따른다.
빈 값 표시:
- `courierCompanyName`, `trackingNumber`가 null이면 `-`로 표시한다.
- 수취인 개인정보가 빈 문자열이면 `미입력`으로 표시한다.
테스트/검증:
- API 클라이언트 함수 단위 테스트 또는 페이지 테스트에서 query parameter가 올바르게 전달되는지 확인한다.
- `availableActions`에 따라 버튼 노출이 달라지는지 확인한다.
- 전달 불가 사유 빈 값 validation을 확인한다.
```
---
## 2. 선물 카테고리 CRUD 페이지 프롬프트
```text
관리자 선물 카테고리 CRUD 페이지를 구현해줘.
목표:
- 관리자 메뉴 `선물함 관리 > 선물 카테고리`에서 선물 카테고리를 조회, 등록, 수정, 비활성화한다.
- 기존 관리자 페이지의 CRUD 테이블, 등록/수정 모달, 삭제 확인창, 토스트 패턴을 그대로 따른다.
라우트:
- `/gift/category`
필수 화면 구성:
- 상단 제목: `선물 카테고리`
- 상단 우측 `카테고리 등록` 버튼
- 목록 테이블 컬럼:
- 카테고리 ID `categoryId`
- 분류번호 `classificationNumber`
- 카테고리 코드 `categoryCode`
- 카테고리명 `name`
- 접수 코드 `receiptCode`
- 대표 품목 `representativeItem`
- 파손면책 동의 필요 여부 `requiresDamageWaiver`
- 활성 여부 `isActive`
- 관리 버튼: 수정, 비활성화
- 등록/수정 폼 필드:
- `classificationNumber`
- `categoryCode`
- `name`
- `receiptCode`
- `representativeItem`
- `requiresDamageWaiver`
- `isActive`
목록 조회 API:
- Method: `GET`
- URL: `/api/v2/admin/gift-categories`
- Request: 없음
- Response `data`:
```ts
type AdminGiftCategoryResponse = {
categoryId: number;
classificationNumber: string;
categoryCode: string;
name: string;
receiptCode: string;
representativeItem: string;
requiresDamageWaiver: boolean;
isActive: boolean;
};
```
등록 API:
- Method: `POST`
- URL: `/api/v2/admin/gift-categories`
- Body:
```ts
type AdminGiftCategoryRequest = {
classificationNumber: string;
categoryCode: string;
name: string;
receiptCode: string;
representativeItem: string;
requiresDamageWaiver: boolean;
isActive: boolean;
};
```
- Response `data`: `AdminGiftCategoryResponse`
수정 API:
- Method: `PUT`
- URL: `/api/v2/admin/gift-categories/{categoryId}`
- Body: `AdminGiftCategoryRequest`
- Response `data`: `AdminGiftCategoryResponse`
비활성화 API:
- Method: `DELETE`
- URL: `/api/v2/admin/gift-categories/{categoryId}`
- Body: 없음
- Response `data`: `AdminGiftCategoryResponse`
- 서버는 실제 삭제가 아니라 `isActive=false` 논리 삭제로 처리한다.
폼 validation:
- `classificationNumber`: 필수, 숫자 3자리. 예: `100`
- `categoryCode`: 필수, 50자 이하. 예: `DOLL`
- `name`: 필수, 50자 이하. 예: `인형`
- `receiptCode`: 필수, 영문 대문자 1~9자. 예: `A`
- `representativeItem`: 필수, 100자 이하. 예: `피규어`
- `requiresDamageWaiver`: boolean
- `isActive`: boolean
UX 요구사항:
- 등록 성공 후 목록을 다시 조회하고 모달을 닫는다.
- 수정 성공 후 목록을 다시 조회하고 모달을 닫는다.
- 비활성화 버튼은 확인창을 띄운 뒤 호출한다.
- 이미 비활성화된 row는 비활성화 버튼을 disabled 처리한다.
- boolean 값은 기존 관리자 페이지 표현에 맞춰 `예/아니오`, `활성/비활성` 또는 badge로 표시한다.
- `categoryCode`는 수정 폼에 표시하되 서버 쪽 엔티티는 코드 변경을 실제로 반영하지 않을 수 있으므로, 가능하면 수정 시 읽기 전용으로 두고 나머지 운영 필드만 수정하도록 UX를 구성한다.
테스트/검증:
- 목록 조회가 `GET /api/v2/admin/gift-categories`를 호출하는지 확인한다.
- 등록/수정 body가 `AdminGiftCategoryRequest` 형태와 일치하는지 확인한다.
- validation 실패 시 API를 호출하지 않고 폼 에러를 표시한다.
- 비활성화 성공 후 해당 row의 `isActive=false`가 반영되는지 확인한다.
```
---
## 3. 선물 사이즈 가격 CRUD 페이지 프롬프트
```text
관리자 선물 사이즈 가격 CRUD 페이지를 구현해줘.
목표:
- 관리자 메뉴 `선물함 관리 > 선물 사이즈`에서 선물 사이즈별 기본가/판매가/활성 여부를 조회하고 수정한다.
- 현재 API는 사이즈 가격의 목록 조회와 수정만 제공한다. 신규 생성과 물리 삭제 기능은 만들지 않는다.
- 기존 관리자 페이지의 테이블, 수정 모달, 저장 확인/토스트 패턴을 그대로 따른다.
라우트:
- `/gift/size`
필수 화면 구성:
- 상단 제목: `선물 사이즈`
- 목록 테이블 컬럼:
- 사이즈 코드 `sizeCode`
- 사이즈명 `name`
- 기본가 `basePriceCan`
- 판매가 `salePriceCan`
- 활성 여부 `isActive`
- 관리 버튼: 수정
- 수정 폼 필드:
- `sizeCode`: 읽기 전용
- `name`: 읽기 전용
- `basePriceCan`: number input
- `salePriceCan`: number input
- `isActive`: checkbox/switch
목록 조회 API:
- Method: `GET`
- URL: `/api/v2/admin/gift-size-prices`
- Request: 없음
- Response `data`:
```ts
type AdminGiftSizePriceResponse = {
sizeCode: GiftSize;
name: string;
basePriceCan: number;
salePriceCan: number;
isActive: boolean;
};
```
수정 API:
- Method: `PUT`
- URL: `/api/v2/admin/gift-size-prices/{sizeCode}`
- Body:
```ts
type AdminGiftSizePriceRequest = {
basePriceCan: number;
salePriceCan: number;
isActive: boolean;
};
```
- Response `data`: `AdminGiftSizePriceResponse`
사이즈 enum:
```ts
type GiftSize = "SMALL" | "MEDIUM" | "LARGE";
```
서버 validation:
- `salePriceCan`은 0보다 커야 한다.
- `salePriceCan`은 `basePriceCan`보다 클 수 없다.
프론트 validation:
- `basePriceCan`: 필수, 1 이상 정수
- `salePriceCan`: 필수, 1 이상 정수
- `salePriceCan <= basePriceCan`
- validation 실패 시 API 호출을 막고 필드 에러를 표시한다.
UX 요구사항:
- 가격은 숫자 입력이지만 테이블 표시에서는 기존 관리자 페이지의 숫자 포맷을 따른다.
- 수정 버튼 클릭 시 현재 row 값을 폼 초기값으로 넣는다.
- 저장 전 확인창은 기존 관리자 페이지 패턴이 있으면 따른다.
- 저장 성공 후 목록을 다시 조회하고 모달을 닫는다.
- 신규 생성/삭제 버튼은 만들지 않는다.
테스트/검증:
- 목록 조회가 `GET /api/v2/admin/gift-size-prices`를 호출하는지 확인한다.
- 수정 저장 시 `PUT /api/v2/admin/gift-size-prices/{sizeCode}`와 request body가 정확한지 확인한다.
- `salePriceCan > basePriceCan`일 때 API를 호출하지 않고 validation 메시지를 표시하는지 확인한다.
- 저장 성공 후 목록 refresh가 일어나는지 확인한다.
```
---
## 구현 시 참고할 공통 타입
```ts
type ApiResponse<T> = {
success: boolean;
data: T;
message?: string;
};
type GiftStatus =
| "RECEIVED"
| "TRACKING_REGISTERED"
| "ARRIVED_AT_MAILBOX"
| "INSPECTION_COMPLETED"
| "DELIVERED"
| "UNDELIVERABLE"
| "CANCELED";
type AdminGiftAction =
| "ARRIVE_MAILBOX"
| "COMPLETE_INSPECTION"
| "COMPLETE_DELIVERY"
| "MARK_UNDELIVERABLE";
type GiftSize = "SMALL" | "MEDIUM" | "LARGE";
```
## 구현 순서 추천
1. API client 함수와 TypeScript 타입을 먼저 추가한다.
2. `/gift/list` 선물함 리스트와 상세/상태변경 모달을 구현한다.
3. `/gift/category` 카테고리 CRUD를 구현한다.
4. `/gift/size` 사이즈 가격 수정 화면을 구현한다.
5. 기존 관리자 메뉴의 `선물함 관리` 하위 route와 연결되는지 확인한다.
6. 각 페이지별 loading/empty/error/success 상태를 확인한다.
@@ -0,0 +1,274 @@
# 선물함 리스트 페이지 생성 프롬프트
이 문서는 Android/iOS 앱의 `선물함` 리스트 페이지를 생성할 때 사용하는 프롬프트다.
참고 Figma:
- 팬 모드, 신청한 내역이 있을 때: `2481:19382`
- 팬 모드, 신청한 내역이 없을 때: `2520:35587`
- 크리에이터 모드, 받은 선물 내역이 없을 때: `2531:36283`
- 크리에이터 모드, 받은 선물 내역이 있을 때: `2531:36194`
- 크리에이터 모드, 받은 선물 상태별 카드 표시: `2531:36237`
전제:
- Android/iOS 각각의 기존 디자인 시스템, 컴포넌트, API client, 상태 관리, toast/dialog, navigation 패턴을 재사용한다.
- Figma의 visual structure를 기준으로 구현하되, 아래 사용자 요구사항을 Figma보다 우선한다.
- 상단 뒤로가기 내비게이션 영역은 고정한다.
- 상단 뒤로가기 내비게이션 영역을 제외한 전체 콘텐츠 영역은 스크롤되어야 한다.
- `신청 내역`, `선물 내역` 섹션 타이틀은 표시하지 않는다.
- 리스트는 신청일/취소일 최신순으로 보여야 한다.
- request/response 설명은 모바일 공통 JSON 기준이다.
---
## 모바일 앱 구현 프롬프트
```text
Android/iOS 앱의 선물함 리스트 페이지를 Figma 기준으로 생성하고 API 연동을 구현해줘.
Figma는 두 관점의 상태를 보여준다.
- 보낸 선물 관점: 사용자가 본인이 신청한 선물 내역을 확인하는 상태다. Figma `2481:19382`, `2520:35587`를 기준으로 한다.
- 받은 선물 관점: 사용자가 본인이 받을 선물 내역을 확인하는 상태다. Figma `2531:36283`, `2531:36194`, `2531:36237`를 기준으로 한다.
- 실제 선물함 API는 두 관점을 나누어 호출하지 않고 `type=ALL`로 한 번 호출한다.
- 응답 item의 `direction` 값이 `SENT`이면 보낸 선물 카드로, `RECEIVED`이면 받은 선물 카드로 렌더링한다.
- 각 카드에는 상대방 프로필 이미지와 닉네임을 표시한다.
- `direction=SENT`: 상대방은 선물을 받는 크리에이터다.
- `direction=RECEIVED`: 상대방은 선물을 보낸 팬이다.
중요 요구사항:
- 상단 뒤로가기 내비게이션은 고정한다.
- 상단 내비게이션 아래의 전체 콘텐츠만 스크롤되게 한다.
- Figma에 보이는 `신청 내역`, `선물 내역` 섹션 타이틀은 실제 구현에서 표시하지 않는다.
- 선물함 리스트는 신청일/취소일 최신순으로 정렬한다.
- 현재 API 응답만으로 취소일 기준 정렬/표시는 완전하지 않으므로, API가 취소일을 내려주지 않으면 서버 응답 순서를 우선 사용하고 취소 건 날짜 라벨은 `createdAt` 기준으로 표시한다. 취소일 표시가 필수이면 API 보강이 필요하다.
API 호출:
- 선물함 리스트는 항상 `GET /api/v2/gifts?type=ALL&page=0&size=20`로 조회한다.
- `SENT`, `RECEIVED`로 따로 호출하지 않는다.
- `items[].direction`으로 보낸 선물/받은 선물 UI를 결정한다.
- 페이지 진입 시 첫 페이지를 조회한다.
- `hasNext=true`이면 스크롤 하단에서 다음 page를 추가 조회한다.
- pull-to-refresh가 기존 앱에 있으면 page를 0으로 초기화하고 다시 조회한다.
공통 레이아웃:
1. 상단 고정 내비게이션
- 타이틀: `선물함`
- 뒤로가기 버튼은 기존 앱 패턴을 사용한다.
- 이 영역은 스크롤되지 않는다.
2. 스크롤 콘텐츠
- nav 아래부터 시작한다.
- 안내 배너, 빈 상태, 리스트, 하단 여백 또는 하단 문의 버튼까지 스크롤 영역에 포함한다.
- iOS safe area와 Android navigation bar 여백은 기존 앱 패턴을 따른다.
보낸 선물 UI:
- `direction=SENT`인 item에 적용한다.
- 사용자가 신청한 선물 내역을 보여준다.
- 보낸 선물만 있는 상태는 Figma `2481:19382`를 따른다.
- 상단 안내 배너를 표시한다.
- 제목: `사서함을 통해 안전하게 전달됩니다`
- 본문: `크리에이터 및 팬의 주소는 공개되지 않습니다. 선물은 소다라이브 사서함에 도착한 뒤 크리에이터에게 전달됩니다.`
- `신청 내역` 섹션 타이틀은 표시하지 않는다.
- 카드 구성:
- 상단 왼쪽: `statusName`
- 상단 오른쪽: 날짜 라벨
- 기본: `{createdAt} 신청`
- 상태가 `CANCELED`이고 취소일 필드가 있으면 `{canceledAt} 취소`
- 현재 리스트 API에는 `canceledAt`이 없으므로 기존 API만 사용할 때는 `{createdAt} 취소` 또는 `{createdAt} 신청` 중 제품 정책에 맞춰 하나로 통일한다.
- 프로필 row 내부: `counterpartProfileImageUrl`, `counterpartNickname`을 사용해 받는 크리에이터 프로필을 표시한다.
- 프로필 row 아래 보조 문구: `sizeName · categoryName`
- 프로필 이미지가 없으면 기존 앱의 기본 프로필 이미지를 표시한다.
- 우측 chevron
- 탭 시 선물 상세 페이지로 이동하며 `applicationNo`를 전달한다.
- 리스트가 비어 있고 보낸 선물 관점으로 진입한 화면이면 Figma `2520:35587`를 따른다.
- 상단 안내 배너만 표시한다.
- 별도 empty title/body를 추가하지 않는다.
받은 선물 UI:
- `direction=RECEIVED`인 item에 적용한다.
- 사용자가 받을 선물 내역을 보여준다.
- 리스트가 비어 있고 받은 선물 관점으로 진입한 화면이면 Figma `2531:36283`를 따른다.
- 가운데 empty title: `전달 예정인 선물이 없어요`
- empty body: `베타 기간에는 팬이 선물을 보내는 기능만 제공돼요. 더 다양해진 선물 기능으로 곧 다시 만나요!`
- 버튼: `의견 남기기`
- 의견 남기기 동작은 기존 앱의 문의/피드백 이동 패턴을 사용한다.
- 받은 선물이 있는 상태는 Figma `2531:36194`, `2531:36237`를 따른다.
- 상단 안내 배너를 표시한다.
- 제목: `배송지 입력 기간 안내`
- 본문: `알림을 받은 날부터 7일 이내에 배송지를 입력해 주세요. 기한 내 입력하지 않으면 선물이 반송됩니다.`
- `선물 내역` 섹션 타이틀은 표시하지 않는다.
- 배송지 입력이 필요한 선물이 있으면 파란 안내 배너를 리스트 상단에 표시한다.
- 제목: `팬이 보낸 선물이 있어요!`
- 본문: `선물이 늦지 않게 전달될 수 있도록 배송지를 입력해 주세요.`
- 보조 문구: `*n일 이내로 입력하지 않으면 선물이 사라져요.`
- 현재 리스트 API에는 배송지 입력 마감일이 없으므로 정확한 n일 계산이 필요하면 API 보강이 필요하다.
- 배너 탭 시 배송지 입력이 필요한 첫 번째 선물 상세로 이동한다.
- 카드 구성:
- 상단 왼쪽 프로필 row 내부: `counterpartProfileImageUrl`, `counterpartNickname`을 사용해 보낸 팬 프로필을 표시한다.
- 상단 오른쪽: `{createdAt} 신청`
- 본문 메인: 상태 표시 문구
- 본문 서브: 상태 보조 문구가 필요한 경우만 표시
- 우측 chevron
- 탭 시 선물 상세 페이지로 이동하며 `applicationNo`를 전달한다.
- 리스트 하단에는 Figma처럼 `문의하기` 하단 버튼을 둘 수 있다. 기존 앱 정책상 고정 버튼이면 하단 safe area를 포함하고, 스크롤 콘텐츠 내부 버튼이면 콘텐츠 하단에 둔다.
상태 표시 매핑:
보낸 선물 카드:
| API status | 표시 문구 |
|---|---|
| `RECEIVED` | `운송장 등록 필요` |
| `TRACKING_REGISTERED` | `발송 확인` |
| `ARRIVED_AT_MAILBOX` | `사서함 도착` |
| `INSPECTION_COMPLETED` | `선물 검수 완료` |
| `DELIVERED` | `전달 완료` |
| `UNDELIVERABLE` | `전달 불가` |
| `CANCELED` | `신청 취소` |
받은 선물 카드:
| API status | 메인 문구 | 서브 문구 |
|---|---|---|
| `TRACKING_REGISTERED` | `배송지 입력 필요` | `전달 예정` |
| `ARRIVED_AT_MAILBOX` | `선물 검수` | `선물 확인 중` |
| `INSPECTION_COMPLETED` | `선물 검수 완료` | `배송 중` |
| `DELIVERED` | `전달 완료` | 없음 |
| `UNDELIVERABLE` | `전달 불가` | 없음 |
API 명세:
선물함 리스트 조회 API:
- Method: `GET`
- URL: `/api/v2/gifts`
- 인증: 로그인 필요
- Query:
- `type`: `ALL`, `SENT`, `RECEIVED`
- `page`: 0부터 시작
- `size`: 페이지 크기. 기본 20
- 이 페이지에서는 `type=ALL`만 사용한다.
- Response envelope 예시:
```json
{
"success": true,
"data": {
"totalCount": 1,
"items": [],
"page": 0,
"size": 20,
"hasNext": false
},
"message": ""
}
```
Response `data` 예시:
```json
{
"totalCount": 2,
"items": [
{
"applicationNo": "A-1002609300001",
"direction": "SENT",
"status": "RECEIVED",
"statusName": "접수 완료",
"priceCan": 80,
"categoryName": "아크릴/스탠드",
"sizeName": "소형",
"counterpartMemberId": 200,
"counterpartNickname": "달빛수집가",
"counterpartProfileImageUrl": "https://example.com/profile.png",
"createdAt": "2026-09-18T03:00:00Z"
}
],
"page": 0,
"size": 20,
"hasNext": false
}
```
Response `data` 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
| `totalCount` | number | 전체 개수 |
| `items` | array | 선물함 리스트 |
| `items[].applicationNo` | string | 선물 신청번호. 상세 이동 ID |
| `items[].direction` | string | `SENT` 또는 `RECEIVED` |
| `items[].status` | string | 선물 상태 코드 |
| `items[].statusName` | string | 서버 상태 표시명 |
| `items[].priceCan` | number | 사용된 캔 |
| `items[].categoryName` | string | 카테고리명 |
| `items[].sizeName` | string | 사이즈명 |
| `items[].counterpartMemberId` | number | 상대방 회원 ID. `SENT`에서는 받는 크리에이터, `RECEIVED`에서는 보낸 팬 |
| `items[].counterpartNickname` | string | 상대방 닉네임 |
| `items[].counterpartProfileImageUrl` | string 또는 null | 상대방 프로필 이미지 URL. 없으면 기본 프로필 이미지 표시 |
| `items[].createdAt` | string 또는 null | 신청일. UTC ISO 문자열 |
| `page` | number | 현재 페이지 |
| `size` | number | 페이지 크기 |
| `hasNext` | boolean | 다음 페이지 존재 여부 |
정렬:
- 현재 서버 API는 `createdAt desc, id desc`로 정렬한다.
- 요구사항은 신청일/취소일 최신순이다.
- 취소일 최신순까지 정확히 맞추려면 리스트 API가 `canceledAt` 또는 `displayedAt` 같은 정렬 기준 시각을 내려주고 그 기준으로 정렬해야 한다.
- API 보강 전에는 서버 응답 순서를 그대로 사용하고, 클라이언트에서 임의 재정렬하지 않는다.
상대방 프로필 표시 규칙:
- `counterpartNickname`은 카드의 프로필 row 이름으로 표시한다.
- `counterpartProfileImageUrl`이 있으면 해당 이미지를 원형 프로필 이미지로 표시한다.
- `counterpartProfileImageUrl`이 `null`이거나 빈 값이면 기존 앱의 기본 프로필 이미지를 표시한다.
- `direction=SENT`에서는 받는 크리에이터 정보로 해석한다.
- `direction=RECEIVED`에서는 보낸 팬 정보로 해석한다.
현재 API만으로 부족한 Figma 표시값:
- 취소일 `canceledAt`
- 배송지 입력 마감일 또는 남은 일수
추가 API 보강이 가능하면 리스트 item에 아래 필드를 추가해달라고 요청한다.
```json
{
"canceledAt": "2026-09-18T03:00:00Z",
"recipientAddressDeadlineAt": "2026-09-25T03:00:00Z",
"sortAt": "2026-09-18T03:00:00Z"
}
```
- `canceledAt`: 취소 상태의 취소일 표시용
- `recipientAddressDeadlineAt`: 크리에이터 배송지 입력 안내 배너의 남은 일수 계산용
- `sortAt`: 신청/취소 최신순 정렬 기준. 취소 건은 취소일, 그 외에는 신청일
로딩/에러/빈 상태:
- 최초 로딩 중에는 기존 앱 리스트 스켈레톤 또는 loading 패턴을 사용한다.
- 실패 시 기존 앱 toast/dialog와 재시도 버튼을 사용한다.
- 빈 상태는 진입 관점에 따라 다르게 표시한다.
- 보낸 선물 관점으로 진입한 화면이면 안내 배너만 남긴다.
- 받은 선물 관점으로 진입한 화면이면 중앙 empty UI와 `의견 남기기` 버튼을 표시한다.
테스트/검증:
- 선물함 진입 시 `type=ALL`로 API를 한 번 호출하는지 확인한다.
- `direction=SENT` item은 보낸 선물 카드 UI로 표시되는지 확인한다.
- `direction=RECEIVED` item은 받은 선물 카드 UI로 표시되는지 확인한다.
- 각 item의 `counterpartNickname`, `counterpartProfileImageUrl`이 프로필 row에 표시되는지 확인한다.
- `counterpartProfileImageUrl=null`이면 기본 프로필 이미지가 표시되는지 확인한다.
- 상단 내비게이션은 고정되고 아래 콘텐츠만 스크롤되는지 확인한다.
- `신청 내역`, `선물 내역` 텍스트가 실제 화면에 노출되지 않는지 확인한다.
- 리스트 item 탭 시 `applicationNo`로 상세 화면 이동이 되는지 확인한다.
- 빈 상태가 진입 관점에 맞는 Figma 기준으로 표시되는지 확인한다.
- `hasNext=true`일 때 다음 page를 추가 조회하는지 확인한다.
- API 실패 시 입력/스크롤 상태를 깨지 않고 오류와 재시도를 제공하는지 확인한다.
```
---
## API 요약
| 목적 | Method | URL | Query | Response data |
|---|---|---|---|---|
| 선물함 리스트 조회 | GET | `/api/v2/gifts` | `type=ALL`, `page`, `size` | 선물 리스트, 페이지 정보 |
## Figma 반영 메모
- 보낸 선물 리스트 있음: 안내 배너 + 카드 리스트. 단, 실제 구현에서는 `신청 내역` 타이틀을 제거한다.
- 보낸 선물 관점 empty: 안내 배너만 표시한다.
- 받은 선물 관점 empty: 중앙 empty UI와 `의견 남기기` 버튼을 표시한다.
- 받은 선물 리스트 있음: 안내 배너 + 배송지 입력 CTA 배너 + 카드 리스트. 단, 실제 구현에서는 `선물 내역` 타이틀을 제거한다.
- 받은 선물 상태별 카드는 `배송지 입력 완료/전달 예정`, `전달 완료`, `전달 불가`, `선물 검수/선물 확인 중`, `선물 검수 완료/배송 중` 패턴을 사용한다.
@@ -0,0 +1,263 @@
# 선물 보내기 페이지 생성 프롬프트
이 문서는 Android/iOS 앱의 `선물 보내기` 페이지를 생성하거나 기존 생성 UI에 API 연동을 붙일 때 사용하는 프롬프트다.
참고 Figma:
- 빈 입력 상태: `2481:18904`
- 입력 완료 및 CTA 활성화 상태: `2481:18957`
전제:
- 크리에이터 닉네임과 `memberId`를 받아 `선물 보내기` 화면으로 이동하는 흐름은 이미 생성되어 있다.
- 새 진입 흐름을 만들지 않는다.
- Figma에 있는 UI 구조와 문구를 유지한다.
- Android/iOS 각각의 기존 디자인 시스템, 컴포넌트, API client, 상태 관리, toast/dialog, navigation 패턴을 재사용한다.
- 모든 API 응답은 공통 envelope를 사용하며 실제 payload는 `data`에 있다.
- request/response 설명은 모바일 공통 JSON 기준이다.
---
## 모바일 앱 구현 프롬프트
```text
Android/iOS 앱의 선물 보내기 페이지를 Figma 기준으로 생성하고 API 연동을 구현해줘.
중요 전제:
- 크리에이터 닉네임과 memberId를 받아 이 화면으로 이동하는 기능은 이미 구현되어 있다.
- 이 작업에서는 이동 경로를 새로 만들지 말고, 전달받은 값만 화면과 API request에 연결한다.
- 전달받은 크리에이터 닉네임은 `받는 크리에이터` 영역에 표시한다.
- 전달받은 memberId는 선물 신청 API의 `recipientMemberId`로 보낸다.
Figma 참고 상태:
- 빈 입력 상태: node `2481:18904`
- 입력 완료 및 최하단 CTA 활성화 상태: node `2481:18957`
화면 구조:
1. 상단
- 타이틀: `선물 보내기`
- 뒤로가기 버튼은 기존 앱 패턴을 사용한다.
2. 안내 배너
- 제목: `선물 보내기 베타 서비스 안내`
- 본문: `베타 기간 동안은 일부 기능만 제공됩니다. 크리에이터에게 마음을 잘 전할 수 있도록 더 넓어진 선물 보내기로 곧 다시 만나요!`
3. 받는 크리에이터
- 이전 화면에서 전달받은 크리에이터 닉네임을 표시한다.
- Figma의 프로필 row 형태를 유지한다.
- memberId는 화면 표시용이 아니라 API request용으로만 사용한다.
4. 선물 사이즈
- 화면 진입 시 `GET /api/v2/gifts/form-options`를 호출해 `sizes`를 가져온다.
- 조회된 `sizes`를 사이즈 카드 목록에 바인딩한다.
- 카드에는 `name`, 사이즈 설명, 캔 가격을 표시한다.
- 서버 응답에는 사이즈 설명 필드가 없으므로 설명 문구는 앱에서 코드별로 매핑한다.
- `SMALL`: `세 변의 합 100cm 이하 · 5kg 이하`
- `MEDIUM`, `LARGE`: 정책 문구가 앱에 이미 있으면 기존 문구를 사용하고, 없으면 이름과 가격만 표시한다.
- 가격은 `salePriceCan`을 표시하고 submit 금액도 `salePriceCan`을 사용한다.
- `basePriceCan`과 `salePriceCan`이 다르면 기존 앱 할인/정가 표시 패턴이 있을 때만 정가를 함께 보여준다.
- 기본 선택은 서버가 내려준 첫 번째 사이즈로 둔다.
5. 카테고리
- 화면 진입 시 같은 `GET /api/v2/gifts/form-options` 응답의 `categories`를 사용한다.
- Figma의 select field를 유지한다.
- 빈 상태 문구: `카테고리 선택`
- 선택 후에는 선택한 카테고리 `name`을 표시한다.
- 카테고리 선택 UI는 기존 Android/iOS 앱 패턴에 맞는 bottom sheet, picker, dialog 중 이미 쓰는 방식을 사용한다.
- `requiresDamageWaiver=true`인 카테고리를 선택한 경우에만 카테고리 아래에 파손 면책 동의 row를 표시한다.
- 파손 면책 동의 문구: `파손 및 분실 면책 사항에 동의합니다. (필수)`
- `requiresDamageWaiver=false` 카테고리를 선택하면 파손 면책 동의 row를 숨기고 `damageWaiverAgreed=false`로 초기화한다.
6. 보내는 사람
- 이름 input → `senderName`
- 휴대폰 번호 input → `senderPhoneNumber`
- 우편번호 input → `senderZipCode`
- 주소 input → `senderAddress`
- 상세 주소 input → `senderAddressDetail`
- `우편번호 검색` 버튼은 기존 주소 검색 기능과 연결한다.
- 주소 검색 결과로 우편번호와 기본 주소를 채운다.
- 기본 주소는 사용자가 직접 수정하지 못하게 하는 기존 패턴이 있으면 그 패턴을 따른다.
7. 이용 약관 동의
- Figma의 발송 규정 요약 박스와 체크박스 2개를 유지한다.
- `소다라이브 크리에이터 상품 전달 이용약관에 동의합니다. (필수)` → `senderTermsAgreed`
- `상품 전달을 위한 개인정보 수집·이용에 동의합니다. (필수)` → `senderPrivacyAgreed`
8. 하단 CTA
- 빈 상태 또는 필수값 누락 상태는 Figma 빈 상태처럼 비활성 버튼을 표시한다.
- 활성 조건을 모두 만족하면 Figma 활성 상태처럼 soda 색상 CTA로 바꾼다.
- 활성 CTA 문구는 선택된 사이즈의 `salePriceCan`을 사용해 `{salePriceCan}캔으로 선물 보내기`로 표시한다.
- submit 중에는 중복 탭을 막고 loading 상태를 표시한다.
화면 진입 시 처리:
- 이미 전달받은 `creatorNickname`과 `memberId`를 읽는다.
- 둘 중 하나라도 없으면 기존 앱의 오류 처리 또는 뒤로가기 패턴을 따른다.
- 즉시 `GET /api/v2/gifts/form-options`를 호출한다.
- 로딩 중에는 기존 화면 스켈레톤/로딩 패턴을 사용한다.
- 조회 실패 시 toast/dialog와 재시도 동작을 기존 패턴으로 제공한다.
- `sizes` 또는 `categories`가 비어 있으면 선물 신청을 막고 CTA를 비활성화한다.
CTA 활성 조건:
- `memberId`가 있다.
- 사이즈가 선택되어 있다.
- 카테고리가 선택되어 있다.
- 이름이 입력되어 있다.
- 휴대폰 번호가 입력되어 있다.
- 우편번호가 입력되어 있다.
- 주소가 입력되어 있다.
- 이용약관 동의가 true다.
- 개인정보 수집·이용 동의가 true다.
- 선택한 카테고리의 `requiresDamageWaiver=true`이면 파손 면책 동의가 true다.
검증 실패 처리:
- CTA는 기본적으로 위 조건을 만족할 때만 활성화한다.
- 그래도 submit 시점에 한 번 더 validation 한다.
- 누락된 필드는 기존 앱의 input error, toast, dialog 중 현재 화면 패턴에 맞춰 안내한다.
- validation 실패 시 `POST /api/v2/gifts`를 호출하지 않는다.
폼 옵션 조회 API:
- Method: `GET`
- URL: `/api/v2/gifts/form-options`
- Request: 없음
- Response envelope 예시:
```json
{
"success": true,
"data": {
"sizes": [],
"categories": []
},
"message": ""
}
```
- Response `data` 예시:
```json
{
"sizes": [
{
"sizeCode": "SMALL",
"name": "소형",
"basePriceCan": 100,
"salePriceCan": 80
}
],
"categories": [
{
"categoryId": 1,
"name": "아크릴",
"requiresDamageWaiver": true
}
]
}
```
폼 옵션 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
| `sizes` | array | 선택 가능한 선물 사이즈 목록 |
| `sizes[].sizeCode` | string | 사이즈 코드. `SMALL`, `MEDIUM`, `LARGE` |
| `sizes[].name` | string | 사이즈 표시명 |
| `sizes[].basePriceCan` | number | 기본 가격, 단위는 캔 |
| `sizes[].salePriceCan` | number | 실제 결제 가격, 단위는 캔 |
| `categories` | array | 선택 가능한 활성 카테고리 목록 |
| `categories[].categoryId` | number | 카테고리 ID |
| `categories[].name` | string | 카테고리 표시명 |
| `categories[].requiresDamageWaiver` | boolean | 파손 면책 동의 필요 여부 |
선물 신청 접수 API:
- Method: `POST`
- URL: `/api/v2/gifts`
- 인증: 로그인 회원 필요. 기존 앱 인증 토큰/세션 처리 방식을 사용한다.
- Request body 예시:
```json
{
"recipientMemberId": 100,
"senderName": "김소다",
"senderPhoneNumber": "01000000000",
"senderZipCode": "12345",
"senderAddress": "서울시 강남구 ...",
"senderAddressDetail": "123동 456호",
"sizeCode": "SMALL",
"categoryId": 1,
"senderTermsAgreed": true,
"senderPrivacyAgreed": true,
"damageWaiverAgreed": true
}
```
Request body 필드:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `recipientMemberId` | number | Y | 화면 진입 시 전달받은 memberId |
| `senderName` | string | Y | 보내는 사람 이름 |
| `senderPhoneNumber` | string | Y | 보내는 사람 휴대폰 번호 |
| `senderZipCode` | string | Y | 보내는 사람 우편번호 |
| `senderAddress` | string | Y | 보내는 사람 기본 주소 |
| `senderAddressDetail` | string 또는 null | N | 보내는 사람 상세 주소 |
| `sizeCode` | string | Y | 선택한 사이즈 코드 |
| `categoryId` | number | Y | 선택한 카테고리 ID |
| `senderTermsAgreed` | boolean | Y | 이용약관 동의 여부. 반드시 `true` |
| `senderPrivacyAgreed` | boolean | Y | 개인정보 수집·이용 동의 여부. 반드시 `true` |
| `damageWaiverAgreed` | boolean | Y | 파손 면책 동의 여부. 필요한 카테고리에서는 `true`, 필요 없는 카테고리에서는 `false` |
- Response `data` 예시:
```json
{
"applicationNo": "A-1002609300001",
"status": "RECEIVED",
"statusName": "접수 완료",
"priceCan": 80,
"trackingDeadlineAt": "2026-10-02T03:00:00Z"
}
```
Response `data` 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
| `applicationNo` | string | 선물 신청번호 |
| `status` | string | 신청 상태 코드 |
| `statusName` | string | 신청 상태 표시명 |
| `priceCan` | number | 실제 차감된 캔 금액 |
| `trackingDeadlineAt` | string | 운송장 등록 기한. UTC ISO 문자열 |
성공 처리:
- 신청 성공 시 기존 앱의 완료 dialog 또는 완료 화면 패턴을 사용한다.
- 최소 표시값은 `applicationNo`, `statusName`, `priceCan`, `trackingDeadlineAt`이다.
- 성공 이후 이동 CTA가 필요하면 기존 선물함 또는 선물 상세 이동 패턴을 사용한다.
실패 처리:
- 서버 오류 메시지가 있으면 기존 앱 에러 노출 방식으로 표시한다.
- 잔액 부족, 유효하지 않은 카테고리, 파손 면책 미동의, 약관 미동의 등의 오류는 서버 메시지를 우선 사용한다.
- 실패 시 입력값은 유지한다.
테스트/검증:
- 화면 진입 시 `GET /api/v2/gifts/form-options`가 호출되는지 확인한다.
- 전달받은 크리에이터 닉네임이 받는 크리에이터 영역에 표시되는지 확인한다.
- 전달받은 memberId가 `POST /api/v2/gifts`의 `recipientMemberId`로 들어가는지 확인한다.
- 빈 상태에서는 CTA가 비활성화되는지 확인한다.
- 필수 입력과 필수 동의를 모두 완료하면 CTA가 활성화되고 `{salePriceCan}캔으로 선물 보내기`가 표시되는지 확인한다.
- `requiresDamageWaiver=true` 카테고리 선택 시 파손 면책 동의 row가 표시되고 동의 전에는 CTA가 비활성인지 확인한다.
- `requiresDamageWaiver=false` 카테고리 선택 시 파손 면책 동의 row가 숨겨지고 request의 `damageWaiverAgreed`가 false인지 확인한다.
- submit 성공 시 요청 body와 성공 처리 값을 확인한다.
- submit 실패 시 입력값이 유지되고 기존 앱 방식으로 오류가 표시되는지 확인한다.
```
---
## API 요약
| 목적 | Method | URL | Request | Response data |
|---|---|---|---|---|
| 폼 옵션 조회 | GET | `/api/v2/gifts/form-options` | 없음 | 사이즈 목록, 카테고리 목록 |
| 선물 신청 접수 | POST | `/api/v2/gifts` | 선물 신청 JSON body | 신청번호, 상태, 차감 금액, 운송장 등록 기한 |
## Figma 반영 메모
- 빈 상태 Figma는 카테고리 미선택, 보내는 사람 정보 미입력, 약관 미동의, 하단 CTA 비활성 상태다.
- 활성 상태 Figma는 크리에이터 닉네임 표시, 카테고리 선택, 파손 면책 동의 노출, 보내는 사람 정보 입력, 약관 동의, 하단 CTA 활성 상태다.
- Figma의 하단 버튼 텍스트는 캔 금액이 포함된 형태이므로 API의 `salePriceCan`으로 동적으로 표시한다.
- Figma의 select, checkbox, radio, bottom action bar, safe area는 각 플랫폼의 기존 컴포넌트로 대응한다.