docs(subbanner): 요구사항과 앱·관리자 연동 계약을 정의한다

This commit is contained in:
2026-10-29 23:24:20 +09:00
parent 73aa6da7f0
commit 103f8c4396
4 changed files with 485 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
# 가로형 서브 배너 백엔드 요구사항
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 소프트 삭제·두 추천 API 통합 및 전체 검증 완료, 미배포 |
| 작성일 | 2026-10-29 |
| 최종 수정일 | 2026-10-29 |
| 대상 기능 | 가로형 서브 배너 백엔드 및 영속성 |
| 관련 API 계약 | [api-contract.md](api-contract.md), 구현·실제 HTTP 검증 기준 |
| 구현 계획 | [plan-task.md](plan-task.md) |
## 요구사항 상태와 현재 승인 범위
- 기존 `USER REQUIREMENTS`는 사용자가 확정한 제품 요구이며, 아래 `USER REQUIREMENTS`와 `DEC-001`, `DEC-005`에 보존한다.
- 초기 API·저장 모델 제안은 `DEC-009`의 사용자 구현 승인에 따라 구현했으며 실제 검증한 계약은 `api-contract.md`에 기록한다. 앱 레이아웃·링크 우선순위 등 UI 추천은 백엔드 구현 완료와 구분한다.
- **현재 승인 범위는 문서대로 백엔드 코드와 DDL 파일을 생성하고 검증하는 것이다.** 운영 DB 적용·배포·앱/관리자 UI 구현은 제외한다. 앞선 문서-only 승인 이력은 Decision Log에 보존한다.
- API 및 저장 모델은 이 문서와 API 계약을 기준으로 구현하되 완료·검증·배포 상태는 구분한다. 소비자 프롬프트는 실제 구현 계약과 다른 설명이 남지 않도록 완료 시 상태를 맞춘다.
## USER REQUIREMENTS
- 가로형 서브 배너 기능이 필요하다.
- 관리자는 배너를 생성, 조회, 수정, 삭제할 수 있어야 한다.
- 웹 URL, Android 딥링크, iOS 딥링크는 각각 선택적으로 등록하며 적어도 하나가 있어야 한다. 정확히 하나만 허용하는 XOR 조건은 아니다. 세 값이 모두 없는 조합은 거부하고 나머지 7개 존재 조합은 허용한다.
- 노출 위치는 메인 홈 추천과 메인 콘텐츠 추천이며, 각 위치에 여러 배너를 조회할 수 있어야 한다.
- 앱은 서버가 반환한 배너 중 웹 URL 또는 현재 대상 OS 링크가 있는 항목을 표시한다.
- 관리자는 기존 콘텐츠 배너 예약 기간과 같은 선택적 시작·종료 기간을 설정할 수 있다.
- 삭제는 `is_active`를 이용한 소프트 삭제로 수행한다. 생성 기본값은 true이며 삭제 시 false로 변경해 행과 기존 데이터를 보존한다.
- 앱 조회는 기존 홈 추천·콘텐츠 추천 API의 `data.subBanners` 배열로 제공하며 두 GET 모두 비로그인 접근을 유지한다. 별도 앱 조회 `/api/v2/sub-banners`는 제거한다. 앱 프롬프트도 이 계약으로 갱신한다.
## 초기 제안과 구현 결정
- 한 레코드에 한 placement를 저장하고 같은 소재를 두 곳에 노출하려면 각각 등록한다. `DEC-003`의 초기 제안을 구현에 적용했으며 최초 사용자 발화로 소급하지 않는다.
- 주소는 trim 후 빈 문자열을 null로 정규화하고 URI 원문을 보존한다. `DEC-002`의 입력 처리 제안을 구현했다.
- 독립 테이블, 서버 생성 이미지 key 및 이미지 검증을 유지한다. 초기 DB row 물리 삭제는 `DEC-010`에 따라 `is_active=false` 소프트 삭제로 변경한다. 이미지의 기존 S3 object 자동 삭제는 제공하지 않는다.
## 목표와 비목표
### 목표
- **G1:** 독립 테이블과 백엔드 도메인으로 두 placement의 배너 레코드를 격리하고, 생성·조회·수정·삭제 후 DB 재조회에서도 데이터가 보존된다. (`SB-001`~`SB-003`, `SB-010`)
- **G2:** 관리자 API는 ADMIN만 사용하고 이미지 multipart 포함 CRUD를 제공하며, payload 검증이 업로드와 영속화보다 먼저 이뤄진다. (`SB-004`~`SB-007`, `SB-011`)
- **G3:** 공개 GET은 placement와 유효 기간으로 필터링하고 전체 목록을 id 오름차순으로 반환하며, 날짜 없는 공개 DTO와 공통 응답 envelope를 유지한다. (`SB-008`~`SB-009`)
- **G4:** 배포 DDL은 적용·롤백 영향을 명시하며 기존 콘텐츠 배너 테이블과 데이터를 변경하지 않는다. (`SB-010`)
### 포함 범위
- Kotlin/Spring 도메인, application service, persistence adapter와 DTO/controller.
- 공개 단일 GET 및 관리자 CRUD endpoint, 보안 matcher, 이미지 업로드 연동.
- 신규 테이블 생성용 SQL artifact와 테스트, 시간 정밀도 및 접근 제어 검증.
### 제외 범위
- 앱 및 관리자 실제 UI 구현, 기존 대형 콘텐츠 배너 변경, 예약 스케줄러/job, 순서 편집, 별도 활성 토글·삭제 복구 UI/API, 분석.
- 새 라이브러리, 공통 예외 처리기 전면 변경, 배포 실행, 운영 DB 직접 변경.
- 기존 콘텐츠 배너 테이블 migration. 새 서브 배너 전용 테이블만 제안한다.
- idempotency key, pagination, 자동 재시도 또는 요청 간 exactly-once 보장.
## 대상 사용자와 권한
| 주체 | 허용 동작 | 거부 조건 |
|---|---|---|
| 익명 앱 클라이언트 | 지정 placement의 공개 GET | 다른 HTTP method 및 인접 경로는 기존 인증 정책 적용 |
| `ADMIN` | 모든 관리자 CRUD | 인증되지 않았거나 ADMIN이 아닌 요청은 변경 전 거부 |
## 기능 요구사항
아래 요구사항은 `DEC-009`의 구현 승인에 따라 완료했으며 각 행을 [plan-task.md](plan-task.md)의 실행 Goal·검증 증거에 연결한다. 상태 열의 초기 제안 표기는 요구사항 출처이며 현재 미구현을 뜻하지 않는다. 최종 wire 계약은 `api-contract.md`를 따른다.
| ID | 상태 | 요구사항 | 수용 기준 | 계약 / Goal |
|---|---|---|---|---|
| `SB-001` | 작성자 영속성 제안 | 서브 배너는 독립적인 테이블에 저장한다. placement는 `HOME_RECOMMENDATION` 또는 `CONTENT_RECOMMENDATION` 하나다. | 신규 레코드가 제안 SQL의 독립 테이블에서 저장/조회되고, 기존 콘텐츠 배너 테이블에는 schema/data 변경이 없다. | §1, `P2-T1` |
| `SB-002` | API 형태 제안, 최소 주소 조건 확정 | 응답 객체는 id, placement, imageUrl, 세 nullable 링크를 가진다. 내부 저장 모델은 CDN URL 대신 object key인 imagePath를 보관한다. | 8개 존재 조합 중 모두 null인 경우는 거부하고 나머지 7개 조합은 허용한다. 관리자와 공개 DTO에만 imagePath를 절대 imageUrl로 조립한다. | §1, `P2-T1`, `P3-T1` |
| `SB-003` | CRUD 확정, 응답·정렬 제안 | 관리자는 placement별 활성 목록, 생성, 수정, 삭제를 수행한다. | GET은 삭제되지 않은 행을 기간과 무관하게 id 오름차순으로 반환한다. 생성/수정은 저장된 객체, 삭제는 `data:null`로 응답한다. | §3, `P3-T1`, `P5-T1` |
| `SB-004` | 최소 주소 조건 확정, 정규화·URI 안전 규칙 제안 | 세 링크는 각각 nullable이며 trim 후 최소 하나가 nonblank여야 한다. | 모든 8개 존재 조합을 검사하며 모두 null/blank인 경우만 거부한다. 공백뿐인 값은 null 처리한다. nonblank 값은 trim 외 원문 byte/문자열을 재인코딩하거나 정규화하지 않는다. web URL은 absolute http/https, 딥링크는 유효한 URI이며 `javascript`, `data`, `file`, `content` scheme은 대소문자와 무관하게 거부한다. 별도의 임의 scheme allowlist는 추가하지 않는다. | §1, `P3-T1` |
| `SB-005` | 작성자 안전 제안 | 이미지가 필수인 생성과 선택적 이미지 교체 수정을 지원한다. | MIME/type 검증 및 모든 request 검증이 S3 업로드와 DB 변경 전에 끝난다. 업로드 실패 시 기존 이미지 값은 바뀌지 않는다. DB rollback이 이미 완료된 S3 업로드를 되돌린다고 가정하지 않는다. | §3, `P3-T1` |
| `SB-006` | 사용자 확정 | 선택적 `startDate`, `endDate`를 독립적으로 설정·조회·수정·해제한다. | KST `yyyy-MM-dd HH:mm`만 받고 응답한다. 생성 누락/null은 unbounded, 수정 누락은 유지, 수정 null은 해당 경계 해제다. 병합 후 날짜 역전은 저장하지 않는다. | §1, §3, `P2-T1`, `P3-T1` |
| `SB-007` | 확정 요구, 상태 매핑은 기존 정책 확인 필요 | 관리자 GET/POST/PUT/DELETE는 ADMIN 권한이어야 한다. | 익명 및 non-ADMIN이 CRUD를 시도해도 데이터가 변하지 않는다. 테스트는 실제 공통 접근 거부 handler의 HTTP/envelope 동작을 확인하고 임의로 403을 가정하지 않는다. | §3, `P3-T1`, `P4-T1` |
| `SB-008` | 사용자 후속 확정 | 두 추천 API가 각 화면 placement의 활성·기간 내 전체 서브 배너를 함께 반환한다. | 비로그인 `GET /api/v2/home/recommendations`는 HOME_RECOMMENDATION, `GET /api/v2/audio/recommendations`는 CONTENT_RECOMMENDATION의 `data.subBanners`를 반환한다. id 오름차순·빈 배열·기간 경계·무절단 규칙은 유지하고 별도 앱 조회 endpoint를 제거한다. | §2, `P6-T1` |
| `SB-009` | 사용자 후속 확정 | 기존 추천 응답에 `subBanners`만 추가하고 다른 필드는 보존한다. | 각 배열 항목은 기존 6필드 `{id, placement, imageUrl, webUrl, androidDeepLink, iosDeepLink}`다. 기존 `banners`와 추천 콘텐츠를 교체하지 않는다. 날짜·isActive는 노출하지 않으며 OS 필터는 앱 책임이다. | §1, §2, `P6-T1` |
| `SB-010` | 작성자 데이터 안전 제안 | 신규 테이블 DDL과 적용 전후 안전성을 제공한다. | `docs/20261029_가로형서브배너/create-sub-banner.sql`에 모든 column/table COMMENT, nullable UTC `TIMESTAMP` 기간 컬럼, 필요 시 기존 audit timestamp 관례를 포함한다. 계획에 적용 조건과 rollback 절차를 적고 자동 live DDL은 하지 않는다. | §1, `P2-T1` |
| `SB-011` | 작성자 API/오류 제안 | 성공 envelope와 계약된 응답 shape를 일관되게 유지한다. | 성공은 `ApiResponse<T>` 4필드, HTTP 200, 관리자 빈 목록 `[]`, 관리자 POST/PUT 객체, DELETE `null`, 공개 날짜 미포함. 잘못된 입력/없음 상태의 status는 API 계약 검토 후 기존 handler를 활용하거나 endpoint 경계에서 처리한다. | `api-contract.md` §1, §5, `P3-T1`, `P4-T1` |
| `SB-012` | 사용자 후속 확정 | 관리자 삭제는 `is_active=false` 소프트 삭제다. | DB 행·이미지 key·주소·기간·생성 시각을 보존한다. 관리자/공개 목록과 id 조회에서 비활성 행을 제외하며 삭제된 id 수정·재삭제는 404다. 응답에 isActive를 추가하지 않고 S3는 보존한다. 복구/활성 토글/cleanup은 범위 밖이다. | §3, `P5-T1` |
## API 및 데이터 계약
- 앱 조회: `GET /api/v2/home/recommendations`, `GET /api/v2/audio/recommendations`의 `data.subBanners` 배열. 비로그인 허용, 화면별 placement 고정, 페이지네이션 없음, id 오름차순. 별도 `/api/v2/sub-banners`는 제거한다.
- 관리자: `GET/POST /admin/sub-banners`, `PUT/DELETE /admin/sub-banners/{id}`, ADMIN 전용. GET은 필수 placement query.
- 성공은 HTTP 200과 공통 `ApiResponse` `{success,message,data,errorProperty}`. POST/PUT multipart에는 `image`와 JSON `request`; POST image 필수, PUT image 선택. PUT의 기존 네 필드는 필수 replacement이고 날짜만 선택적 보존/해제다.
- 두 추천 응답의 `subBanners` 항목은 기본 6필드, 관리자 DTO는 날짜 문자열 두 필드를 더한다. 추천 응답의 기존 `banners` 등 모든 기존 필드는 유지한다.
- 관리자 목록은 삭제되지 않은 활성 행의 모든 기간 상태를 반환한다. 공개 조회는 활성 행에 현재 기간 필터를 추가하며 앱에서 로컬 시간으로 기간 판정하지 않는다.
- 입력 검증과 없는 id는 `ResponseStatusException`을 통해 400/404 Boot 오류 body를 반환한다. 익명은 401, 유효한 비관리자 및 multipart 바인딩 오류는 기존 handler의 200 `success:false`다. 공통 예외 정책을 변경하지 않았다.
## 시간·검증·저장 규칙
- 입력 시각은 엄격한 KST `yyyy-MM-dd HH:mm` 분 정밀도만 허용한다. 초, offset, `Z`, 공백/빈 문자열, raw scalar의 문자열 외 타입, 잘못된 달력 날짜 및 병합 후 start > end는 저장/업로드 전에 거부한다.
- 시작 분은 `:00`, 종료 분은 `:59`까지 포함한다. 동일 분 경계는 유효하다. null 경계는 독립적으로 unbounded다.
- 저장 경계는 KST 입력에서 한 번 정규화한 UTC 초 단위 값이며, 그 값을 영속화와 관리자 응답 변환에 공통 사용한다. DB flush/clear/reload 후 시작 `:00`·종료 `:59`와 관리자 날짜 문자열을 정확히 왕복 비교한다. 공개 조회의 현재 시각은 한 번만 얻어 초 단위로 절삭한다. 이 시각 seam에 나노초 `0`, `123456789`, `999999999` 및 날짜 경계를 넣어 종료 분 마지막 초와 다음 초의 판정을 확인한다. 분 형식 API에 소수초 입력을 허용하는 것은 아니다.
- 링크 입력은 JSON 원문 유형 확인 후 trim 처리한다. nonblank URI는 trim 이후 원문을 보존한다. 기존 Tika 기반 `validateImage(file, gifAllowed = true)`를 호출해 이미지 내용을 검사하고 GIF를 허용한다. 빈 파일은 거부한다. 신규 dependency나 임의 추가 크기 제한은 없다.
- 현재 `application.yml` multipart 제한은 파일 및 요청 각각 1024MB다. 이를 새 제품 상한으로 해석하거나 몰래 변경하지 않는다.
- `S3Uploader.upload(InputStream, bucket, filePath, metadata?)` 반환은 object key다. `imageUrl` 생성 시 기존 CDN 변환 관례를 사용하며 object key를 URL로 오인해 응답하지 않는다. 저장 실패 뒤 이미 업로드된 S3 객체는 자동 삭제/rollback을 보장하지 않으며 cleanup job은 범위 밖이다.
- 삭제는 `is_active=false`만 반영하고 DB 행과 S3 object를 보존한다. 관리자 목록은 활성 행의 모든 기간 상태를, 공개 목록은 활성 행 중 기간 내 항목만 반환한다.
## 근거와 제안 경계
현행 동작 근거:
- 추천 배너 구조: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/common/dto/RecommendationBannerResponse.kt`, `.../v2/api/home/adapter/in/web/HomeRecommendationController.kt`, `.../v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt`.
- 기존 관리자 배너: `src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt`, `AdminContentBannerService.kt`, `GetAdminContentBannerResponse.kt`.
- 예약 기간 규칙: `docs/20261027_콘텐츠배너예약기간/prd.md` §6-8, `src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerVisibility.kt`, `docs/20261027_콘텐츠배너예약기간/alter-content-banner-reservation.sql`.
- 경량 헥사고날 패키지와 권한: `docs/agent-guides/코드스타일.md`, `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`.
- 응답/예외 사실: `src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt`, `SodaExceptionHandler.kt`.
- 이미지 업로드/검증: `src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/utils/ImageValidation.kt`.
- 테스트 관례: `src/test/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerControllerIntegrationTest.kt`, `AdminContentBannerServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`.
신규 `v2.subbanner` 경로와 SQL은 구현되어 있다. 기존 `v2.creatorname` 패턴을 따른 단일 독립 기능 모듈이며 불필요한 API composition 모듈이나 utility wrapper를 추가하지 않았다.
## 수용 기준
- [x] `SB-001`~`SB-012` 각각을 구현 Goal·테스트·Gate 증거로 연결했다.
- [x] 신규 테이블, 기간 및 기본 CRUD는 기존 콘텐츠 배너 schema/data를 변경하지 않는다.
- [x] 관리자 기간 경계를 실제 DB flush/clear/reload와 HTTP 날짜 왕복으로 정확히 검증했다.
- [x] 익명 public GET만 permitAll이며 인접 경로·다른 method·관리자 권한 보호를 실제 HTTP로 검증했다.
- [x] payload 검증 실패의 이미지 upload/DB mutation 0회와 실제 오류 응답을 검증했다.
- [x] 전체 2,676 tests, lint, bootJar 및 독립 최종 리뷰 통과. 운영 DB 적용·배포는 별도이며 완료로 표시하지 않는다.
- [x] 후속 P5·P6는 전체 2,682 tests(실패·무시 0), lint·bootJar와 독립 리뷰를 통과했다. 비로그인 두 추천 응답의 subBanners와 standalone handler 제거, 소프트 삭제 행 보존 및 동시 비복원을 실제 HTTP/DB로 검증했다. 앞선 2,676건은 초기 구현 이력이다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 |
|---|---|---|---|---|
| 2026-10-29 | DEC-001 | 사용자 확정 | 가로형 서브 배너, 관리자 CRUD, 세 nullable 주소 중 최소 하나, 두 추천 placement, 각 목록 다중 항목, 웹 URL 또는 대상 OS 딥링크가 있는 앱 항목 표시 | 사용자 확인 |
| 2026-10-29 | DEC-002 | 작성자 제안 | trim 후 blank를 null로 만들고 nonblank URI를 변형하지 않음 | 입력 안전성 제안, 승인되지 않음 |
| 2026-10-29 | DEC-003 | 작성자 제안 | API 경로·모델·정렬 순서·단일 placement 레코드 정책 | 일관된 최소 계약 제안, 승인 및 서버 제공 대기 |
| 2026-10-29 | DEC-004 | 작성자 제안 | 대상 OS 딥링크 우선 및 실패 시 웹 URL fallback | 앱 이동 정책 제안, 승인되지 않음 |
| 2026-10-29 | DEC-005 | 사용자 확정 | 관리자에서 콘텐츠 배너와 같은 선택적 노출 기간을 설정한다. KST 분 형식 및 경계 의미는 기존 콘텐츠 배너 예약 동작을 따른다. | 사용자 확인 및 기존 콘텐츠 배너 근거 |
| 2026-10-29 | DEC-006 | 사용자 승인, 범위 제한 | 이번 승인으로 기존 디렉터리의 PRD와 plan-task를 백엔드 요구사항 및 구현 계획으로 완성한다. 이번 변경은 문서 전용이며 제안 API 전체 승인이나 실제 구현 승인이 아니다. | 사용자 요청의 현재 작업 승인 범위 |
| 2026-10-29 | DEC-007 | 작성자 구현 권고, 검토 필요 | 독립 `v2.subbanner` 모듈과 제안된 신규 테이블을 사용한다. 이미 업로드된 S3 객체의 DB rollback 후 자동 정리는 보장하지 않으며 삭제 정책은 첫 구현 Task에서 확인한다. | 현행 코드 패턴과 S3/DB transaction 한계. 사용자 제품 결정으로 간주하지 않음 |
| 2026-10-29 | DEC-008 | 사용자 승인 | 문서 전용 작업의 검증은 문서 대조와 공백 검사로 한정하고 Gradle·테스트·빌드는 실행하지 않는다. 향후 구현용 명령을 문서에 적는 것만으로 실행 검증을 요구하지 않는다. | 사용자 승인 및 `docs/agent-guides/문서유지보수.md`의 검증 지침 수정 |
| 2026-10-29 | DEC-009 | 사용자 구현 승인 | 문서대로 백엔드 코드와 DDL 파일을 구현한다. 운영 DB 적용·배포는 승인 범위에 포함하지 않는다. 기존 기간·독립 API 모델을 사용하며 검증 시 H2·테스트 JWT·S3 대체 객체로 외부 부작용을 격리한다. | 후속 사용자 요청 및 ulw 실행 |
| 2026-10-29 | DEC-010 | 사용자 후속 확정·정정 | `is_active BOOLEAN NOT NULL DEFAULT TRUE`를 추가하고 DELETE는 false 갱신으로 변경한다. 삭제된 행은 모든 소비자 조회에서 제외하며 응답·권한·기간·S3 정책은 유지한다. 앞선 하드 삭제 완료 기록은 당시 이력으로 보존한다. | "is_active를 두고 소프트 삭제로 하자" |
| 2026-10-29 | DEC-011 | 사용자 후속 확정·정정 | 별도 앱 GET을 제거하고 두 추천 GET에 `data.subBanners`를 추가한다. 두 GET 모두 비로그인 접근을 유지하고 앱 프롬프트도 함께 변경한다. 관리자 CRUD·소프트 삭제·기간·응답 항목은 유지한다. | 두 추천 API 통합 및 앱 프롬프트 수정 요청 |
## 후속 사용자 요구사항: 관리자 노출 기간
- 관리자는 가로형 서브 배너의 선택적 `startDate`, `endDate`를 설정·조회·수정·해제할 수 있다.
- 두 경계는 독립적 nullable 값이다. 둘 다 null이면 기간 제한이 없고, 시작만 있으면 시작 시각부터, 종료만 있으면 종료 경계까지 노출한다. 같은 분의 시작/종료도 유효하다.
- 관리자 입력과 응답은 `Asia/Seoul`의 `yyyy-MM-dd HH:mm` 문자열이며 분 정밀도다. 시작 분은 `:00`부터 포함하고 종료 분은 `:59`까지 포함한다. 초, `Z`, offset은 받지 않는다.
- 공개 조회는 현재 기간 안에 있는 항목 전체를 반환한다. 앱은 그 결과에 기존 웹 URL/현재 OS 딥링크 필터를 적용하며 앱 로컬 시계로 기간을 계산하지 않는다.
- 공개 DTO에는 날짜·활성 필드를 추가하지 않는다. 관리자 목록은 삭제되지 않은 활성 행의 미래·현재·만료·무제한 상태를 모두 포함한다.
- 기존 근거는 `docs/20261027_콘텐츠배너예약기간/prd.md` §6-8, `AdminContentBannerService.kt` 및 `GetAdminContentBannerResponse.kt`다. 신규 계약의 승인이나 구현을 뜻하지 않는다.