Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb822a096 | ||
|
|
322b07d794 | ||
|
|
8a95dc171d | ||
|
|
33b6662cb4 | ||
|
|
f66c89fb7f |
@@ -1,11 +1,12 @@
|
|||||||
# 선물 관리자 페이지 구현 프롬프트
|
# 선물 관리자 페이지 구현 프롬프트
|
||||||
|
|
||||||
이 문서는 관리자 페이지의 선물 관련 3개 화면을 프론트엔드 구현 에이전트에게 전달하기 위한 프롬프트 모음이다.
|
이 문서는 관리자 페이지의 선물 관련 4개 화면을 프론트엔드 구현 에이전트에게 전달하기 위한 프롬프트 모음이다.
|
||||||
|
|
||||||
공통 전제:
|
공통 전제:
|
||||||
|
|
||||||
- 관리자 메뉴에는 parent 메뉴 `선물함 관리` 아래에 다음 3개 하위 메뉴가 있다.
|
- 관리자 메뉴에는 parent 메뉴 `선물함 관리` 아래에 다음 4개 하위 메뉴가 있다.
|
||||||
- `선물함 리스트` → `/gift/list`
|
- `선물함 리스트` → `/gift/list`
|
||||||
|
- `받을 주소` → `/gift/mailbox`
|
||||||
- `선물 카테고리` → `/gift/category`
|
- `선물 카테고리` → `/gift/category`
|
||||||
- `선물 사이즈` → `/gift/size`
|
- `선물 사이즈` → `/gift/size`
|
||||||
- 모든 API 응답은 기존 공통 envelope를 사용한다. 화면에서는 `response.data`를 실제 payload로 사용한다.
|
- 모든 API 응답은 기존 공통 envelope를 사용한다. 화면에서는 `response.data`를 실제 payload로 사용한다.
|
||||||
@@ -237,7 +238,84 @@ type AdminGiftOperationStatusResponse = {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 선물 카테고리 CRUD 페이지 프롬프트
|
## 2. 받을 주소 설정 페이지 프롬프트
|
||||||
|
|
||||||
|
```text
|
||||||
|
관리자 선물 받을 주소 설정 페이지를 구현해줘.
|
||||||
|
|
||||||
|
목표:
|
||||||
|
- 관리자 메뉴 `선물함 관리 > 받을 주소`에서 팬이 선물을 보낼 전역 단일 주소를 조회하고 저장한다.
|
||||||
|
- 이 주소는 팬의 선물 상세 API에서 운송장 등록 전 상태일 때 `mailbox`로 노출된다.
|
||||||
|
- 기존 관리자 페이지의 폼, 저장 버튼, 토스트, 에러 표시 패턴을 그대로 따른다.
|
||||||
|
|
||||||
|
라우트:
|
||||||
|
- `/gift/mailbox`
|
||||||
|
|
||||||
|
필수 화면 구성:
|
||||||
|
- 상단 제목: `받을 주소`
|
||||||
|
- 설명 문구: `팬이 선물을 발송할 때 확인하는 받을 주소입니다.`
|
||||||
|
- 입력 폼:
|
||||||
|
- 받을 사람 이름 `name`
|
||||||
|
- 연락처 `phoneNumber`
|
||||||
|
- 우편번호 `zipCode`
|
||||||
|
- 주소 `address`
|
||||||
|
- 상세주소 `addressDetail`
|
||||||
|
- 저장 버튼: `저장`
|
||||||
|
|
||||||
|
초기 조회 API:
|
||||||
|
- Method: `GET`
|
||||||
|
- URL: `/api/v2/admin/gift-mailbox`
|
||||||
|
- response `data`:
|
||||||
|
```ts
|
||||||
|
type AdminGiftMailboxResponse = {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
} | null;
|
||||||
|
```
|
||||||
|
- `data`가 null이면 빈 폼을 표시한다.
|
||||||
|
|
||||||
|
저장 API:
|
||||||
|
- Method: `PUT`
|
||||||
|
- URL: `/api/v2/admin/gift-mailbox`
|
||||||
|
- Request:
|
||||||
|
```ts
|
||||||
|
type AdminGiftMailboxRequest = {
|
||||||
|
name: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
zipCode: string;
|
||||||
|
address: string;
|
||||||
|
addressDetail: string | null;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- Response `data`:
|
||||||
|
```ts
|
||||||
|
type AdminGiftMailboxResponse = {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `name`, `phoneNumber`, `zipCode`, `address`는 필수다.
|
||||||
|
- 공백만 입력할 수 없다.
|
||||||
|
- 저장 실패 시 기존 관리자 페이지의 공통 에러 표시 방식을 따른다.
|
||||||
|
|
||||||
|
저장 성공 UX:
|
||||||
|
- 성공 토스트를 표시한다.
|
||||||
|
- 저장 API response 기준으로 화면 값을 갱신한다.
|
||||||
|
- `address`는 서버가 조립한 `(우편번호) 주소, 상세주소` 형식으로 표시해도 되고, 입력 필드는 사용자가 입력한 값을 유지해도 된다.
|
||||||
|
|
||||||
|
테스트/검증:
|
||||||
|
- 초기 조회 시 null이면 빈 폼을 표시하는지 확인한다.
|
||||||
|
- 저장 시 PUT body가 정확히 전달되는지 확인한다.
|
||||||
|
- 필수값 공백 validation을 확인한다.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 선물 카테고리 CRUD 페이지 프롬프트
|
||||||
|
|
||||||
```text
|
```text
|
||||||
관리자 선물 카테고리 CRUD 페이지를 구현해줘.
|
관리자 선물 카테고리 CRUD 페이지를 구현해줘.
|
||||||
@@ -345,7 +423,7 @@ UX 요구사항:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 선물 사이즈 가격 CRUD 페이지 프롬프트
|
## 4. 선물 사이즈 가격 CRUD 페이지 프롬프트
|
||||||
|
|
||||||
```text
|
```text
|
||||||
관리자 선물 사이즈 가격 CRUD 페이지를 구현해줘.
|
관리자 선물 사이즈 가격 CRUD 페이지를 구현해줘.
|
||||||
@@ -464,7 +542,8 @@ type GiftSize = "SMALL" | "MEDIUM" | "LARGE";
|
|||||||
|
|
||||||
1. API client 함수와 TypeScript 타입을 먼저 추가한다.
|
1. API client 함수와 TypeScript 타입을 먼저 추가한다.
|
||||||
2. `/gift/list` 선물함 리스트와 상세/상태변경 모달을 구현한다.
|
2. `/gift/list` 선물함 리스트와 상세/상태변경 모달을 구현한다.
|
||||||
3. `/gift/category` 카테고리 CRUD를 구현한다.
|
3. `/gift/mailbox` 받을 주소 설정 화면을 구현한다.
|
||||||
4. `/gift/size` 사이즈 가격 수정 화면을 구현한다.
|
4. `/gift/category` 카테고리 CRUD를 구현한다.
|
||||||
5. 기존 관리자 메뉴의 `선물함 관리` 하위 route와 연결되는지 확인한다.
|
5. `/gift/size` 사이즈 가격 수정 화면을 구현한다.
|
||||||
6. 각 페이지별 loading/empty/error/success 상태를 확인한다.
|
6. 기존 관리자 메뉴의 `선물함 관리` 하위 route와 연결되는지 확인한다.
|
||||||
|
7. 각 페이지별 loading/empty/error/success 상태를 확인한다.
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ CREATE TABLE gift_category (
|
|||||||
UNIQUE KEY uk_gift_category_receipt_code (receipt_code)
|
UNIQUE KEY uk_gift_category_receipt_code (receipt_code)
|
||||||
) COMMENT='선물 카테고리';
|
) COMMENT='선물 카테고리';
|
||||||
|
|
||||||
|
CREATE TABLE gift_mailbox (
|
||||||
|
id BIGINT NOT NULL COMMENT '전역 단일 설정 ID. 항상 1',
|
||||||
|
name VARCHAR(50) NOT NULL COMMENT '받을 사람 이름',
|
||||||
|
phone_number VARCHAR(30) NOT NULL COMMENT '받을 연락처',
|
||||||
|
zip_code VARCHAR(20) NOT NULL COMMENT '받을 우편번호',
|
||||||
|
address VARCHAR(255) NOT NULL COMMENT '받을 주소',
|
||||||
|
address_detail VARCHAR(255) NULL COMMENT '받을 상세주소',
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) COMMENT='선물 받을 주소 전역 설정';
|
||||||
|
|
||||||
CREATE TABLE gift (
|
CREATE TABLE gift (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
|
||||||
application_no VARCHAR(40) NOT NULL COMMENT '신청번호',
|
application_no VARCHAR(40) NOT NULL COMMENT '신청번호',
|
||||||
|
|||||||
@@ -1309,6 +1309,48 @@ P7은 FCM 확장 후 전체 상태 전이를 연결하고 최종 회귀로 종
|
|||||||
- Gate 확인: Phase 9 focused suite, `./gradlew ktlintCheck`, `git diff --check` 모두 성공
|
- Gate 확인: Phase 9 focused suite, `./gradlew ktlintCheck`, `git diff --check` 모두 성공
|
||||||
- 남은 항목: 없음
|
- 남은 항목: 없음
|
||||||
|
|
||||||
|
## Phase 11: 선물 받을 주소 설정
|
||||||
|
|
||||||
|
**Phase 결과:** 관리자는 전역 단일 선물 받을 주소를 등록/수정하고, 팬은 운송장 등록 전 상세에서 해당 주소를 확인할 수 있다.
|
||||||
|
|
||||||
|
**선행조건:** `P3-GATE`, `P10-GATE` 완료.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
#### Task 11.1 받을 주소 관리자 설정 API 구현
|
||||||
|
|
||||||
|
**Goal 실행 `P11-T1`:** 전역 단일 받을 주소 조회/저장 API를 구현한다.
|
||||||
|
|
||||||
|
- **Files:** Create `GiftMailbox.kt`, `GiftMailboxRepository.kt`; Modify `GiftAdminService.kt`, `AdminGiftController.kt`, `AdminGiftRequest.kt`, `AdminGiftResponse.kt`, `gift-schema.sql`; Test `GiftAdminServiceTest.kt`, `AdminGiftControllerTest.kt`.
|
||||||
|
- [x] **RED:** 설정 미등록 시 `GET /api/v2/admin/gift-mailbox`가 `data=null`을 반환하는 실패 test를 작성한다.
|
||||||
|
- [x] **RED:** `PUT /api/v2/admin/gift-mailbox`가 전역 단일 주소를 생성하고, 재호출 시 같은 row를 갱신하는 실패 test를 작성한다.
|
||||||
|
- [x] **RED:** 이름/연락처/우편번호/주소 공백 입력을 거부하는 실패 test를 작성한다.
|
||||||
|
- [x] **RED 확인:** focused test 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** 단일 row persistence, 관리자 service, request/response DTO, controller endpoint를 구현한다.
|
||||||
|
- [x] **GREEN 확인:** focused test 통과를 확인한다.
|
||||||
|
|
||||||
|
#### Task 11.2 선물 상세 받을 주소 노출 구현
|
||||||
|
|
||||||
|
**Goal 실행 `P11-T2`:** 보낸 선물의 운송장 등록 전 상세에만 받을 주소를 노출한다.
|
||||||
|
|
||||||
|
- **Files:** Modify `GiftQueryService.kt`; Test `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`.
|
||||||
|
- [x] **RED:** `direction=SENT`이고 `status=RECEIVED`인 상세만 `mailbox`를 반환하는 실패 test를 작성한다.
|
||||||
|
- [x] **RED:** 받을 주소 미등록 또는 다른 상태/방향이면 `mailbox=null`인 실패 test를 작성한다.
|
||||||
|
- [x] **RED 확인:** focused test 실패를 확인한다.
|
||||||
|
- [x] **GREEN:** `GiftQueryService`에서 받을 주소 repository를 조회해 기존 `GiftMailboxResult`로 매핑한다.
|
||||||
|
- [x] **GREEN 확인:** focused test 통과를 확인한다.
|
||||||
|
|
||||||
|
### Phase 11 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P11-GATE`:** 관리자 설정과 사용자 상세 노출을 판정한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests "*GiftAdminServiceTest" --tests "*AdminGiftControllerTest" --tests "*GiftQueryServiceTest" --tests "*GiftControllerTest"
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 받을 주소 미등록/등록/수정, 상세 `mailbox` 노출 조건, 기존 선물 상세 권한 정책이 통과한다.
|
||||||
|
|
||||||
## Decision Log
|
## Decision Log
|
||||||
|
|
||||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|||||||
@@ -384,8 +384,8 @@ Response:
|
|||||||
{
|
{
|
||||||
"applicationNo": "A-1002609300001",
|
"applicationNo": "A-1002609300001",
|
||||||
"direction": "SENT",
|
"direction": "SENT",
|
||||||
"status": "TRACKING_REGISTERED",
|
"status": "RECEIVED",
|
||||||
"statusName": "발송 확인",
|
"statusName": "접수 완료",
|
||||||
"giftInfo": {
|
"giftInfo": {
|
||||||
"recipientCreatorNickname": "크리에이터닉네임",
|
"recipientCreatorNickname": "크리에이터닉네임",
|
||||||
"senderNickname": null,
|
"senderNickname": null,
|
||||||
@@ -393,7 +393,7 @@ Response:
|
|||||||
"categoryName": "인형",
|
"categoryName": "인형",
|
||||||
"applicationNo": "A-1002609300001",
|
"applicationNo": "A-1002609300001",
|
||||||
"paidCan": 80,
|
"paidCan": 80,
|
||||||
"tracking": "CJ대한통운_1234567890",
|
"tracking": null,
|
||||||
"shippingRequestedAt": null
|
"shippingRequestedAt": null
|
||||||
},
|
},
|
||||||
"senderInfo": {
|
"senderInfo": {
|
||||||
@@ -403,11 +403,11 @@ Response:
|
|||||||
},
|
},
|
||||||
"recipientAddress": null,
|
"recipientAddress": null,
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"name": "소다라이브 사서함",
|
"name": "소다라이브 선물 담당자",
|
||||||
"address": "구현 후 제공",
|
"address": "(06234) 서울시 강남구 ..., 3층 선물 접수처",
|
||||||
"phoneNumber": "구현 후 제공"
|
"phoneNumber": "01012345678"
|
||||||
},
|
},
|
||||||
"trackingRequired": false,
|
"trackingRequired": true,
|
||||||
"recipientAddressRequired": false,
|
"recipientAddressRequired": false,
|
||||||
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z",
|
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z",
|
||||||
"delivery": {
|
"delivery": {
|
||||||
@@ -446,6 +446,8 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`mailbox`는 관리자가 등록한 전역 단일 받을 주소다. 보낸 사람 관점(`direction=SENT`)이고 상태가 운송장 등록 전인 `RECEIVED`일 때만 내려준다. 받을 주소가 등록되지 않았거나 다른 상태/방향이면 `mailbox`는 `null`이다. 주소는 상세 조회 시점의 최신 관리자 설정을 사용한다.
|
||||||
|
|
||||||
받는 크리에이터 관점 응답 예시는 다음과 같다.
|
받는 크리에이터 관점 응답 예시는 다음과 같다.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -802,6 +804,40 @@ Response:
|
|||||||
|
|
||||||
크리에이터가 배송지를 아직 입력하지 않은 경우에도 `recipientInfo.nickname`은 내려주고, `recipientInfo.name`, `recipientInfo.phoneNumber`, `recipientInfo.address`는 빈 문자열 `""`로 내려준다.
|
크리에이터가 배송지를 아직 입력하지 않은 경우에도 `recipientInfo.nickname`은 내려주고, `recipientInfo.name`, `recipientInfo.phoneNumber`, `recipientInfo.address`는 빈 문자열 `""`로 내려준다.
|
||||||
|
|
||||||
|
#### 8.13.3 관리자 선물 받을 주소 설정 API
|
||||||
|
|
||||||
|
선물 받을 주소는 전역 단일 설정이다. 팬이 운송장을 등록하기 전에 보낸 선물 상세에서 이 주소를 확인하고 택배를 발송한다.
|
||||||
|
|
||||||
|
`GET /api/v2/admin/gift-mailbox`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "소다라이브 선물 담당자",
|
||||||
|
"address": "(06234) 서울시 강남구 ..., 3층 선물 접수처",
|
||||||
|
"phoneNumber": "01012345678"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
설정이 없으면 공통 envelope의 `data`는 `null`이다.
|
||||||
|
|
||||||
|
`PUT /api/v2/admin/gift-mailbox`
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "소다라이브 선물 담당자",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"zipCode": "06234",
|
||||||
|
"address": "서울시 강남구 ...",
|
||||||
|
"addressDetail": "3층 선물 접수처"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내려준다. `address`는 기존 주소 응답과 동일하게 `(우편번호) 주소, 상세주소` 형식이다. `name`, `phoneNumber`, `zipCode`, `address`는 필수이며 공백만 입력할 수 없다. `addressDetail`은 선택이다.
|
||||||
|
|
||||||
`availableActions`는 현재 상태별로 다음 값만 내려준다.
|
`availableActions`는 현재 상태별로 다음 값만 내려준다.
|
||||||
|
|
||||||
| 현재 상태 | availableActions |
|
| 현재 상태 | availableActions |
|
||||||
|
|||||||
+10
@@ -5,6 +5,8 @@ import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryRequest
|
|||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryResponse
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftDetailResponse
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftDetailResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftListResponse
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftListResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMailboxRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMailboxResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMarkUndeliverableRequest
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMarkUndeliverableRequest
|
||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftOperationStatusResponse
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftOperationStatusResponse
|
||||||
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceRequest
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceRequest
|
||||||
@@ -45,6 +47,14 @@ class AdminGiftController(
|
|||||||
AdminGiftDetailResponse.from(queryService.getGiftDetail(applicationNo))
|
AdminGiftDetailResponse.from(queryService.getGiftDetail(applicationNo))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@GetMapping("/gift-mailbox")
|
||||||
|
fun getMailbox() = ApiResponse.ok(service.getMailbox()?.let { AdminGiftMailboxResponse.from(it) })
|
||||||
|
|
||||||
|
@PutMapping("/gift-mailbox")
|
||||||
|
fun updateMailbox(@RequestBody request: AdminGiftMailboxRequest) = ApiResponse.ok(
|
||||||
|
AdminGiftMailboxResponse.from(service.updateMailbox(request.toCommand()))
|
||||||
|
)
|
||||||
|
|
||||||
@GetMapping("/gift-categories")
|
@GetMapping("/gift-categories")
|
||||||
fun getCategories() = ApiResponse.ok(service.getCategories().map { AdminGiftCategoryResponse.from(it) })
|
fun getCategories() = ApiResponse.ok(service.getCategories().map { AdminGiftCategoryResponse.from(it) })
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.admin.gift.dto
|
package kr.co.vividnext.sodalive.v2.api.admin.gift.dto
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.GiftCategoryCommand
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftCategoryCommand
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftMailboxCommand
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceCommand
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceCommand
|
||||||
|
|
||||||
data class AdminGiftCategoryRequest(
|
data class AdminGiftCategoryRequest(
|
||||||
@@ -35,6 +36,22 @@ data class AdminGiftSizePriceRequest(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AdminGiftMailboxRequest(
|
||||||
|
val name: String,
|
||||||
|
val phoneNumber: String,
|
||||||
|
val zipCode: String,
|
||||||
|
val address: String,
|
||||||
|
val addressDetail: String?
|
||||||
|
) {
|
||||||
|
fun toCommand() = GiftMailboxCommand(
|
||||||
|
name = name,
|
||||||
|
phoneNumber = phoneNumber,
|
||||||
|
zipCode = zipCode,
|
||||||
|
address = address,
|
||||||
|
addressDetail = addressDetail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class AdminGiftMarkUndeliverableRequest(
|
data class AdminGiftMarkUndeliverableRequest(
|
||||||
val reason: String
|
val reason: String
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftListResult
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftMemberInfoResult
|
import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftMemberInfoResult
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftProductInfoResult
|
import kr.co.vividnext.sodalive.v2.gift.application.AdminGiftProductInfoResult
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.GiftCategoryResult
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftCategoryResult
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftMailboxResult
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.GiftOperationStatusResult
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftOperationStatusResult
|
||||||
import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceResult
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceResult
|
||||||
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
|
||||||
@@ -70,6 +71,20 @@ data class AdminGiftOperationStatusResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AdminGiftMailboxResponse(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phoneNumber: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(result: GiftMailboxResult) = AdminGiftMailboxResponse(
|
||||||
|
name = result.name,
|
||||||
|
address = result.address,
|
||||||
|
phoneNumber = result.phoneNumber
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class AdminGiftListResponse(
|
data class AdminGiftListResponse(
|
||||||
val totalCount: Long,
|
val totalCount: Long,
|
||||||
val items: List<AdminGiftListItemResponse>,
|
val items: List<AdminGiftListItemResponse>,
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence
|
||||||
|
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.Id
|
||||||
|
import javax.persistence.Table
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "gift_mailbox")
|
||||||
|
class GiftMailbox(
|
||||||
|
@Id
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
val id: Long = SINGLETON_ID,
|
||||||
|
|
||||||
|
@Column(name = "name", nullable = false, length = 50)
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@Column(name = "phone_number", nullable = false, length = 30)
|
||||||
|
var phoneNumber: String,
|
||||||
|
|
||||||
|
@Column(name = "zip_code", nullable = false, length = 20)
|
||||||
|
var zipCode: String,
|
||||||
|
|
||||||
|
@Column(name = "address", nullable = false, length = 255)
|
||||||
|
var address: String,
|
||||||
|
|
||||||
|
@Column(name = "address_detail", length = 255)
|
||||||
|
var addressDetail: String?
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val SINGLETON_ID = 1L
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface GiftMailboxRepository : JpaRepository<GiftMailbox, Long>
|
||||||
@@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.SodaException
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailbox
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailboxRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
@@ -41,6 +43,14 @@ data class GiftSizePriceCommand(
|
|||||||
val isActive: Boolean
|
val isActive: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class GiftMailboxCommand(
|
||||||
|
val name: String,
|
||||||
|
val phoneNumber: String,
|
||||||
|
val zipCode: String,
|
||||||
|
val address: String,
|
||||||
|
val addressDetail: String?
|
||||||
|
)
|
||||||
|
|
||||||
data class GiftSizePriceResult(
|
data class GiftSizePriceResult(
|
||||||
val sizeCode: GiftSize,
|
val sizeCode: GiftSize,
|
||||||
val sizeName: String,
|
val sizeName: String,
|
||||||
@@ -62,8 +72,31 @@ class GiftAdminService(
|
|||||||
private val sizePriceRepository: GiftSizePriceRepository,
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
private val giftRepository: GiftRepository,
|
private val giftRepository: GiftRepository,
|
||||||
private val giftDeliveryRepository: GiftDeliveryRepository,
|
private val giftDeliveryRepository: GiftDeliveryRepository,
|
||||||
|
private val giftMailboxRepository: GiftMailboxRepository,
|
||||||
private val giftPushService: GiftPushService
|
private val giftPushService: GiftPushService
|
||||||
) {
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getMailbox(): GiftMailboxResult? = giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)?.toResult()
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun updateMailbox(command: GiftMailboxCommand): GiftMailboxResult {
|
||||||
|
validateMailbox(command)
|
||||||
|
val mailbox = giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)
|
||||||
|
?: GiftMailbox(
|
||||||
|
name = command.name,
|
||||||
|
phoneNumber = command.phoneNumber,
|
||||||
|
zipCode = command.zipCode,
|
||||||
|
address = command.address,
|
||||||
|
addressDetail = command.addressDetail?.takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
mailbox.name = command.name
|
||||||
|
mailbox.phoneNumber = command.phoneNumber
|
||||||
|
mailbox.zipCode = command.zipCode
|
||||||
|
mailbox.address = command.address
|
||||||
|
mailbox.addressDetail = command.addressDetail?.takeIf { it.isNotBlank() }
|
||||||
|
return giftMailboxRepository.save(mailbox).toResult()
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getCategories(): List<GiftCategoryResult> = categoryRepository.findAll().map { it.toResult() }
|
fun getCategories(): List<GiftCategoryResult> = categoryRepository.findAll().map { it.toResult() }
|
||||||
|
|
||||||
@@ -198,6 +231,22 @@ class GiftAdminService(
|
|||||||
if (command.salePriceCan <= 0 || command.salePriceCan > command.basePriceCan) throw invalidRequest()
|
if (command.salePriceCan <= 0 || command.salePriceCan > command.basePriceCan) throw invalidRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateMailbox(command: GiftMailboxCommand) {
|
||||||
|
if (
|
||||||
|
command.name.isBlank() ||
|
||||||
|
command.name.length > 50 ||
|
||||||
|
command.phoneNumber.isBlank() ||
|
||||||
|
command.phoneNumber.length > 30 ||
|
||||||
|
command.zipCode.isBlank() ||
|
||||||
|
command.zipCode.length > 20 ||
|
||||||
|
command.address.isBlank() ||
|
||||||
|
command.address.length > 255 ||
|
||||||
|
(command.addressDetail?.length ?: 0) > 255
|
||||||
|
) {
|
||||||
|
throw invalidRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun GiftCategory.toResult() = GiftCategoryResult(
|
private fun GiftCategory.toResult() = GiftCategoryResult(
|
||||||
categoryId = id ?: throw invalidRequest(),
|
categoryId = id ?: throw invalidRequest(),
|
||||||
classificationNumber = classificationNumber,
|
classificationNumber = classificationNumber,
|
||||||
@@ -217,6 +266,15 @@ class GiftAdminService(
|
|||||||
isActive = isActive
|
isActive = isActive
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun GiftMailbox.toResult() = GiftMailboxResult(
|
||||||
|
name = name,
|
||||||
|
address = formatAddress(zipCode, address, addressDetail),
|
||||||
|
phoneNumber = phoneNumber
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun formatAddress(zipCode: String, address: String, detail: String?) =
|
||||||
|
"($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
|
||||||
|
|
||||||
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
|
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailbox
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailboxRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.domain.GiftDirection
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftDirection
|
||||||
@@ -105,6 +107,7 @@ class GiftQueryService(
|
|||||||
private val giftRepository: GiftRepository,
|
private val giftRepository: GiftRepository,
|
||||||
private val deliveryRepository: GiftDeliveryRepository,
|
private val deliveryRepository: GiftDeliveryRepository,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
|
private val giftMailboxRepository: GiftMailboxRepository,
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
@@ -186,7 +189,7 @@ class GiftQueryService(
|
|||||||
),
|
),
|
||||||
senderInfo = delivery.toSenderInfo().takeIf { direction == GiftDirection.SENT },
|
senderInfo = delivery.toSenderInfo().takeIf { direction == GiftDirection.SENT },
|
||||||
recipientAddress = recipientAddress,
|
recipientAddress = recipientAddress,
|
||||||
mailbox = null,
|
mailbox = mailboxFor(direction, gift.status),
|
||||||
trackingRequired = direction == GiftDirection.SENT && gift.status == GiftStatus.RECEIVED,
|
trackingRequired = direction == GiftDirection.SENT && gift.status == GiftStatus.RECEIVED,
|
||||||
recipientAddressRequired = direction == GiftDirection.RECEIVED && recipientAddress == null &&
|
recipientAddressRequired = direction == GiftDirection.RECEIVED && recipientAddress == null &&
|
||||||
gift.status !in TERMINAL_STATUSES,
|
gift.status !in TERMINAL_STATUSES,
|
||||||
@@ -247,6 +250,17 @@ class GiftQueryService(
|
|||||||
return GiftAddressResult(name, phoneNumber, formatAddress(zipCode, address, recipientAddressDetail))
|
return GiftAddressResult(name, phoneNumber, formatAddress(zipCode, address, recipientAddressDetail))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mailboxFor(direction: GiftDirection, status: GiftStatus): GiftMailboxResult? {
|
||||||
|
if (direction != GiftDirection.SENT || status != GiftStatus.RECEIVED) return null
|
||||||
|
return giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)?.let {
|
||||||
|
GiftMailboxResult(
|
||||||
|
name = it.name,
|
||||||
|
address = formatAddress(it.zipCode, it.address, it.addressDetail),
|
||||||
|
phoneNumber = it.phoneNumber
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun GiftDelivery.tracking() = if (courierCompanyName != null && trackingNumber != null) {
|
private fun GiftDelivery.tracking() = if (courierCompanyName != null && trackingNumber != null) {
|
||||||
"${courierCompanyName}_$trackingNumber"
|
"${courierCompanyName}_$trackingNumber"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+48
@@ -59,6 +59,54 @@ class AdminGiftControllerTest @Autowired constructor(
|
|||||||
categoryRepository.deleteAll()
|
categoryRepository.deleteAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 받을 주소를 조회하고 저장한다")
|
||||||
|
fun shouldManageGiftMailbox() {
|
||||||
|
mockMvc.perform(get("/api/v2/admin/gift-mailbox").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data").isEmpty)
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/api/v2/admin/gift-mailbox")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"name":"소다라이브 선물 담당자",
|
||||||
|
"phoneNumber":"01012345678",
|
||||||
|
"zipCode":"06234",
|
||||||
|
"address":"서울시 강남구",
|
||||||
|
"addressDetail":"3층 선물 접수처"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.name").value("소다라이브 선물 담당자"))
|
||||||
|
.andExpect(jsonPath("$.data.phoneNumber").value("01012345678"))
|
||||||
|
.andExpect(jsonPath("$.data.address").value("(06234) 서울시 강남구, 3층 선물 접수처"))
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/v2/admin/gift-mailbox").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data.name").value("소다라이브 선물 담당자"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 공백 받을 주소를 저장할 수 없다")
|
||||||
|
fun shouldRejectInvalidGiftMailbox() {
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/api/v2/admin/gift-mailbox")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""{"name":" ","phoneNumber":"01012345678","zipCode":"06234","address":"서울시 강남구"}""")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("관리자는 전체 선물함 목록을 필터링해 조회한다")
|
@DisplayName("관리자는 전체 선물함 목록을 필터링해 조회한다")
|
||||||
fun shouldListAdminGifts() {
|
fun shouldListAdminGifts() {
|
||||||
|
|||||||
+45
@@ -14,6 +14,8 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailbox
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailboxRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
@@ -60,6 +62,7 @@ class GiftControllerTest @Autowired constructor(
|
|||||||
private val giftDeliveryRepository: GiftDeliveryRepository,
|
private val giftDeliveryRepository: GiftDeliveryRepository,
|
||||||
private val sizePriceRepository: GiftSizePriceRepository,
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
|
private val giftMailboxRepository: GiftMailboxRepository,
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
) {
|
) {
|
||||||
@MockBean
|
@MockBean
|
||||||
@@ -190,6 +193,48 @@ class GiftControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.statusTimeline[1].occurredAt").value("2026-09-30T12:00:00Z"))
|
.andExpect(jsonPath("$.data.statusTimeline[1].occurredAt").value("2026-09-30T12:00:00Z"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("발신자는 운송장 등록 전 선물 상세에서 받을 주소를 조회한다")
|
||||||
|
fun shouldGetMailboxForSentReceivedGiftDetail() {
|
||||||
|
val sender = memberRepository.save(Member(password = "password", nickname = "fan"))
|
||||||
|
val recipient = memberRepository.save(Member(password = "password", nickname = "creator"))
|
||||||
|
val gift = saveGift(
|
||||||
|
applicationNo = "G-CONTROLLER-MAILBOX",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
giftDeliveryRepository.save(
|
||||||
|
GiftDelivery(
|
||||||
|
giftId = gift.id!!,
|
||||||
|
senderName = "홍길동",
|
||||||
|
senderPhoneNumber = "01012345678",
|
||||||
|
senderZipCode = "06234",
|
||||||
|
senderAddress = "서울시 강남구",
|
||||||
|
trackingDeadlineAt = LocalDateTime.of(2026, 10, 2, 12, 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
giftMailboxRepository.save(
|
||||||
|
GiftMailbox(
|
||||||
|
name = "소다라이브 선물 담당자",
|
||||||
|
phoneNumber = "01012345678",
|
||||||
|
zipCode = "06234",
|
||||||
|
address = "서울시 강남구",
|
||||||
|
addressDetail = "3층 선물 접수처"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
get("/api/v2/gifts/{applicationNo}", gift.applicationNo)
|
||||||
|
.with(user(MemberAdapter(sender)))
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.mailbox.name").value("소다라이브 선물 담당자"))
|
||||||
|
.andExpect(jsonPath("$.data.mailbox.phoneNumber").value("01012345678"))
|
||||||
|
.andExpect(jsonPath("$.data.mailbox.address").value("(06234) 서울시 강남구, 3층 선물 접수처"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("팬은 POST /api/v2/gifts로 선물을 신청하고 접수 결과를 받는다")
|
@DisplayName("팬은 POST /api/v2/gifts로 선물을 신청하고 접수 결과를 받는다")
|
||||||
fun shouldRegisterGift() {
|
fun shouldRegisterGift() {
|
||||||
|
|||||||
+55
-1
@@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailboxRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
|
||||||
@@ -33,7 +34,8 @@ class GiftAdminServiceTest @Autowired constructor(
|
|||||||
private val categoryRepository: GiftCategoryRepository,
|
private val categoryRepository: GiftCategoryRepository,
|
||||||
private val sizePriceRepository: GiftSizePriceRepository,
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
private val giftRepository: GiftRepository,
|
private val giftRepository: GiftRepository,
|
||||||
private val giftDeliveryRepository: GiftDeliveryRepository
|
private val giftDeliveryRepository: GiftDeliveryRepository,
|
||||||
|
private val giftMailboxRepository: GiftMailboxRepository
|
||||||
) {
|
) {
|
||||||
private val giftPushService = Mockito.mock(GiftPushService::class.java)
|
private val giftPushService = Mockito.mock(GiftPushService::class.java)
|
||||||
private val service = GiftAdminService(
|
private val service = GiftAdminService(
|
||||||
@@ -41,9 +43,61 @@ class GiftAdminServiceTest @Autowired constructor(
|
|||||||
sizePriceRepository,
|
sizePriceRepository,
|
||||||
giftRepository,
|
giftRepository,
|
||||||
giftDeliveryRepository,
|
giftDeliveryRepository,
|
||||||
|
giftMailboxRepository,
|
||||||
giftPushService
|
giftPushService
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("받을 주소는 미등록이면 null이고 저장 후 단일 설정으로 갱신된다")
|
||||||
|
fun shouldUpsertGiftMailbox() {
|
||||||
|
assertEquals(null, service.getMailbox())
|
||||||
|
|
||||||
|
val created = service.updateMailbox(
|
||||||
|
GiftMailboxCommand(
|
||||||
|
name = "소다라이브 선물 담당자",
|
||||||
|
phoneNumber = "01012345678",
|
||||||
|
zipCode = "06234",
|
||||||
|
address = "서울시 강남구",
|
||||||
|
addressDetail = "3층 선물 접수처"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val updated = service.updateMailbox(
|
||||||
|
GiftMailboxCommand(
|
||||||
|
name = "소다라이브 운영팀",
|
||||||
|
phoneNumber = "01087654321",
|
||||||
|
zipCode = "04524",
|
||||||
|
address = "서울시 중구",
|
||||||
|
addressDetail = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("소다라이브 선물 담당자", created.name)
|
||||||
|
assertEquals("(06234) 서울시 강남구, 3층 선물 접수처", created.address)
|
||||||
|
assertEquals("01012345678", created.phoneNumber)
|
||||||
|
assertEquals("소다라이브 운영팀", updated.name)
|
||||||
|
assertEquals("(04524) 서울시 중구", updated.address)
|
||||||
|
assertEquals(1, giftMailboxRepository.count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("받을 주소 필수값이 공백이면 저장할 수 없다")
|
||||||
|
fun shouldRejectInvalidGiftMailbox() {
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.updateMailbox(
|
||||||
|
GiftMailboxCommand(
|
||||||
|
name = " ",
|
||||||
|
phoneNumber = "01012345678",
|
||||||
|
zipCode = "06234",
|
||||||
|
address = "서울시 강남구",
|
||||||
|
addressDetail = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||||
|
assertEquals(0, giftMailboxRepository.count())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("카테고리를 등록, 수정, 논리삭제한다")
|
@DisplayName("카테고리를 등록, 수정, 논리삭제한다")
|
||||||
fun shouldCreateUpdateAndDeactivateCategory() {
|
fun shouldCreateUpdateAndDeactivateCategory() {
|
||||||
|
|||||||
+64
-1
@@ -9,6 +9,8 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
|||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailbox
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftMailboxRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
@@ -39,7 +41,8 @@ class GiftQueryServiceTest @Autowired constructor(
|
|||||||
private val sizePriceRepository: GiftSizePriceRepository,
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
private val giftRepository: GiftRepository,
|
private val giftRepository: GiftRepository,
|
||||||
private val deliveryRepository: GiftDeliveryRepository,
|
private val deliveryRepository: GiftDeliveryRepository,
|
||||||
private val memberRepository: MemberRepository
|
private val memberRepository: MemberRepository,
|
||||||
|
private val giftMailboxRepository: GiftMailboxRepository
|
||||||
) {
|
) {
|
||||||
@Test
|
@Test
|
||||||
fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() {
|
fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() {
|
||||||
@@ -230,6 +233,65 @@ class GiftQueryServiceTest @Autowired constructor(
|
|||||||
assertNull(result.statusTimeline[1].occurredAt)
|
assertNull(result.statusTimeline[1].occurredAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("보낸 선물의 운송장 등록 전 상세만 받을 주소를 반환한다")
|
||||||
|
fun shouldReturnMailboxOnlyForSentReceivedGiftDetail() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val receivedGift = saveGift(
|
||||||
|
applicationNo = "G-MAILBOX-RECEIVED",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
saveDelivery(receivedGift)
|
||||||
|
val trackingGift = saveGift(
|
||||||
|
applicationNo = "G-MAILBOX-TRACKING",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.TRACKING_REGISTERED
|
||||||
|
)
|
||||||
|
saveDelivery(trackingGift).trackingRegisteredAt = NOW
|
||||||
|
giftMailboxRepository.save(
|
||||||
|
GiftMailbox(
|
||||||
|
name = "소다라이브 선물 담당자",
|
||||||
|
phoneNumber = "01012345678",
|
||||||
|
zipCode = "06234",
|
||||||
|
address = "서울시 강남구",
|
||||||
|
addressDetail = "3층 선물 접수처"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val service = queryService()
|
||||||
|
|
||||||
|
val receivedResult = service.getGiftDetail(sender.id!!, receivedGift.applicationNo)
|
||||||
|
val trackingSenderResult = service.getGiftDetail(sender.id!!, trackingGift.applicationNo)
|
||||||
|
val trackingRecipientResult = service.getGiftDetail(recipient.id!!, trackingGift.applicationNo)
|
||||||
|
|
||||||
|
assertEquals("소다라이브 선물 담당자", receivedResult.mailbox?.name)
|
||||||
|
assertEquals("01012345678", receivedResult.mailbox?.phoneNumber)
|
||||||
|
assertEquals("(06234) 서울시 강남구, 3층 선물 접수처", receivedResult.mailbox?.address)
|
||||||
|
assertNull(trackingSenderResult.mailbox)
|
||||||
|
assertNull(trackingRecipientResult.mailbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("받을 주소가 없으면 운송장 등록 전 상세도 mailbox가 null이다")
|
||||||
|
fun shouldReturnNullMailboxWhenMailboxNotConfigured() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val gift = saveGift(
|
||||||
|
applicationNo = "G-MAILBOX-EMPTY",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
saveDelivery(gift)
|
||||||
|
|
||||||
|
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo)
|
||||||
|
|
||||||
|
assertNull(result.mailbox)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("수신자는 노출된 선물 상세에서 자신의 배송지와 발신자 닉네임만 조회한다")
|
@DisplayName("수신자는 노출된 선물 상세에서 자신의 배송지와 발신자 닉네임만 조회한다")
|
||||||
fun shouldReturnReceivedGiftDetailWithoutSenderInformation() {
|
fun shouldReturnReceivedGiftDetailWithoutSenderInformation() {
|
||||||
@@ -450,6 +512,7 @@ class GiftQueryServiceTest @Autowired constructor(
|
|||||||
giftRepository,
|
giftRepository,
|
||||||
deliveryRepository,
|
deliveryRepository,
|
||||||
memberRepository,
|
memberRepository,
|
||||||
|
giftMailboxRepository,
|
||||||
"https://cdn.test"
|
"https://cdn.test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user