docs(banner): 언어 필터 계약과 검증 결과를 기록한다
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
|
||||
**상태:** 관리자 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을 채운다.
|
||||
@@ -12,15 +14,17 @@
|
||||
|
||||
아래 계약은 구현되어 있지만 아직 배포되지 않았다. 배포된 target을 확인한 뒤 그 target에만 실제 연결한다. target 오류 후 환경 변경이나 mock fallback을 자동 적용하지 않는다. fixture는 명시적인 UI 검증에서만 사용하며 mock 상태를 서버 성공으로 표시하지 않는다.
|
||||
|
||||
- `GET /admin/sub-banners?placement={placement}`: 필수 placement query, 삭제되지 않은 활성 배너를 기간과 무관하게 반환한다.
|
||||
- `GET /admin/sub-banners?placement={placement}`: 필수 placement query, 삭제되지 않은 활성 배너를 기간·언어와 무관하게 모두 반환한다. 서버 언어 필터를 가정하지 않는다.
|
||||
- `POST /admin/sub-banners`: 생성.
|
||||
- `PUT /admin/sub-banners/{id}`: 기본 JSON 네 필드 교체. 두 날짜 경계만 선택적으로 생략 보존 또는 명시적 null 해제한다.
|
||||
- `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`: `id` integer JSON number(정밀도를 보존하는 불투명 식별자), `placement` enum(`HOME_RECOMMENDATION` 또는 `CONTENT_RECOMMENDATION`), `imageUrl` 절대 http/https URL 문자열, `webUrl`·`androidDeepLink`·`iosDeepLink` 각각 string|null, `startDate`·`endDate` 각각 string|null. 모든 날짜는 `Asia/Seoul` 기준 `yyyy-MM-dd HH:mm` 문자열 또는 null이다. 목록, 생성, 수정 모두 이 관리자 shape를 쓴다.
|
||||
- 관리자 응답 형식 `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`를 명시한다.
|
||||
@@ -31,7 +35,7 @@
|
||||
- 파일 내용은 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}
|
||||
{"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 성공 응답 예시:
|
||||
@@ -40,7 +44,7 @@ POST/PUT 성공 응답 예시:
|
||||
{
|
||||
"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},
|
||||
"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
|
||||
}
|
||||
```
|
||||
@@ -49,6 +53,7 @@ POST/PUT 성공 응답 예시:
|
||||
|
||||
- 관리자 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가 두 날짜를 모두 보낼 때도 유지된다.
|
||||
@@ -77,6 +82,7 @@ Android/iOS 탭 동작은 작성자 추천이며 제품 승인된 UI 동작이
|
||||
|
||||
## 필수 테스트와 완료 확인
|
||||
|
||||
- 언어 KO/EN/JA 입력·응답, 생성 생략 KO, 수정 생략 유지, null·비문자열·blank·unknown 거부 및 관리자 전체 언어 목록을 확인한다.
|
||||
- 여덟 주소 조합의 form 유효성 및 Android/iOS 대상 판단.
|
||||
- 전부 null로 수정할 때 오류, `null`로 한 필드 지우기, 공백 trim, URI 원형 보존.
|
||||
- 두 날짜 null, 시작만, 종료만, 같은 분, 경계의 `:00` 및 `:59`, 과거 날짜가 유효한 경우를 확인한다.
|
||||
|
||||
Reference in New Issue
Block a user