12 KiB
관리자 구현 프롬프트: 가로형 서브 배너
이 프롬프트는 관리자 작업에 단독으로 사용할 수 있다. 작업 대상 repository의 실제 stack, 관리자 인증, API client, multipart 전송 및 form/list 패턴을 먼저 확인한다. 특정 framework를 전제하지 않는다. 이 작업은 frontend-only이며 서버, 저장소, schema, migration, 배포 작업으로 확장하지 않는다.
상태: 관리자 CRUD API 구현·검증 완료, 미배포. 백엔드 전체 테스트·lint·빌드와 독립 코드 리뷰를 통과했다. 화면 배치, 폼 표현, 삭제 확인 등 UI 선택은 작성자 추천이며 계약으로 가장하지 않는다.
목표
가로형 서브 배너의 목록·생성·수정·삭제 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 200success:false이며 403을 보장하지 않는다. - 성공은 HTTP 200 및 네 필드
ApiResponse<T>:success,message,data,errorProperty. GETdata는 전체 객체 배열, POST/PUT은 저장된 전체 객체, DELETE는null. 빈 목록[]. - 관리자 응답 형식
AdminSubBanner:idinteger JSON number(정밀도를 보존하는 불투명 식별자),placementenum(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를 쓴다. - 앱은 두 추천 API의
data.subBanners를 사용한다. 배열 항목은 날짜·활성 필드가 없는 기존 6필드이며 활성·기간 내 항목에 앱의 웹/현재 OS 링크 조건을 적용한다. 관리자 CRUD 경로·요청·응답은 이 조회 통합으로 바뀌지 않는다. - POST/PUT은 multipart: 파일 파트
image,Content-Type: application/json인 JSON 파트request. 기본 JSON의placement,webUrl,androidDeepLink,iosDeepLink네 필드는 항상 모두 포함한다. 기존 네 필드는 PUT replacement이며 주소null은 지운다. POSTimage필수. PUT에서image생략은 기존 이미지 유지, 포함은 교체다. 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/Seoulyyyy-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,contentscheme은 대소문자와 무관하게 거부된다. 임의 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 200success:false다. 프레임워크 바인딩 오류까지 모두 400이라고 가정하지 않는다. 유효한 비관리자는 HTTP 200success:false다. 모든 응답에서 HTTP 상태와success를 함께 확인하며 403을 전제하지 않는다. - 파일 내용은 MIME 감지로 이미지인지 검사하며 GIF를 허용한다. 빈 파일과 비이미지는 HTTP 400이다. 현재 설정의 최대 파일/요청 크기는 1024 MB이며 제품 권장 용량이 아니다. reverse proxy 또는 배포 설정이 더 낮은 제한을 둘 수 있다. 범용 업로드 endpoint는 없다.
{"placement":"HOME_RECOMMENDATION","webUrl":"https://example.test/promo","androidDeepLink":"sampleapp://promo","iosDeepLink":null,"startDate":"2026-11-01 09:30","endDate":null}
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},
"errorProperty": null
}
폼과 목록 동작
- 관리자 API는 placement별 삭제되지 않은 모든 배너를 id 오름차순으로 반환한다. 클라이언트에서 재정렬하지 않고 전부 표시한다. 별도 정렬 편집은 추가하지 않는다.
- 폼은 이미지, 세 주소, KST 시작/종료 날짜 선택기를 제공한다. 날짜 입력을 비우면
null을 보내며 빈 문자열은 보내지 않는다. 각 경계를 별도로 지울 수 있다. 화면에 KST를 표시하고 브라우저 지역 시각 또는 UTC ISO로 바꾸지 않는다. - 편집은 목록의 전체 객체를 기반으로 모든 값을 채운다. 기존 이미지 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 딥링크 우선, 실패 시 웹 |
필수 테스트와 완료 확인
- 여덟 주소 조합의 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는 변경하지 않는다.