97 lines
14 KiB
Markdown
97 lines
14 KiB
Markdown
# 관리자 구현 프롬프트: 가로형 서브 배너
|
|
|
|
이 프롬프트는 관리자 작업에 단독으로 사용할 수 있다. 작업 대상 repository의 실제 stack, 관리자 인증, API client, multipart 전송 및 form/list 패턴을 먼저 확인한다. 특정 framework를 전제하지 않는다. 이 작업은 frontend-only이며 서버, 저장소, schema, migration, 배포 작업으로 확장하지 않는다.
|
|
|
|
**상태:** 관리자 CRUD API 구현·검증 완료, 미배포. 백엔드 전체 테스트·lint·빌드와 독립 코드 리뷰를 통과했다. 화면 배치, 폼 표현, 삭제 확인 등 UI 선택은 작성자 추천이며 계약으로 가장하지 않는다.
|
|
|
|
2026-11-03 언어 후속 계약의 전체 2,928개 테스트·영향 HTTP 재검증·lint·빌드·독립 리뷰를 완료했다. 신규 환경은 `create-sub-banner.sql`, 기존 테이블은 `20261102_add_sub_banner_lang.sql`로 언어 컬럼을 서버 배포 전에 준비해야 한다. 운영 적용은 미실행이며 이 프롬프트의 UI 작업에서 SQL을 실행하지 않는다.
|
|
|
|
## 목표
|
|
|
|
가로형 서브 배너의 목록·생성·수정·삭제 UI를 제공한다. 레코드 하나는 `HOME_RECOMMENDATION` 또는 `CONTENT_RECOMMENDATION` placement 하나에 속한다. 별도 상세 조회는 없으므로 목록 데이터로 수정 form을 채운다.
|
|
|
|
## API 계약
|
|
|
|
아래 계약은 구현되어 있지만 아직 배포되지 않았다. 배포된 target을 확인한 뒤 그 target에만 실제 연결한다. target 오류 후 환경 변경이나 mock fallback을 자동 적용하지 않는다. fixture는 명시적인 UI 검증에서만 사용하며 mock 상태를 서버 성공으로 표시하지 않는다.
|
|
|
|
- `GET /admin/sub-banners?placement={placement}`: 필수 placement query, 삭제되지 않은 활성 배너를 기간·언어와 무관하게 모두 반환한다. 서버 언어 필터를 가정하지 않는다.
|
|
- `POST /admin/sub-banners`: 생성.
|
|
- `PUT /admin/sub-banners/{id}`: 기본 JSON 네 필드 교체. 날짜와 언어는 선택적으로 생략 보존한다. 명시적 null 해제는 두 날짜에만 허용된다.
|
|
- `DELETE /admin/sub-banners/{id}`: 소프트 삭제. 성공 시 목록에서 제거한다. 삭제된 id의 수정·재삭제는 404이며, 응답은 기존 HTTP 200과 `data:null`이다. `isActive`를 직접 보내거나 복원·활성 토글 UI를 추가하지 않는다.
|
|
- 기존 관리자 API client로 `Authorization: Bearer <token>`을 전송하고 `ADMIN` 역할이 필요하다. 익명 요청은 HTTP 401이다. 유효한 비관리자 요청은 현재 정책상 HTTP 200 `success:false`이며 403을 보장하지 않는다.
|
|
- 성공은 HTTP 200 및 네 필드 `ApiResponse<T>`: `success`, `message`, `data`, `errorProperty`. GET `data`는 전체 객체 배열, POST/PUT은 저장된 전체 객체, DELETE는 `null`. 빈 목록 `[]`.
|
|
- 관리자 응답 형식 `AdminSubBanner`는 정확히 9필드다: `id` integer JSON number(정밀도를 보존하는 불투명 식별자), `placement` enum(`HOME_RECOMMENDATION` 또는 `CONTENT_RECOMMENDATION`), `imageUrl` 절대 http/https URL 문자열, `webUrl`·`androidDeepLink`·`iosDeepLink` 각각 string|null, `startDate`·`endDate` 각각 string|null, `lang`은 `KO`·`EN`·`JA` 중 하나다. 모든 날짜는 `Asia/Seoul` 기준 `yyyy-MM-dd HH:mm` 문자열 또는 null이다. 목록, 생성, 수정 모두 이 관리자 shape를 쓴다.
|
|
- 앱은 두 추천 API의 `data.subBanners`를 사용한다. 배열 항목은 날짜·활성 필드가 없는 기존 6필드이며 활성·기간 내 항목에 앱의 웹/현재 OS 링크 조건을 적용한다. 관리자 CRUD 경로·요청·응답은 이 조회 통합으로 바뀌지 않는다.
|
|
- POST/PUT은 multipart: 파일 파트 `image`, `Content-Type: application/json`인 JSON 파트 `request`. 기본 JSON의 `placement`, `webUrl`, `androidDeepLink`, `iosDeepLink` 네 필드는 항상 모두 포함한다. 기존 네 필드는 PUT replacement이며 주소 `null`은 지운다. POST `image` 필수. PUT에서 `image` 생략은 기존 이미지 유지, 포함은 교체다.
|
|
- 선택적 `lang`은 기존 `Lang.fromCode` 규칙으로 `KO`/`EN`/`JA` 및 소문자 code를 대소문자 무시·앞뒤 trim으로 허용한다. 생성 생략은 KO, 수정 생략은 기존 언어 보존이다. 명시적 null·숫자·boolean·배열·객체·blank·알 수 없는 코드는 업로드/DB 쓰기 전에 HTTP 400이다. 언어를 지우는 null은 보내지 않는다.
|
|
- 앱 공개 서브 배너는 기존 회원 국가 resolver의 강제 매핑 우선, `CloudFront-Viewer-Country` 누락 KR 규칙으로 JP→JA, 그 외→KO만 반환한다. `Accept-Language`와 무관하며 EN은 저장 가능하지만 현재 공개 노출되지 않는다. 공개 DTO는 `lang` 없는 6필드다.
|
|
- `startDate`, `endDate`는 각각 선택적이다. create에서 생략 또는 null이면 무제한이다. update에서 생략하면 해당 저장 값을 보존하고 null이면 해당 경계만 해제한다. update는 두 날짜를 모두 제출해도 된다. 현재 저장된 placement와 주소가 예시와 같을 때 `{ "placement":"HOME_RECOMMENDATION", "webUrl":"https://example.test/promo", "androidDeepLink":"sampleapp://promo", "iosDeepLink":null, "startDate":"2026-11-01 09:30" }`는 주소를 유지하고 생략한 `endDate`를 유지한다. `"endDate":null`을 추가하면 종료만 해제한다. 날짜를 둘 다 null로 보내면 기간 제한을 모두 해제한다.
|
|
- 기간 의미: null/null은 무제한, 시작만 있으면 시작 분의 `:00`부터, 종료만 있으면 종료 분의 `:59`까지, 두 값이 있으면 양 끝을 포함한다. 같은 분도 유효하며 과거 시작일 제한은 없다.
|
|
- 입력은 엄격한 `Asia/Seoul` `yyyy-MM-dd HH:mm` 분 단위다. 초, `Z`, offset, blank string 및 존재하지 않는 KST 현지 시각 `1988-05-08 02:30`은 HTTP 400이다. 겹치는 현지 시각은 기존 앞선 offset으로 처리된다. browser local timezone이나 UTC ISO 문자열로 변환하지 않는다. 날짜 선택기에는 `KST`를 명시한다.
|
|
- 변환된 경계 instant는 UTC `1970-01-01 00:00:01`부터 `2038-01-19 03:14:07` 사이여야 한다. 이에 따른 KST 입력 범위는 시작 `1970-01-01 09:01`부터 `2038-01-19 12:14`, 종료 `1970-01-01 09:00`부터 `2038-01-19 12:13`이다. 이 범위 안의 과거 날짜는 유효하다. 잘못된 날짜/type 또는 effective start가 end보다 늦으면 HTTP 400이며 값 보정은 없다. update에서는 저장된 값과 새 값의 조합으로 검증한다.
|
|
- **작성자 제안:** form submit 경계에서 주소를 trim하고 blank를 `null`로 정규화한 뒤, 그 결과 세 주소 중 적어도 하나가 nonblank인지 검증한다. 이는 XOR 조건이 아니며 두 개 이상도 유효하다. nonblank URI 원문은 trim 외 재인코딩·변형하지 않는다.
|
|
- 웹 URL은 host를 가진 절대 http(s) URL이어야 한다. Android/iOS 딥링크는 문법적으로 유효한 절대 URI이며 `javascript`, `data`, `file`, `content` scheme은 대소문자와 무관하게 거부된다. 임의 scheme allowlist는 없다.
|
|
- 서비스가 검증하는 JSON/URI/date/type/placement 오류, 빈 파일 또는 비이미지 업로드는 HTTP 400이다. 유효한 정수 ID의 수정·삭제 대상이 없으면 HTTP 404이며 상태 오류 및 익명 401은 표준 Spring Boot `{timestamp,status,error,path}` body를 쓴다. 이는 `ApiResponse`가 아니다. multipart 파트 누락 또는 malformed multipart는 HTTP 200 `success:false`다. 프레임워크 바인딩 오류까지 모두 400이라고 가정하지 않는다. 유효한 비관리자는 HTTP 200 `success:false`다. 모든 응답에서 HTTP 상태와 `success`를 함께 확인하며 403을 전제하지 않는다.
|
|
- 파일 내용은 MIME 감지로 이미지인지 검사하며 GIF를 허용한다. 빈 파일과 비이미지는 HTTP 400이다. 현재 설정의 최대 파일/요청 크기는 1024 MB이며 제품 권장 용량이 아니다. reverse proxy 또는 배포 설정이 더 낮은 제한을 둘 수 있다. 범용 업로드 endpoint는 없다.
|
|
|
|
```json
|
|
{"placement":"HOME_RECOMMENDATION","webUrl":"https://example.test/promo","androidDeepLink":"sampleapp://promo","iosDeepLink":null,"startDate":"2026-11-01 09:30","endDate":null,"lang":"JA"}
|
|
```
|
|
|
|
POST/PUT 성공 응답 예시:
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": null,
|
|
"data": {"id": 123, "placement": "HOME_RECOMMENDATION", "imageUrl": "https://cdn.example.test/promo.jpg", "webUrl": "https://example.test/promo", "androidDeepLink": "sampleapp://promo", "iosDeepLink": null, "startDate": "2026-11-01 09:30", "endDate": null, "lang": "JA"},
|
|
"errorProperty": null
|
|
}
|
|
```
|
|
|
|
## 폼과 목록 동작
|
|
|
|
- 관리자 API는 placement별 삭제되지 않은 모든 배너를 id 오름차순으로 반환한다. 클라이언트에서 재정렬하지 않고 전부 표시한다. 별도 정렬 편집은 추가하지 않는다.
|
|
- 폼은 이미지, 세 주소, KST 시작/종료 날짜 선택기를 제공한다. 날짜 입력을 비우면 `null`을 보내며 빈 문자열은 보내지 않는다. 각 경계를 별도로 지울 수 있다. 화면에 KST를 표시하고 브라우저 지역 시각 또는 UTC ISO로 바꾸지 않는다.
|
|
- 언어 선택은 KO/EN/JA를 사용하고 편집 시 목록의 `lang`을 채운다. 언어 수정 생략을 KO로 재작성하지 않는다. 기존 행의 KO backfill은 자동 분류가 아니므로 JA 소재를 수동 확인하고 연결 이벤트 언어와 일치시킨다. 외부 URL/딥링크는 언어 추론·파싱 없이 원문을 유지한다.
|
|
- 편집은 목록의 전체 객체를 기반으로 모든 값을 채운다. 기존 이미지 URL/선택 상태, 주소 및 기간 값을 오류 후 보존한다. 기간만 바꾸는 요청은 기존 placement와 세 주소를 그대로 전송하고, image를 생략해 기존 이미지를 유지한다. 주소를 임의로 null로 초기화하지 않는다.
|
|
- 관리자 목록은 삭제되지 않은 배너의 미래/현재/만료/무기간 상태를 모두 표시하며 노출 기간으로 필터링하지 않는다. 기간 밖 레코드도 열어 수정할 수 있지만 소프트 삭제된 배너는 표시·수정하지 않는다. 같은 placement의 서버 id 오름차순을 유지한다.
|
|
- 업데이트 기본 네 JSON 필드는 항상 보내되, 날짜 필드는 둘 다 보내거나 필요한 경계만 보내거나 생략할 수 있다. 생략은 보존, 명시적 null은 해당 경계 해제다. API의 생략 보존 의미는 UI가 두 날짜를 모두 보낼 때도 유지된다.
|
|
- 최소 한 주소 검증, 주소 trim/null 직렬화, URI 원형 보존을 적용한다.
|
|
- 날짜 선택값은 KST 분 문자열로 직렬화한다. 시간대 변환으로 날짜나 분을 바꾸지 않는다. create의 날짜 미입력은 null/no-bound로 보내거나 생략할 수 있다. update에서 미입력 경계를 보존하려면 해당 필드를 생략하고, 사용자가 지웠으면 명시적 null을 보낸다.
|
|
- 저장 후 해당 placement 목록을 갱신한다. 실패 시 입력값과 기존 이미지 상태를 보존하고 실패를 알린다. autosave는 두지 않는다.
|
|
- 삭제 확인, 중복 제출 방지, 상태 표시 및 성공 후 갱신은 기존 관리자 UI 패턴을 따른다. mutation 자동 재시도는 하지 않으며 timeout 결과가 불명확하면 상태를 조회한다.
|
|
- 조회 실패를 빈 성공으로 취급하지 않는다.
|
|
- 필요하면 두 딥링크 주소를 입력해 특정 OS만 지원하는 레코드임을 설명할 수 있으나 별도 기능으로 확장하지 않는다.
|
|
|
|
## 주소 조합 acceptance matrix (작성자 제안)
|
|
|
|
`web`, `android`, `ios` 열은 해당 주소가 nonblank인 경우다. 빈 값은 null로 전송한다. Valid 여부는 최소 한 값 요구에 따른다.
|
|
Android/iOS 탭 동작은 작성자 추천이며 제품 승인된 UI 동작이 아니다. 권장 흐름은 현재 OS 딥링크 우선, 없으면 웹 URL이다.
|
|
|
|
| web | android | ios | 유효 | Android 동작 | iOS 동작 |
|
|
|---|---|---|---|---|---|
|
|
| 없음 | 없음 | 없음 | 아니오 | 표시 안 함 | 표시 안 함 |
|
|
| 있음 | 없음 | 없음 | 예 | 웹 | 웹 |
|
|
| 없음 | 있음 | 없음 | 예 | Android 딥링크 | 표시 안 함 |
|
|
| 없음 | 없음 | 있음 | 예 | 표시 안 함 | iOS 딥링크 |
|
|
| 있음 | 있음 | 없음 | 예 | Android 딥링크 우선, 실패 시 웹 | 웹 |
|
|
| 있음 | 없음 | 있음 | 예 | 웹 | iOS 딥링크 우선, 실패 시 웹 |
|
|
| 없음 | 있음 | 있음 | 예 | Android 딥링크 | iOS 딥링크 |
|
|
| 있음 | 있음 | 있음 | 예 | Android 딥링크 우선, 실패 시 웹 | iOS 딥링크 우선, 실패 시 웹 |
|
|
|
|
## 필수 테스트와 완료 확인
|
|
|
|
- 언어 KO/EN/JA 입력·응답, 생성 생략 KO, 수정 생략 유지, null·비문자열·blank·unknown 거부 및 관리자 전체 언어 목록을 확인한다.
|
|
- 여덟 주소 조합의 form 유효성 및 Android/iOS 대상 판단.
|
|
- 전부 null로 수정할 때 오류, `null`로 한 필드 지우기, 공백 trim, URI 원형 보존.
|
|
- 두 날짜 null, 시작만, 종료만, 같은 분, 경계의 `:00` 및 `:59`, 과거 날짜가 유효한 경우를 확인한다.
|
|
- create 날짜 생략/null, update 날짜 생략 보존, 개별 null 해제, 두 경계 해제, loaded date 재전송, 유효 범위가 retained/new 값 조합으로 검증됨을 확인한다.
|
|
- 잘못된 날짜/type, blank, 초, `Z`/offset 및 시작이 종료보다 늦은 값이 거부되고 자동 교환/rollover되지 않는지 확인한다.
|
|
- 미래/현재/만료/무기간 레코드가 관리자 목록에 남고 기간 외 레코드도 편집 가능한지 확인한다. 일정만 수정하고 image를 생략하면 기존 이미지가 유지되어야 한다.
|
|
- 여러 항목 표시, placement별 목록 분리, create/update/delete 성공과 실패.
|
|
- 실패 시 입력·이미지 상태 보존, 삭제 확인, 중복 제출 방지 및 timeout 후 중복 POST 방지 흐름.
|
|
- 인증/권한 실패와 목록 조회 실패를 empty로 오인하지 않는 상태.
|
|
|
|
프롬프트는 frontend-only로 실행한다. 백엔드 검증은 완료됐지만 운영 DB 적용·배포는 하지 않았다. 배포 target이 확인되기 전 운영 요청이나 배포 성공을 주장하지 않는다. UI를 수정하는 작업은 기존 프로젝트 방식으로 검증하고, 서버 코드 및 API는 변경하지 않는다.
|