# 평가 분석 — 효용성 및 유용성 (ERD / RESTful / OpenAPI)
분석 기준: `origin/dev` (HEAD = `cb41e36b`, 2026-05-25 기준)
대상 디렉토리: `src/main/java/com/groute/groute_server/**`, `src/main/resources/db/migration/**`
> 본 문서는 평가 기준의 3개 소분류에 대해 각 항목의 만족도/근거를 상단에 요약하고, 하단에 자세한 분석과 실행 가능한 To-do를 정리한다.
---
## 0. 요약 (Executive Summary)
| # | 소분류 | 만족도 | 한 줄 근거 |
|---|--------|--------|-----------|
| 1 | ERD가 비즈니스 로직을 잘 반영하여 올바르게 설계되었는가 | **충족** | 핵심 도메인(User-Scrum-StarRecord-Report)의 관계·상태머신·소프트삭제·집계 테이블 설계가 비즈니스 규칙을 반영. 다만 Scrum↔ScrumTitle 교차 사용자 검증 미흡, 일부 부분 인덱스/CHECK 미세조정 여지. |
| 2 | API가 RESTful 원칙에 맞게 잘 설계 되었는가 | **충족** | 리소스 중심 URI, 적절한 HTTP 메서드·상태코드, 401/403/404 명확 구분, 공통 응답 포맷. 단 DELETE가 일괄 200(204 미사용), PATCH 전체 교체 시맨틱 혼재, 일부 action-style URI(`/scrums/write`) 잔존. |
| 3 | API 명세서가 OpenAPI(Swagger)로 문서화되어 있으며, 응답 형식과 예제 데이터가 포함되어 있는가 | **우수** | SpringDoc 2.8.16, 모든 컨트롤러에 `@Tag`/`@Operation`, 대부분 엔드포인트에 `@ApiResponses`(다중 코드)·`@Schema(example=…)` 적용, JWT BearerAuth 정의, `GroupedOpenApi` 6개 분리. 보강 여지는 `@Parameter` 적용률과 polymorphic 응답(MINI/CAREER) oneOf 명세. |
---
## 1. ERD 평가
### 1.1 만족도: **충족**
핵심 도메인의 관계, 상태 기계, 소프트 삭제, 집계 테이블 설계가 비즈니스 규칙을 합리적으로 반영. 마이그레이션 일관성·교차 사용자 검증·부분 인덱스 등 운영 안정성 측면에서 보강 여지가 있다.
### 1.2 근거 (잘된 점)
- **계층 구조의 명확한 cardinality**
- `Scrum.user`(N:1) + `Scrum.title`(N:1) 이중 FK로 일자/제목 양쪽 조회 경로 최적화. `Scrum.java:32-38`
- `StarRecord.scrum` (1:1, `UNIQUE`)로 "스크럼 1개당 STAR 1개" 비즈니스 규칙을 DB 제약으로 강제. `StarRecord.java:36-39`, `V1__init_schema.sql:152-157`
- **상태 머신을 컬럼·CHECK로 표현**
- `StarRecord.status ∈ (WRITING, WRITTEN, TAGGED)` CHECK 제약과 엔티티 `complete()`/`tag()`/`saveStep()` 메서드로 단계 전이 캡슐화. `StarRecord.java:53-126`, `V20260511110000__add_star_record_status.sql`
- `ScrumTitle.status ∈ (PENDING, COMMITTED)` 도입으로 "임시 작성 vs STAR 확정" 구분 강제. `ScrumTitle.java:50-58`, `V20260509100000__add_scrum_title_status.sql`
- `AiTaggingJob.status ∈ (QUEUED, RUNNING, SUCCESS, FAILED)` + `retryCount` 이중 관리로 워커 폴링/재시도 모델 명확. `AiTaggingJob.java:37-100`, `V1__init_schema.sql:203-220`
- **소프트 삭제의 일관성 있는 적용 범위**
- `SoftDeleteEntity` 상속: `User`, `Scrum`, `ScrumTitle`, `StarRecord`, `Project` (회원 탈퇴 grace period·복구 가능 데이터). `SoftDeleteEntity.java:1-33`
- 비-상속: `StarTag`, `StarImage`, `Report`, 인증 엔티티 → 통계/감사용은 hard delete 차단 의도와 부합.
- **집계 테이블의 합리적 비정규화**
- `DailyCompetencyStat` `(user_id, stat_date) UNIQUE` + JSONB `categoryCounts` → 홈 잔디(HOM-002)/레이더 3개월 조회 1쿼리화. `DailyCompetencyStat.java:25-60`, `V1__init_schema.sql:230-247`
- `Project.titleCount`, `ScrumTitle.scrumCount` 카운터 비정규화로 "N회 사용" 뱃지·삭제 가능 여부 즉시 판정.
- **법적/감사 추적 모델**
- `UserTermAgreement` append-only 설계(`agreed_at`, `ip_address`, `user_agent` 스냅샷). `UserTermAgreement.java:1-53`, `V1__init_schema.sql:315-334`
- **타임존 일관성 (기억과 일치)**
- `BaseTimeEntity` 의 `createdAt/updatedAt`는 UTC(TIMESTAMPTZ), 도메인 비교는 KST `LocalDate` 사용 (`Scrum.scrumDate`, `User.lastRecordDate`).
- **핵심 조회 경로를 커버하는 인덱스**
- `idx_scrums_user_scrum_date` (캘린더 일자별 조회)
- `idx_star_records_user_completed_at` (리포트 10개 단위 임계치 판정)
- `idx_ai_tagging_jobs_status_created_at` (워커 FIFO 폴링)
- `idx_daily_competency_stats_user_stat_date` (홈 잔디 3개월)
- 근거: `V1__init_schema.sql:141-247`
- **도메인 행위 캡슐화**
- `User.recordOnDate(LocalDate)`로 streak 갱신 로직 일원화. `User.java:195-270`
- `Report` 의 상태 전이가 도메인 메서드에 모여 있어 컨트롤러/서비스에서 직접 상태 필드를 만지지 않음.
### 1.3 결함 / 우려 (검증 완료)
| # | 항목 | 파일:line | 비즈니스 영향 | 심각도 |
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --- |
| E1 | **Scrum-ScrumTitle 간 user_id 교차 검증이 도메인·DB 어디에도 없음** — `Scrum.create(User, ScrumTitle, …)` 호출 시 `user.id != title.user.id` 검증 없음. 컨트롤러 권한 검사가 깨지면 타 사용자의 ScrumTitle에 Scrum 부착 가능. | `Scrum.java:60-68`, `V1__init_schema.sql:120-144` | 멀티테넌시 경계 침해 가능성(취약점 발생 시 폭발 반경 큼) | 중 |
| E2 | **ScrumTitle.status 기본값 비대칭** — JPA 엔티티 default=`PENDING`, DB DEFAULT=`'COMMITTED'`. JPA 영속화 경로에서는 PENDING이 명시되어 정상이나, **기존 row 백필**과 **JPA 외 INSERT 경로**(native, raw seed SQL 등)에서 COMMITTED로 들어가 PENDING→COMMITTED 전이 의도와 불일치. | `ScrumTitle.java:50-53`, `V20260509100000__add_scrum_title_status.sql` | 시드/마이그레이션 시점 데이터가 의도와 다른 상태로 머무를 수 있음 | 중 |
| E3 | **`is_completed` DROP 후 재추가 마이그레이션 — 의도는 합리적이나 히스토리 복잡** — V20260511에서 DROP, V20260512에서 다시 ADD(BOOLEAN 인덱싱 성능). `status`와 동기화 책임이 엔티티 메서드(`complete()`, `tag()`)에만 존재하며 DB 트리거/CHECK로 강제되지 않음. | `V20260511110000__add_star_record_status.sql`, `V20260512110000__add_star_record_is_completed.sql`, `StarRecord.java:58-69` | 향후 다른 진입점(배치/SQL)에서 동기화 누락 시 통계 오차 | 중 |
| E4 | **`Project (user_id, name, is_deleted)` UNIQUE는 부분 인덱스로 표현해야 의도가 분명** — 현재 일반 UNIQUE 제약이라 `is_deleted=true`였던 동일 이름이 다시 활성화될 때 충돌 케이스 처리가 코드/제약에 분산. | `V1__init_schema.sql:100`, `Project.java` | 동일 이름 재사용 시 경합/오류 케이스 모호 | 하 |
| E5 | **`AiTaggingJob.requestPayload/responsePayload` JSONB 스키마 미문서화** — 코멘트만 "직군 + 선택 역량 + S-T-A-R 텍스트". 모델 변경 시 역호환성 검증 어려움. | `AiTaggingJob.java:45-53` | AI 프롬프트 스키마 변경 시 회귀 위험 | 중 |
| E6 | **`Report.selectedStarCount`, `selectedStarRecordIds` nullable 정책 미확정** — 마이그레이션에 "백필 정책 확정 후 NOT NULL" 주석이 남아 있음. MINI/CAREER 분기에 따라 의미가 달라 클라이언트가 null 분기를 항상 처리해야 함. | `Report.java:54-61`, `V20260518130000…`, `V20260518140000…` | 클라이언트 분기 부담, 추후 NOT NULL 전환 시 백필 필요 | 하 |
| E7 | **`Project.name`/`ScrumTitle.freeText` 길이 제약이 글자 수가 아닌 바이트 기준 VARCHAR** — UTF-8에서 한글 1글자=3바이트, VARCHAR(15)/VARCHAR(20)이 "한글 5/6자"를 의미하지 않음. 클라이언트 검증·DB 컬럼 길이 의미가 어긋남. | `Project.java:36-38`, `ScrumTitle.java:41-43`, `V1__init_schema.sql:91, 107` | 입력 검증과 DB 제약의 의미 불일치 (사용자에게 잘림/오류 노출 가능) | 하 |
| E8 | **`is_completed` 보조 인덱스(`idx_star_records_user_is_completed`) 부재** — V20260511에서 DROP 후 V20260512에서 컬럼만 재추가, 인덱스는 복원되지 않았음. 리포트 임계치 판정이 `completed_at` 기반이라 즉시 문제는 없으나, 컬럼이 인덱스 없이 매번 풀스캔 보조 조건이 됨. | `V20260511110000__add_star_record_status.sql:9`, `V20260512110000__add_star_record_is_completed.sql` | 향후 `is_completed` 조건 단독 쿼리 시 성능 저하 가능 | 하 |
> 검증 노트: ERD 1차 스캔에서 "`notification_copy_index` 컬럼 DB 미정의"로 보고됐으나, `V20260505233236__add_users_notification_copy_index.sql`에 `ADD COLUMN notification_copy_index SMALLINT NOT NULL DEFAULT 0`로 정상 추가되어 있음 — **결함 아님, 정정함**.
### 1.4 남은 To-do (action 단위)
- [ ] **E1 — Scrum.create() 교차 사용자 검증 추가**
- 파일: `src/main/java/com/groute/groute_server/record/domain/Scrum.java` `create(...)`
- 변경: `if (!user.getId().equals(title.getUser().getId())) throw new BusinessException(ErrorCode.RECORD_xxx_OWNER_MISMATCH);`
- 함께: `StarRecord.create(...)` 도 `Scrum.user`와 호출자 user 동일성 가드 추가
- 단위 테스트: `ScrumServiceTest`에 `should_throw_when_title_belongs_to_other_user` 케이스 추가
- [ ] **E2 — ScrumTitle.status DEFAULT를 엔티티와 일치시키기**
- 신규 마이그레이션 `V20260525__align_scrum_title_status_default.sql`:
```sql
ALTER TABLE scrum_titles ALTER COLUMN status SET DEFAULT 'PENDING';
```
- 기존 백필 row의 상태가 운영상 PENDING이 맞는지 데이터 점검 후 진행 (운영 DDL 영향 검토 필요)
- [ ] **E3 — status↔is_completed 동기화를 DB 레벨로 강제 (선택)**
- 옵션 A: BEFORE INSERT/UPDATE 트리거로 `is_completed = (status = 'TAGGED')` 자동 갱신
- 옵션 B: 컴퓨트 컬럼 / 인덱스로 대체 (`CREATE INDEX … ON star_records (user_id) WHERE status = 'TAGGED'`)
- 권장: 옵션 B 부분 인덱스 + 엔티티에 `@Formula` 또는 derived getter로 `isCompleted` 노출, 컬럼 자체 제거 (장기 과제)
- [ ] **E4 — `projects (user_id, name)` 부분 UNIQUE 인덱스로 전환**
- 신규 마이그레이션:
```sql
ALTER TABLE projects DROP CONSTRAINT uq_projects_user_name_is_deleted;
CREATE UNIQUE INDEX uq_projects_user_name_active
ON projects (user_id, name) WHERE is_deleted = FALSE;
```
- [ ] **E5 — AiTaggingJob JSON 페이로드 스키마 문서화**
- `AiTaggingJob.java` 의 `requestPayload`/`responsePayload` Javadoc에 정확한 JSON 키 명세 추가 (예: `jobRole`, `competencyCategory`, `situationTask`, `action`, `result`)
- 동시에 `record/adapter/out/ai/dto/` 의 DTO와 일치성 점검 (오프셋된 키가 있다면 수정)
- [ ] **E6 — Report selected 필드 NOT NULL 전환 로드맵 수립**
- 단계 1: 모든 신규 INSERT 경로에서 두 컬럼을 명시적으로 채우는지 통합 테스트 추가
- 단계 2: 레거시 row 백필 (MINI=0/빈 배열, CAREER=실제 선택분)
- 단계 3: 마이그레이션으로 NOT NULL DEFAULT 부여
- [ ] **E7 — 길이 제약을 글자 수 검증으로 명시**
- `@Size(max=15)`는 코드포인트 길이가 아닌 `CharSequence.length()` — 한글에는 적절하나 이모지/서로게이트 페어 고려 필요
- validator에 `Character.codePointCount(...)` 또는 별도 `@CharLength` 어노테이션 도입 검토
- Swagger description에 "한글/영문/숫자 N자 (이모지·서로게이트 미포함)"로 명시
- [ ] **E8 — `is_completed` 부분 인덱스 복원 (필요 시)**
- 사용 쿼리가 있다면:
```sql
CREATE INDEX idx_star_records_user_is_completed
ON star_records (user_id) WHERE is_completed = TRUE;
```
- 사용처가 모두 `completed_at` 기반이라면 컬럼 자체 폐기 검토(E3과 연계)
---
## 2. RESTful API 평가
### 2.1 만족도: **충족**
리소스 중심 URI, HTTP 메서드 시맨틱, 상태 코드 분기(401/403/404/409)가 대체로 표준을 따른다. 다만 모든 DELETE가 200을 반환(204 미사용), 일부 action-style URI(`/scrums/write`), PATCH가 사실상 전체 교체로 쓰이는 케이스 등 정합성 보강 포인트가 있다.
### 2.2 엔드포인트 인벤토리
> 도메인별 그룹화. 총 45개 엔드포인트(8개 컨트롤러 + record/report adapter 컨트롤러).
#### Auth
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| POST | `/api/auth/reissue` | 200 / 400 / 401 | 액세스 토큰 재발급 |
| POST | `/api/auth/logout` | 200 / 401 | 로그아웃 |
| POST | `/api/users/me/device-tokens` | 200 / 400 / 401 / 404 | 푸시 토큰 등록 (DeviceTokenController) |
#### User / Onboarding / Notification
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| GET | `/api/users/me` | 200 / 401 / 404 | 내 프로필 |
| PATCH | `/api/users/me` | 200 / 400 / 401 / 404 | 프로필 수정 |
| DELETE | `/api/users/me` | 200 / 401 / 404 | 회원 탈퇴 |
| GET | `/api/users/me/notification-settings` | 200 / 401 | 알림 설정 조회 |
| PATCH | `/api/users/me/notification-settings` | 200 / 400 / 401 / 404 | 알림 설정 (전체 교체) |
| GET | `/api/onboarding/status` | 200 / 401 | 온보딩 상태 |
| POST | `/api/onboarding/complete` | 200 / 400 / 401 / 404 / 409 | 온보딩 완료 |
#### Home / Calendar
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| GET | `/api/home/radar` | 200 / 401 | 레이더 차트 |
| GET | `/api/home/branding` | 200 / 401 / 404 | 직무 브랜딩 문구 |
| GET | `/api/home/competency-stats` | 200 / 400 / 401 | 월별 역량 잔디 |
| GET | `/api/calendar/monthly` | 200 / 400 / 401 | 월별 캘린더 |
| GET | `/api/calendar/daily-preview` | 200 / 400 / 401 | 날짜 프리뷰 |
| GET | `/api/calendar/daily` | 200 / 400 / 401 | 일자별 스크럼 |
#### Project
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| POST | `/api/projects` | 201 / 400 / 409 | 프로젝트 태그 생성 |
| GET | `/api/projects` | 200 / 400 | 목록(페이지네이션) |
| PATCH | `/api/projects/{projectId}` | 200 / 400 / 404 / 409 | 이름 수정 |
| DELETE | `/api/projects/{projectId}` | 200 / 404 / 409 | 삭제 |
#### Scrum
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| POST | `/api/scrums/write` | 200 / 400 / 401 / 404 | 일괄 저장 |
| PATCH | `/api/scrums/competencies` | 200 / 400 / 401 / 404 | 역량 선택 |
| PUT | `/api/scrums/daily` | 200 / 400 / 401 / 404 / 409 | 일자별 동기화 |
| DELETE | `/api/scrums/{scrumId}` | 200 / 401 / 404 | 단건 삭제 |
| DELETE | `/api/scrums/titles/{titleId}` | 200 / 401 / 404 | 제목 단위 삭제 |
#### Star Record / Image / AI Tagging
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| GET | `/api/star-records/home-summary` | 200 / 401 | 홈 복귀 요약 |
| POST | `/api/star-records/bulk` | 200 / 400 / 404 / 409 | 일괄 생성 |
| POST | `/api/star-records/{id}/steps/{step}` | 200 / 400 / 401 / 403 / 404 | 단계별 저장 |
| GET | `/api/star-records/{id}` | 200 / 401 / 403 / 404 | 상세 |
| DELETE | `/api/star-records/{id}` | 200 / 401 / 403 / 404 | 삭제 |
| GET | `/api/star-records/{id}/images` | 200 / 401 / 403 / 404 | 이미지 목록 |
| POST | `/api/star-records/{id}/images/presigned-url` | 200 / 400 / 401 / 403 / 404 / 409 | S3 presigned URL |
| POST | `/api/star-records/{id}/images/confirm` | 200 / 400 / 401 / 403 / 404 / 409 | 업로드 확인 |
| DELETE | `/api/star-records/{id}/images/{imageId}` | 200 / 401 / 403 / 404 / 409 | 이미지 삭제 |
| POST | `/api/star-records/{id}/ai-tagging` | 201 / 400 / 401 / 403 / 404 / 409 | AI 태깅 트리거 |
| GET | `/api/star-records/{id}/ai-tagging/status` | 200 / 401 / 403 / 404 | 상태 폴링 |
| GET | `/api/star-records/{id}/ai-tagging/result` | 200 / 400 / 401 / 403 / 404 | 결과 |
#### Report
| HTTP | URI | Status | 설명 |
|------|-----|--------|------|
| GET | `/api/reports/gauge` | 200 / 401 | 게이지 |
| GET | `/api/reports` | 200 / 401 | 목록 |
| GET | `/api/reports/{reportId}` | 200 / 401 / 403 / 404 | 상세 |
| GET | `/api/reports/selectable-info` | 200 / 401 | 생성 사전 정보 |
| POST | `/api/reports` | 200 / 400 / 401 / 409 | 생성 |
| GET | `/api/reports/{reportId}/status` | 200 / 401 / 403 / 404 | 생성 상태 |
| POST | `/api/reports/{reportId}/retry` | 200 / 400 / 401 / 403 / 404 | 재시도 |
| GET | `/api/reports/selectable-records/{date}` | 200 / 401 | 날짜별 선택 가능 STAR |
### 2.3 근거 (잘된 점)
- **공통 응답 포맷의 일관성**: `ApiResponse<T>`(success/code/message/data) + `@JsonInclude.NON_NULL`. `common/response/ApiResponse.java`
- **에러 처리의 표준화**: `GlobalExceptionHandler`가 `BusinessException`, validation 실패, 404, 409, 보안 예외를 표준 `ErrorResponse`로 변환. `common/exception/GlobalExceptionHandler.java:37-123`
- **상태 코드의 정밀한 분기**
- 401(미인증) / 403(권한 없음) / 404(없음) 명확 구분 — STAR/Report 도메인에서 일관되게 적용
- 409(중복/충돌) 사용: `POST /api/projects`, `POST /api/star-records/bulk`, `PUT /api/scrums/daily`, presigned-url 발급 등
- **컬렉션/단건 자원 모델**: `/api/projects`(컬렉션) ↔ `/api/projects/{id}`(단건), `/api/star-records/{id}/images` 계층 자원
- **`POST → 201 Created` 정확 사용**: `ProjectController.java:55`, `StarTaggingController` AI 태깅 트리거
- **로그인 사용자 표현**: `/api/users/me` 패턴으로 토큰 식별, ID 노출 회피
- **인증 추상화**: `@CurrentUser` argument resolver로 컨트롤러가 인증 컨텍스트에 비결합. `common/resolver/`
- **이미지 업로드를 3단계로 분리**: presigned-url → 클라이언트 직접 S3 PUT → confirm. 서버 메모리 절약, RESTful 자원 관점 유지
- **HTTP 메서드 시맨틱**
- `PUT /api/scrums/daily`: 일자별 멱등 동기화에 PUT 채택 (전체 교체 시맨틱과 부합)
- `PATCH /api/scrums/competencies`: 부분 업데이트
- `DELETE`는 모두 멱등 (재호출 시 같은 결과)
### 2.4 결함 / 우려
| # | 항목 | 파일:line | 위반 내용 | 심각도 |
|---|------|-----------|----------|--------|
| R1 | **모든 DELETE 가 200 + ApiResponse 본문 반환 (204 No Content 미사용)** | `ScrumController.java:148`, `:171`, `StarRecordController.java:181`, `ProjectController.java:117`, `StarImageController.java:158`, `UserController.java:109` | DELETE 후 반환할 표현이 없는 케이스에서는 204가 표준. 메시지만 들어있는 ApiResponse 본문은 RFC 7231 §4.3.5 권장 안과 어긋남 | 중 |
| R2 | **PATCH 가 사실상 "전체 교체"로 쓰이는 곳 존재** | `NotificationSettingController.java:70-80` ( `PATCH /api/users/me/notification-settings`, 기존 슬롯 전체 삭제 후 재삽입) | 부분 수정 시맨틱(PATCH)과 실제 동작(전체 교체)이 불일치 → PUT이 더 정확 | 중 |
| R3 | **Action-style URI 잔존** | `ScrumController.java:67` (`POST /api/scrums/write`) | `POST /api/scrums` 또는 `POST /api/scrums:bulk` 가 더 RESTful. 현재는 `bulk` 의미가 URI에 명시되지 않아 의도 추론 필요 | 하 |
| R4 | **`POST /api/onboarding/complete` 가 계층적 자원이 아님** | `OnboardingController.java:69-82` | 사용자의 자원 상태이므로 `PUT /api/users/me/onboarding` 또는 `POST /api/users/me/onboarding` 가 자원 모델에 더 부합 | 하 |
| R5 | **`POST /api/reports/{id}/retry`, `POST /api/scrums/write` 등 액션 엔드포인트의 상태 코드 통일성** | `ReportController.java`, `ScrumController.java` | retry는 새 작업 트리거에 가까우니 202 Accepted (비동기 처리)도 후보. 현재는 200 일괄 사용 | 하 |
| R6 | **`POST /api/star-records/bulk` 가 200 반환 (201 후보)** | `StarRecordController.java` 의 bulk 생성 | 신규 자원 생성에는 201이 표준. POST → 200으로 처리 시 자원 위치(Location 헤더) 누락 | 하 |
| R7 | **콘텐츠 협상 헤더 비명시** | 전 컨트롤러 | `produces = MediaType.APPLICATION_JSON_VALUE` 등을 컨트롤러 레벨에 명시하면 OpenAPI/클라이언트 정확도 향상 | 하 |
| R8 | **페이지네이션 응답 메타 표준화 부재** | `ProjectController.java:75-` | 현재 `page`/`size` 입력은 있으나 응답에 `totalElements`/`hasNext` 등 표준 메타가 일관되게 들어있는지 점검 필요 | 하 |
| R9 | **에러 응답에 `traceId`/`timestamp` 없는 경우** | `common/response/ErrorResponse.java` | 운영 트러블슈팅 시 `traceId`가 있으면 로그-응답 매칭 용이 | 하 |
### 2.5 남은 To-do (action 단위)
- [ ] **R1 — DELETE 6건 → 204 No Content 전환**
- 대상: `ScrumController#deleteScrum`, `#deleteTitle`, `StarRecordController#deleteStarRecord`, `ProjectController#deleteProject`, `StarImageController#deleteImage`, `UserController#deleteMe`
- 변경 패턴:
```java
@DeleteMapping("/{scrumId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteScrum(...) { useCase.delete(...); }
```
- Swagger `@ApiResponses` 의 200 → 204로 갱신
- 단, "삭제와 함께 부수효과를 보여주고 싶을 때(예: 부모 자원의 갱신된 상태 반환)"는 200 유지 가능 → 케이스별 컨펌 필요
- [ ] **R2 — Notification 설정 PUT으로 변경**
- `PATCH /api/users/me/notification-settings` → `PUT /api/users/me/notification-settings`
- 메서드 시그니처에 `PUT` 매핑 추가, PATCH 폐기 일정 잡기 (deprecation 헤더 1주, 이후 제거)
- 또는 PATCH 유지하되 description에 "기존 모든 슬롯을 교체합니다" 명시
- [ ] **R3 — `POST /api/scrums/write` 리네이밍 (장기)**
- 후보: `POST /api/scrums` (단건 처리 + 배열 입력 허용)
- 클라이언트 호환성 위해 deprecate 헤더 + 임시 alias 라우팅 1~2 sprint
- [ ] **R4 — Onboarding 자원 모델 정렬 (장기, 클라이언트 변경 필요)**
- 신규 URI 후보: `GET/PUT /api/users/me/onboarding` 로 통합
- [ ] **R5 — Report retry/생성에 비동기 시맨틱 명확화**
- 동기 응답이 빠르게 끝나면 200 유지
- AI 호출 등 비동기 큐 적재 후 즉시 반환이면 202 Accepted + 상태 폴링 URL(`Location` 헤더)로 명세
- [ ] **R6 — `POST /api/star-records/bulk` → 201 + `Location: /api/star-records/{firstId}` 또는 응답에 생성 자원 목록 명시**
- [ ] **R7 — 컨트롤러 어노테이션에 `produces = APPLICATION_JSON_VALUE` 일괄 추가**
- `@RestController @RequestMapping(value="/api/…", produces=MediaType.APPLICATION_JSON_VALUE)`
- [ ] **R8 — 페이지네이션 응답 메타 표준화**
- `PagedResponse<T>` 공통 DTO 도입(`content`, `page`, `size`, `totalElements`, `hasNext`)
- Project/Report 목록 응답에 적용
- [ ] **R9 — ErrorResponse 에 `traceId`, `timestamp(UTC ISO-8601)` 필드 추가**
- `GlobalExceptionHandler` 에서 MDC/현재 시각을 기반으로 채움
- [ ] **(보강) HEAD/OPTIONS 대응** — 필요한 자원은 `HEAD` 매핑으로 메타데이터만 반환 (선택). 일반적으로 미구현이어도 큰 감점은 없으나, CORS preflight 인접 처리 일관성 확인
---
## 3. OpenAPI(Swagger) 문서화 평가
### 3.1 만족도: **우수**
`org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16`(build.gradle:33) 기반으로 전 컨트롤러에 `@Tag`/`@Operation`/`@ApiResponses` 적용, 대부분 DTO에 `@Schema(description=…, example=…)` 적용. `SwaggerConfig`에 JWT Bearer 보안 스키마 정의·환경별 서버 URL·도메인별 `GroupedOpenApi` 6개 구성. 보강 여지는 `@Parameter` 적용률과 polymorphic 응답(MINI/CAREER) oneOf 명세 정도.
### 3.2 정량 지표 (조사 기준)
| 지표 | 값 | 메모 |
|------|----|------|
| 컨트롤러 수 | 17개 (auth/user/calendar/home/onboarding/notification/device-token + record/report adapter) | `@Tag` 100% 적용 |
| 엔드포인트 수 | 45개 | `@Operation` 100% 적용 |
| `@ApiResponses` (다중 코드) 사용 | 거의 모든 보호된 엔드포인트 | 코드별 description 일관 |
| `@Schema` 적용 DTO | 약 36/49개 (≈73%) | Response DTO는 거의 전수 적용, 일부 Request DTO 필드 누락 가능 |
| `example` 속성 포함 필드 | 120+ | enum/날짜/문자/숫자/배열 전반 |
| `@Parameter` 적용 | 일부 (`HomeCompetencyStatsController` 외 산발적) | 보강 필요 |
| `SecurityScheme` | 1 (`bearerAuth`, HTTP Bearer/JWT) | Swagger UI Authorize 작동 |
| `GroupedOpenApi` 그룹 | 6 (auth/calendar/home/record/report/user) | 도메인별 탐색 용이 |
| 한글 description 일관성 | 100% | 모든 summary/description 한글 |
| 환경별 서버 URL | local/stg/prod 3개 | `SwaggerConfig` |
### 3.3 근거 (잘된 점)
- **설정 클래스의 완성도**: `common/config/SwaggerConfig.java` — `Info`(title/version/description), `SecurityScheme(JWT)`, `GroupedOpenApi` 6개, 환경별 `Server` URL 정의. `application.yaml:110-121`에서 `try-it-out`/`deep-linking`/`default-models-expand-depth` 활성화.
- **에러 코드 카탈로그 자동 노출**: `common/docs/ErrorCodeDocsController.java`가 `ErrorCode` enum을 HTML로 렌더링 → `/docs/error-code` 페이지 제공. Swagger와 상호 보완.
- **Response 래퍼 자체에 `@Schema`** 가 적용되어 모든 API 응답의 공통 필드(success/code/message/data) 의미가 문서에서 일관되게 보임 (`common/response/ApiResponse.java`).
- **예제 데이터의 도메인 친화성**: `"겨레"`, `"밋업 프로젝트"`, `"2026-05-25"`, `"PLANNING_EXECUTION"` 등 실제 데이터와 유사 → 클라이언트 개발자 가독성 우수.
- **권한·인증 응답 코드의 일관 명시**: 보호된 엔드포인트에 401/403 명시(특히 STAR/Report 도메인).
### 3.4 결함 / 우려
| # | 항목 | 파일:line | 내용 | 심각도 |
|---|------|-----------|------|--------|
| S1 | **`@Parameter` 미적용 컨트롤러 다수** — 쿼리/경로 파라미터의 description/example/required 가 명시되지 않은 곳이 많음 | `CalendarHomeController.java:54`, `CalendarController.java:50`, `ProjectController.java:75-76`, `ReportSelectableRecordsController.java:48` | Swagger UI 에서 파라미터 의미가 자바 타입에만 의존 | 중 |
| S2 | **Polymorphic 응답 명세 미흡** — `ReportDetailResponse.content` 가 MINI/CAREER 분기로 구조가 다르나 `oneOf` 명시되지 않음 | `report/adapter/in/web/dto/ReportDetailResponse.java` (정확 라인 점검 필요) | Swagger 모델이 실제 응답을 정확히 표현하지 못함 | 중 |
| S3 | **POST 응답 코드의 201/200 혼재** | `StarRecordController#bulk` (200), `ProjectController#create` (201), `StarTaggingController#trigger` (201) | 생성=201 원칙 일관 적용 권장 (R6와 짝) | 하 |
| S4 | **일부 단일 `@ApiResponse` 만 있는 엔드포인트** | 예: `OnboardingController.java:42` getStatus는 200만 명시 | 401 등 표준 에러 코드 추가 필요 | 하 |
| S5 | **JWT 인증을 모든 보호 엔드포인트에 `@SecurityRequirement` 로 명시하지 않은 경우** — global default 적용으로 보일 수 있으나, 명시적 표기가 더 안전 | 전 컨트롤러 | Swagger Authorize 미설정 사용자가 401을 받는 원인 파악이 직관적이지 않을 수 있음 | 하 |
| S6 | **`@Schema(requiredMode = ...)` 사용 부족** — Request DTO 필드의 required 여부가 명시되지 않은 곳 일부 | 일부 DTO | 클라이언트 측 검증 코드 생성 시 정확도 저하 | 하 |
| S7 | **예제 데이터가 응답 스키마와 100% 일치하는지 spot-check 필요** — 코드래빗 리뷰 반영(cb41e36)으로 일부 example 추가됨, 그러나 polymorphic/optional 케이스의 example 완성도는 미확인 | 다수 | 가독성 저하 | 하 |
| S8 | **공통 에러 응답이 각 엔드포인트의 `@ApiResponse(content = …)` 에 명시되지 않음** — 에러 코드는 잘 정의돼 있으나, 엔드포인트별 응답 컨텐츠 스키마(ErrorResponse)를 명시한 곳이 적음 | 다수 | Swagger UI 에서 에러 응답 본문 구조 미리보기 불가 | 중 |
### 3.5 남은 To-do (action 단위)
- [ ] **S1 — 모든 쿼리/경로 파라미터에 `@Parameter` 추가**
- 우선순위 컨트롤러:
- `CalendarHomeController.getMonthly` — `@Parameter(description="조회 월(yyyy-MM)", example="2026-05", required=true)` on `month`
- `CalendarController.getDailyPreview/getDaily` — `date` 파라미터에 동일
- `ProjectController.list` — `page`, `size` 에 description/default/min/max
- `ReportSelectableRecordsController.get` — `date` PathVariable 에 추가
- `HomeCompetencyStatsController` — 이미 적용된 패턴 참고
- 일괄 적용 후 `./gradlew spotlessApply && ./gradlew test` 확인
- [ ] **S2 — `ReportDetailResponse.content` polymorphic 모델링**
- 옵션 A: 별도 DTO 분리 (`ReportDetailMiniResponse`, `ReportDetailCareerResponse`) + `@JsonTypeInfo`
- 옵션 B: `@Schema(oneOf = {MiniContent.class, CareerContent.class}, discriminatorProperty = "reportType")`
- 권장: 옵션 A (Swagger UI 표현 가장 깔끔)
- 함께: `ReportController#getDetail` 의 `@ApiResponse(content = @Content(...))` 에 oneOf 명시
- [ ] **S3 — 생성 엔드포인트 일괄 201 통일** (R6과 짝)
- `POST /api/star-records/bulk` → 201
- 동시에 Swagger `@ApiResponse(responseCode="201")` 갱신
- [ ] **S4 — 단일 `@ApiResponse` 엔드포인트에 표준 에러 응답 보강**
- 대상 조사: `grep -n "@ApiResponse(\b" -A2 src/main/java | grep -v "@ApiResponses"` 로 단일 사용 위치 식별
- 각 보호 엔드포인트에 최소 401 추가, 자원 단건 조회면 404 추가
- [ ] **S5 — `@SecurityRequirement(name="bearerAuth")` 를 보호 엔드포인트에 명시**
- 컨트롤러 클래스 레벨에 일괄 추가 (public auth 엔드포인트 제외)
- 추가 시 Swagger UI에서 자물쇠 아이콘이 명확히 표시됨
- [ ] **S6 — Request DTO 필수성 표시**
- `@Schema(requiredMode = Schema.RequiredMode.REQUIRED, example=...)` 일괄 적용
- 동시에 `@NotNull`/`@NotBlank` 와 일치성 점검 (서로 모순되면 클라이언트 혼란)
- [ ] **S7 — 예제 데이터 spot-check**
- polymorphic Response, optional 필드, 에러 응답의 example 표본 10건 수동 검토
- 부적합한 예제 갱신
- [ ] **S8 — 표준 에러 응답 컨텐츠 명시**
- 컨트롤러 어드바이스에 글로벌 응답 정의 추가:
```java
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(implementation = ErrorResponse.class), examples = @ExampleObject(...)))
```
- 보일러플레이트가 많아지면 `@interface StandardErrorResponses` 메타 어노테이션 도입 검토
- [ ] **(보강) `springdoc.api-docs.path`, `swagger-ui.path` 운영 환경 권한 가드 점검** — staging/prod 에서 노출 정책 명확화
---
## 4. 우선순위 종합 To-do (실행 순서 제안)
> 평가 점수 끌어올리기 + 운영 안정성 측면에서 ROI 가 높은 순서.
### Sprint 1 (이번 주 — 빠르게)
1. **R1**: DELETE 6건 204 전환 + Swagger 응답 갱신 (1~2h)
2. **S1**: 쿼리/경로 파라미터 `@Parameter` 일괄 보강 (2~3h)
3. **S4**: 단일 `@ApiResponse` 엔드포인트에 401/404 보강 (1~2h)
4. **E2**: ScrumTitle status DEFAULT 정정 마이그레이션 + 데이터 점검 (1h, 운영 영향 검토 필수)
### Sprint 2 (다음 주)
5. **R2**: NotificationSetting PATCH → PUT 전환 (또는 description 강화) (1h)
6. **R8 + R9**: PagedResponse 표준화 + ErrorResponse traceId/timestamp (2~3h)
7. **S2**: Report polymorphic 응답 분리 + `oneOf` 명세 (2~4h)
8. **E1**: Scrum.create / StarRecord.create 교차 사용자 검증 + 테스트 (2h)
### 장기 (반기 내)
9. **R3/R4**: action-style URI 정규화 + 클라이언트 호환성 deprecate 일정 (sprint 단위)
10. **E3**: status↔is_completed 동기화 DB 강제 또는 컬럼 폐기 (1~2 sprint)
11. **E4**: Project 부분 인덱스 전환 + 코드 호환성 (3h)
12. **E5/E7**: AiTaggingJob JSON 스키마 문서화, 길이 검증 글자수 기반화 (각 2h)
13. **S5/S6/S8**: SecurityRequirement/requiredMode/표준 에러 응답 컨텐츠 일괄 적용 (2~3h)
---
## 5. 참고/검증 노트
- 본 분석은 `origin/dev` HEAD `cb41e36b` 시점에서 수행.
- 1차 자동 스캔에서 보고된 "`notification_copy_index` DB 컬럼 미정의" 는 검증 결과 `V20260505233236__add_users_notification_copy_index.sql` 로 정상 적용된 것으로 확인 → 결함 목록에서 제외.
- `is_completed` DROP 후 ADD 패턴은 의도된 설계(상태머신 + 인덱싱 최적화 BOOLEAN). 다만 동기화 책임이 엔티티 메서드에 집중되어 있어 운영상 잠재 리스크로 표시 (E3).
- 마이그레이션은 Flyway, 17개 파일(`V1__init_schema.sql` + 16개 증분). 전반적으로 증분 일관성·롤백 안전성 양호.
- 평가 점수 측면에서 본 카테고리의 **현재 위치는 "충족 2 + 우수 1"** 로 충분히 경쟁력 있으며, Sprint 1 액션만 적용해도 ERD/REST 가 "우수" 근처로 끌어올라갈 여지가 있음.