Test #462
@@ -0,0 +1,65 @@
|
|||||||
|
# 최근 공지사항 v2 API 계약
|
||||||
|
|
||||||
|
## 1. 요청
|
||||||
|
|
||||||
|
| 항목 | 계약 |
|
||||||
|
|---|---|
|
||||||
|
| Method / Path | `GET /api/v2/notices/latest` |
|
||||||
|
| 접근 | 인증 없이 허용, Authorization 필수 아님 |
|
||||||
|
| 입력 | path 변수·query·body 없음 |
|
||||||
|
| 성공 | HTTP 200, JSON `ApiResponse<List<NoticeItem>>` |
|
||||||
|
| 페이지네이션 | 없음. 항상 최대 3개, page/size/total/hasNext 없음 |
|
||||||
|
|
||||||
|
## 2. 데이터 규칙
|
||||||
|
|
||||||
|
`isActive=true`인 공지만 id 내림차순으로 최대 3개 반환한다. createdAt 최신순이 아니다. 기존 `NoticeItem`과 목록 쿼리를 재사용하며 아래 필드는 변경하지 않는다.
|
||||||
|
|
||||||
|
| 필드 | 타입 | 의미 |
|
||||||
|
|---|---|---|
|
||||||
|
| id | JSON number / Kotlin Long | 기존 공지 식별자 |
|
||||||
|
| title | string | 원래 제목 전체 |
|
||||||
|
| content | string | 원래 본문 전체, 축약·변환하지 않음 |
|
||||||
|
| date | string | 기존 UTC createdAt을 Asia/Seoul로 변환한 `yyyy-MM-dd` |
|
||||||
|
|
||||||
|
UTC `2026-10-27 15:00:00`은 `2026-10-28`이다. 날짜 formatter·timezone·필드 이름·타입을 새로 정의하지 않는다.
|
||||||
|
|
||||||
|
## 3. 성공 envelope와 예시
|
||||||
|
|
||||||
|
공통 `ApiResponse`는 success/message/data/errorProperty 네 필드를 모두 갖는다. 성공의 `success=true`, data는 null이 아닌 0~3개 배열이며 `message:null`, `errorProperty:null`이 생략되지 않고 포함된다. 2026-10-28 focused 실제 HTTP 테스트 11개에서 전체 JSON tree 비교로 확인했다. 전역 Jackson 설정은 변경하지 않는다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": null,
|
||||||
|
"data": [
|
||||||
|
{"id": 104, "title": "서비스 안내", "content": "공지 본문 전체\n두 번째 줄", "date": "2026-10-28"},
|
||||||
|
{"id": 103, "title": "점검 안내", "content": "점검 안내 전체", "date": "2026-10-27"},
|
||||||
|
{"id": 102, "title": "업데이트 안내", "content": "업데이트 안내 전체", "date": "2026-10-26"}
|
||||||
|
],
|
||||||
|
"errorProperty": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
활성 공지가 없으면 (전부 비활성인 경우 포함):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"success":true,"message":null,"data":[],"errorProperty":null}
|
||||||
|
```
|
||||||
|
|
||||||
|
활성 공지가 1개 또는 2개면 실제 개수만 반환하며 채우거나 반복하지 않는다. 예시 id는 설명용이며 고정 데이터나 실제 배포 응답이 아니다.
|
||||||
|
|
||||||
|
## 4. 호환성과 오류
|
||||||
|
|
||||||
|
- 기존 `GET /notice/latest`는 기존 단일 객체 또는 `data:null` 계약을 유지한다. 홈 응답도 그대로다.
|
||||||
|
- 새 permitAll은 정확한 GET 경로 하나에만 적용한다. 실제 HTTP 검증에서 익명 `POST /api/v2/notices/latest`와 `GET /api/v2/notices/private`는 HTTP 401이었다. 기존 보안 오류 응답은 아래와 같이 성공 envelope와 다르다. timestamp 값은 요청마다 달라진다.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"timestamp":"2026-10-28T06:28:16.011+00:00","status":401,"error":"Unauthorized","path":"/api/v2/notices/latest"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 기존 공통 오류 처리의 `ApiResponse.error`는 `success=false`다. 일부 공통 예외는 HTTP 200에도 오류 envelope가 오므로 클라이언트는 HTTP 상태와 success를 함께 확인한다. HTTP/보안 오류가 모두 같은 envelope라고 가정하지 않는다.
|
||||||
|
- 조회 실패를 정상 `[]`로 바꾸지 않는다. 새 오류 코드·status·전역 예외 정책·자동 fallback은 추가하지 않는다.
|
||||||
|
|
||||||
|
## 5. 제공 상태
|
||||||
|
|
||||||
|
구현 및 focused 실제 HTTP 검증 완료(공지 11개와 기존 배포 엔드포인트 4개, 총 15 tests, failures/errors/skipped 모두 0). `bootJar`와 전체 `ktlintCheck`는 통과했으나 전체 `build --continue`는 test 단계에서 20분 제한으로 종료되어 최종 Gate는 미완료다. 실응답·명령 결과와 독립 리뷰 상태는 [plan-task.md](plan-task.md)에 기록했다. 운영 배포는 수행하지 않았으며 배포 완료로 간주하지 않는다.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 클라이언트 전달용 프롬프트
|
||||||
|
|
||||||
|
아래 내용을 클라이언트 작업자에게 전달한다. 서버 코드 구현·로컬 검증과 실제 배포는 별개다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
기존 공지 UI, 레이아웃, 스타일, 진입점, 상세 이동과 내비게이션을 유지하면서 최근 공지 데이터만 아래 새 계약으로 연결해 주세요. UI 재설계나 캐러셀 추가는 하지 마세요.
|
||||||
|
|
||||||
|
API: 익명 GET /api/v2/notices/latest
|
||||||
|
요청 body/query와 인증 필수 요구는 없습니다.
|
||||||
|
응답: ApiResponse<List<NoticeItem>>
|
||||||
|
NoticeItem: id(number), title(string), content(string), date(string, yyyy-MM-dd).
|
||||||
|
data는 활성 공지 중 id 내림차순 최대 3개입니다. 순서를 다시 정렬하지 말고 서버 순서를 사용하세요. date는 이미 기존 UTC→Asia/Seoul 규칙으로 포맷된 문자열이므로 다시 timezone 변환하지 마세요. title/content는 전체 문자열을 유지하세요.
|
||||||
|
활성 공지가 없으면 data:[]이며 null이 아닙니다. 1/2개면 해당 개수만 표시하고 3개를 채우려고 중복하지 마세요. 기존 단일 공지 v1과 홈 응답 스키마는 변경되지 않습니다.
|
||||||
|
|
||||||
|
서버 배포가 확인되기 전에는 개발 환경의 명시적 mock 모드로만 작업하세요. 이 서버 작업은 운영 배포를 수행하지 않습니다. 실제 404/오류를 mock이나 v1로 자동 fallback하지 말고, 배포 후 명시적으로 API 연결을 전환하세요. production mock은 금지합니다.
|
||||||
|
|
||||||
|
공통 envelope의 success를 HTTP 상태와 함께 검사하세요. 성공 응답에는 message:null과 errorProperty:null이 생략 없이 포함됩니다(실제 HTTP 검증 완료). 기존 정책상 HTTP 200에도 success:false가 올 수 있습니다. 조회 실패를 정상 빈 목록으로 숨기지 말고 기존 화면의 오류 처리와 로딩 상태를 유지하세요. 보안/HTTP 오류가 모두 동일한 envelope라고 가정하지 마세요.
|
||||||
|
|
||||||
|
0/1/2/3개 fixture를 각각 테스트하세요. 0개일 때 기존 빈 상태, 1/2개일 때 중복 없음, 3개일 때 id 역순 유지, 상세 이동 시 동일 id/전체 본문 보존을 확인하세요. 긴 제목·여러 줄 본문·KST 날짜 경계도 기존 UI 안에서 확인하세요.
|
||||||
|
범위 밖: 캐러셀, 자동 fallback, 캐싱, 페이지네이션, 홈 응답 변경, 기존 내비게이션 변경.
|
||||||
|
계약: docs/20261028_최근공지사항_v2/api-contract.md
|
||||||
|
```
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 최근 공지사항 v2 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | 구현·HTTP 15개·패키징·lint 검증 완료, 전체 회귀 시간 초과로 최종 Gate 미완료, 미배포 |
|
||||||
|
| 작성일 | 2026-10-28 |
|
||||||
|
| 결정권자 | 요청 사용자 |
|
||||||
|
| 계약 | [api-contract.md](api-contract.md) |
|
||||||
|
| 구현 계획 | [plan-task.md](plan-task.md) |
|
||||||
|
| 클라이언트 전달 | [client-prompt.md](client-prompt.md) |
|
||||||
|
|
||||||
|
## 목표와 문제
|
||||||
|
|
||||||
|
기존 `/notice/latest`는 공지 하나 또는 null을 반환한다. 이 계약과 홈 응답을 유지하면서 클라이언트가 최근 활성 공지 최대 3개를 익명으로 조회할 수 있는 별도 API를 제공한다. 최신은 작성 시각이 아니라 기존 공지 정책인 id 내림차순이다.
|
||||||
|
|
||||||
|
## 확정 요구사항과 수용 기준
|
||||||
|
|
||||||
|
| ID | 요구사항 | 수용 기준 | 추적 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| NOTICE-001 | 익명 `GET /api/v2/notices/latest` | HTTP 200, `ApiResponse<List<NoticeItem>>` | 계약 §1, P1-T1 |
|
||||||
|
| NOTICE-002 | 활성 공지 중 id 내림차순 최대 3개 | 활성 4개 이상과 더 큰 비활성 id fixture에서 정확한 3개 반환, createdAt 정렬 금지 | 계약 §2, P1-T1 |
|
||||||
|
| NOTICE-003 | 기존 필드와 날짜 보존 | id/title/content/date 전체 일치, 본문 생략 없음, UTC→Asia/Seoul `yyyy-MM-dd` 날짜 경계 검증 | 계약 §2, P1-T1 |
|
||||||
|
| NOTICE-004 | 적은 데이터도 배열 | 0/1/2개 및 전부 비활성 시 각각 0/1/2개와 빈 배열 | 계약 §3, P1-T1 |
|
||||||
|
| NOTICE-005 | 기존 동작·권한 보존 | v1 객체/null 유지, 익명 POST와 인접 보호 경로는 401 | 계약 §4, P1-T1 |
|
||||||
|
|
||||||
|
## 구현 제약과 제외
|
||||||
|
|
||||||
|
- Kotlin 1.6.21, Java 17, Spring Boot 2.7.14 및 기존 의존성을 사용한다.
|
||||||
|
- 신규 v2 controller와 읽기 전용 service가 기존 `ServiceServiceNoticeRepository.getNoticeList(PageRequest.of(0, 3))`를 호출한다. 기존 `NoticeItem`과 `ApiResponse`를 재사용한다.
|
||||||
|
- 공개 보안 허용은 정확히 새 GET 한 경로만 추가한다. 공지 본문은 기존 활성 공지의 공개 데이터이며 새 사용자·인증 데이터는 취급하지 않는다.
|
||||||
|
- v1 공지 함수, 홈 응답, 기존 repository 쿼리, DB 스키마, 공통 JSON·오류 정책과 의존성은 변경하지 않는다.
|
||||||
|
- UI·캐러셀·자동 fallback·캐시·페이지네이션·신규 DTO·port·범용 facade·배포는 제외한다.
|
||||||
|
|
||||||
|
## 검증과 배포 상태
|
||||||
|
|
||||||
|
실제 SecurityConfig, RANDOM_PORT HTTP 서버, TestRestTemplate, 실제 repository, 로컬 H2와 embedded Redis로 통합 검증한다. 새 구현 클래스 참조 없이 HTTP로 먼저 실패를 확인한다. H2는 기존 MySQL 날짜 함수 alias로 쿼리를 실행하며 운영 MySQL 실검증은 별도다.
|
||||||
|
|
||||||
|
공지 HTTP 11개와 기존 배포 엔드포인트 HTTP 4개가 통과했고, `bootJar`와 전체 `ktlintCheck`가 성공했다. 전체 `build --continue`는 test 단계에서 20분 제한으로 종료되어 전체 회귀는 미검증이다. 최종 Gate 상태와 독립 리뷰는 구현 계획에 기록한다. 구현·로컬 검증은 배포 완료를 뜻하지 않으며 클라이언트는 배포 확인 전 명시적 mock을 사용한다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 결정 | 근거 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2026-10-28 | NOTICE-D1 | 기존 목록 쿼리를 page 0/size 3으로 호출 | 기존 쿼리에 활성 필터·id 역순·offset/limit·날짜 projection이 존재 |
|
||||||
|
| 2026-10-28 | NOTICE-D2 | 기존 v1과 홈은 전혀 수정하지 않음 | 사용자 승인 범위 및 객체/null 호환성 |
|
||||||
|
| 2026-10-28 | NOTICE-D3 | 전체 Gate는 부모 실행자가 수행 | 명시적 작업 분담, 이 작업은 focused HTTP 검증까지만 수행 |
|
||||||
Reference in New Issue
Block a user