Files

14 KiB

관리자 구현 프롬프트: 가로형 서브 배너

이 프롬프트는 관리자 작업에 단독으로 사용할 수 있다. 작업 대상 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는 없다.
{"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 성공 응답 예시:

{
  "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는 변경하지 않는다.