From f81991b409aeb8cacf79e5881096ac9a567a583d Mon Sep 17 00:00:00 2001 From: art Date: Mon, 22 Jun 2026 16:59:18 +0900 Subject: [PATCH] =?UTF-8?q?chore(atp):=20work-session=20=EC=82=B0=EC=B6=9C?= =?UTF-8?q?=EB=AC=BC=20=EA=B8=B0=EB=A1=9D=20(3=EA=B0=9C=20=EC=84=B8?= =?UTF-8?q?=EC=85=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit work-session 은 추적 대상(durable history)이나 3개 디렉토리가 untracked 로 남아 있던 것을 소급 기록 — 추적 관행 일관성 복원. - 20260622-162345: 이번 세션(로컬 DDL 적용 + 커밋 정책). - 20260622-092800: 오늘 선행 세션. - 20260618-145152: 6/18 orphan(후속 W3-2 작업에 이미 계승됨). Co-Authored-By: Claude Opus 4.8 (1M context) --- .atp/work-session/20260618-145152/report.md | 162 ++++++ .../artifacts/w3-2-ui-preview.html | 364 ++++++++++++ .../20260622-092800/documentation.md | 41 ++ .../20260622-092800/implementation/design.md | 536 ++++++++++++++++++ .../20260622-092800/implementation/fix-L1.md | 68 +++ .../implementation/ownership.md | 60 ++ .atp/work-session/20260622-092800/report.md | 250 ++++++++ .../20260622-092800/verification.md | 194 +++++++ .atp/work-session/20260622-162345/report.md | 123 ++++ 9 files changed, 1798 insertions(+) create mode 100644 .atp/work-session/20260618-145152/report.md create mode 100644 .atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html create mode 100644 .atp/work-session/20260622-092800/documentation.md create mode 100644 .atp/work-session/20260622-092800/implementation/design.md create mode 100644 .atp/work-session/20260622-092800/implementation/fix-L1.md create mode 100644 .atp/work-session/20260622-092800/implementation/ownership.md create mode 100644 .atp/work-session/20260622-092800/report.md create mode 100644 .atp/work-session/20260622-092800/verification.md create mode 100644 .atp/work-session/20260622-162345/report.md diff --git a/.atp/work-session/20260618-145152/report.md b/.atp/work-session/20260618-145152/report.md new file mode 100644 index 0000000..b627ee8 --- /dev/null +++ b/.atp/work-session/20260618-145152/report.md @@ -0,0 +1,162 @@ +--- +schema_version: 2 +sid: 20260618-145152 +started_at: 2026-06-18T14:51:52+09:00 +ended_at: 2026-06-18T14:58:00+09:00 # 인터뷰 1차에서 사용자 지시로 일시정지 (구현 다음 세션) +user_request: | + W3-2 댓글/리뷰 고도화 — 인터뷰 주도 디테일 빌드. + W3-2 코어(2026-06-18 구현·검증 완료) 유지하며 세세한 디테일을 사용자 인터뷰로 누적. + 바로 설계·구현 금지. 질문 시드 카탈로그(A 일관성 / B 목록규모 / C UX)를 한 축씩 + AskUserQuestion 으로 물어 결정 누적 → design-advisor → 구현 → L1 회귀 31건 + 신규 → L3 스모크. +mode: interview-first (orchestrator 직접 인터뷰, requirements-advisor 대체) +--- + +# Advisor Invocation Decision Log +- advisor: requirements-advisor + decision: skip + rationale: '카탈로그가 요구 분해를 이미 제공 — orchestrator 가 AskUserQuestion 인터뷰로 직접 스코프 확정(task 명시)' + checked_at: 2026-06-18T14:52:00+09:00 +- advisor: design-advisor + decision: deferred-to-next-session + rationale: '인터뷰 완료·FINAL SPEC 사용자 확정. 단 사용자 지시로 구현은 다음 세션 → design-advisor 도 다음 세션 진입. 선결: W2-3 freeze 근거 확인' + checked_at: 2026-06-18T15:10:00+09:00 +- advisor: retrospective-advisor + decision: skip + rationale: '코드 변경 0줄·구현 미시작 인터뷰 전용 세션. 회고 입력(구현/검증 결과) 부재 → 구현 세션 종료 시 수행' + checked_at: 2026-06-18T15:10:00+09:00 + +# Summary +인터뷰 완료 — 카탈로그 전 축(A1·A2·A3 / B1·B2·B3 / 리뷰정책4 / C1~C6) 결정 누적 완료. +사용자 지시로 이 세션은 **결정·기록만, 구현은 다음 세션**. 코드 변경 0줄(Serena/grep 탐색만). +다음 세션: 새 sid + `resumed_from: 20260618-145152`, 아래 FINAL SPEC 으로 design-advisor 진입. + +## ⚠️ 다음 세션 선결·고위험 항목 +- **C3 = W2-3 평점 집계 동결 해제**: 설계 진입 전 W2-3 freeze 근거(ADR/문서) 필독. §6 파괴적 게이트 인접 — orchestrator 사용자 재확인. 회귀 검증 필수 +- **다축 평점 = 대규모 신규**: game_review_axes 테이블 + 6축 입력 UI + overall 자동평균/수동덮어쓰기 + game_review_stats 뷰(축별 평균). 작업량·테스트 비중 큼 +- 입력 부담: 리뷰 제출 = 6축 필수 + 본문 10자 — UX 마찰 인지 + +# FINAL SPEC (✅ 사용자 최종확인 완료 2026-06-18 — 다음 세션 design-advisor 입력) + +## A. 일관성 +- **A1** 댓글 응답 = `commentView` 단일 통일(POST·PUT·list 전부). POST는 insert 후 재조회(createReview 패턴) +- **A2** 작성자명 = 하이브리드 `COALESCE(u.display_name, c.nickname)` + LEFT JOIN. user_id 있으면 현재닉(개명반영), NULL이면 스냅샷(QG-2 해결). createComment nickname 스냅샷 저장 유지 + - 탈퇴자: 댓글 보존(LEFT JOIN) / 리뷰 현행 제외 유지(INNER JOIN). 탈퇴자 댓글 이름 = "(탈퇴한 사용자)" 마스킹: `CASE WHEN u.id IS NULL THEN c.nickname WHEN u.is_delete THEN '(탈퇴한 사용자)' ELSE u.display_name END` +- **A3** 댓글 edited+updatedAt 추가. game_comments.updated_at 멱등 ALTER + 매퍼/모델 갱신. editGameComment 시 updated_at 갱신, nickname 미덮어씀 + +## B. 목록 규모 +- **B1** 페이지네이션 offset/limit + "더보기", 20건/페이지. 정렬 토글: 댓글(오래된순 기본/최신순), 리뷰(최신순 기본/별점 높은순/낮은순, tiebreaker rating·created_at·id). sort 화이트리스트 enum→고정 SQL(${} 금지) +- **B2** 리뷰 본문 필수 + 최소 10자(별점만 제출 불가). 댓글은 현행(200자 상한, 최소 없음) +- **B3** 본문 정규화 = 제어문자 제거 + 외곽 trim(댓글·리뷰 공통). 내부 연속공백·줄바꿈 보존 + +## 리뷰 정책 +- 게임당 **1회 유지**(ux 유니크), 수정 **무제한 유지**(edited 마커) +- **다축 평점**: overall rating 유지(W2-3 집계용) + game_review_axes 신규 테이블(review_id FK, axis_key enum, score). 축 6개 **몰입성·창의성·조작성·완성도·사운드·비주얼**, 각 1~5 정수 **전 축 필수**. UI: overall = 축 평균 자동계산(정수 반올림)되다 유저 직접선택 시 덮어쓰기(수동 우선, 출처 구분 플래그 검토) + - **다축 시각화 = 육각형 레이더 차트(hexagon)**: + - 위치 = **요약 + 개별 리뷰 둘 다** (요약=game_review_stats 6축 평균 육각형 / 각 리뷰 카드=그 리뷰 6축 육각형) + - 렌더 = **인라인 SVG 자체 구현** (polygon points 계산, 기존 JSP 인라인 SVG idiom·빌드 없음·의존성 0). 차트 라이브러리/CDN 배제 + - 입력은 **6개 별점 위젯 유지(C6)** — 육각형은 표시 전용(레이더 드래그 입력 a11y 제외) + - **a11y**: SVG 시각요소엔 텍스트 대체 필수 — 축별 점수 aria-label 또는 인접 텍스트/표(스크린리더). 폴리곤만으로 끝내지 말 것 + - 개별 리뷰 카드 육각형은 컴팩트 사이즈(페이지당 20건 DOM 고려) +- 신고·숨김 **보류**(W1 운영자 role 선행) + +## C. UX (game-detail.jsp) +- **C1** submit 잠금 + 로딩 상태(제출 중 disabled) +- **C2** 상대시각 "n분 전" + title 절대시각(fmtDate 확장) +- **C3** ⚠️ W2-3 동결 해제 + game_review_stats **뷰**(game_id, avg_rating, review_count + 6축 평균). 표시 = 소수1자리 + 반올림 별 + 0개 "아직 평가 없음". client 계산 폐기(페이지네이션 정합) +- **C4** 실시간 글자수 카운터(123/200, 리뷰 최소10자 안내). 댓글·리뷰 둘 다 +- **C5** 삭제 = 완전 소멸 유지(현행) +- **C6** 별점 위젯 완전 radiogroup 패턴(roving tabindex + 화살표/Home/End), 다축 6위젯 적용 + +# Grounded Baseline (evidence) +- GameCommentController 응답 3종 불일치: + - POST createComment flat: status,message,commentId,gameId,authorName,userId,content (createdAt/edited 없음) + - PUT updateComment 부분: status,message,commentId,content + - list commentView: commentId,gameId,authorName,userId,content,createdAt (edited 없음) +- GameReviewController 이미 통일: POST/PUT/list/get 전부 reviewView 사용 + - reviewView: reviewId,gameId,authorName,userId,rating,body,edited,createdAt,updatedAt +- 댓글 작성자명 = nickname 스냅샷 (createComment: session displayName 저장) +- GameCommentData 필드: id,gameId,userId,nickname,content,createdAt,deletedAt — **updatedAt 없음** + → A3(댓글 수정됨 마커)는 game_comments.updated_at ALTER 필요 (무료 아님) +- createReview: rating 필수, body trimToEmpty 후 length 검사만 → 빈 본문 리뷰 허용 (B2) +- trimToNull/trimToEmpty = 외곽 trim 만; 내부 연속공백/제어문자 미처리 (B3) +- DDL game-reviews-ddl.sql: game_reviews(updated_at 보유), game_comments user_id nullable(레거시 보존) +- W2-3 평점 집계 스키마 동결 / QG-2 레거시 user_id NULL 댓글 미결 +- **A2 작성자명 메커니즘 분기 (신규 발견)**: + - 리뷰 = `JOIN users u, u.display_name AS authorName` (GameReviewsMapper) → 항상 현재 닉네임(live) + + `u.is_delete IS NOT TRUE` 조건 → **탈퇴자 리뷰는 목록서 제외됨** + - 댓글 = `nickname` 컬럼 스냅샷(작성 시점 고정, GameCommentsMapper listGameComments:36) → 탈퇴자 댓글도 표시 + - 댓글 매퍼에 미사용 `updateGameComment`(nickname 갱신 가능) 존재 (:81) — A2/A3 설계 시 활용 여지 + - QG-2 결합: 레거시 user_id NULL 댓글은 JOIN 불가 → live-join 채택 시 스냅샷 fallback 필수 + +# Decisions (누적) +- A3 댓글 수정됨 마커: **edited+updatedAt 추가** (리뷰 대칭). game_comments.updated_at 멱등 ALTER + 매퍼 갱신 필요. [2026-06-18] +- A1 응답 스키마: **commentView 단일 통일** — POST·PUT·list 전부 commentView 반환(commentId,gameId,authorName,userId,content,createdAt,edited,updatedAt). POST는 insert 후 재조회(createReview 패턴). 프론트 렌더 1함수. [2026-06-18] + +# Invocations +[] + +# user_signals +positive: [] +negative: [] + +# verified_by_me +(없음 — 코드 변경 0줄, 인터뷰만 수행. L1/L2/log-scan 비대상) + +# needs_user_verification +- 다음 세션: 인터뷰 재개(아래 어젠다) → 결정 충분 시 design-advisor → 구현 → L1 회귀 31건+신규 → L3 스모크. + +# graph_refresh +skip: 코드 변경 0줄 (graph-refresh-checker 비대상) + +# Next-session agenda (인터뷰 재개 지점) +## 결정 잠금 (재확인만) +- A1 = commentView 단일 통일 (POST·PUT·list 전부 commentView; POST insert 후 재조회) +- A3 = 댓글 edited+updatedAt 추가 (game_comments.updated_at 멱등 ALTER + 매퍼/모델 갱신) + - 종속: A1 통일 시 commentView 가 edited/updatedAt 자동 포함 → 두 결정 한 묶음 구현 +- A2 = **하이브리드 작성자명**: `COALESCE(u.display_name, c.nickname)` + **LEFT JOIN users** (GameCommentsMapper getGameComment/listGameComments 둘 다) + - user_id 있으면 현재 닉네임(개명 반영, 리뷰와 일관) / NULL이면 스냅샷 nickname → **QG-2 자동 해결** + - createComment 의 nickname 스냅샷 저장은 **유지**(fallback 안전망). 미사용 updateGameComment(:81)는 edit 시 nickname 덮어쓰지 않도록 주의 +- A2-탈퇴자 = **댓글 보존 / 리뷰 현행(제외) 유지**: 댓글 LEFT JOIN(is_delete 무필터) → 탈퇴자 댓글 표시. 리뷰 INNER JOIN+`u.is_delete IS NOT TRUE` 그대로 → 리뷰 스코프·31건 테스트 무영향 + - A2-탈퇴자-이름 = **"(탈퇴한 사용자)" 마스킹**. 댓글 SQL: `CASE WHEN u.id IS NULL THEN c.nickname WHEN u.is_delete THEN '(탈퇴한 사용자)' ELSE u.display_name END AS authorName` (LEFT JOIN). 탈퇴 플래그 노출 불필요 — 이름만 대체 +- B1 = **페이지네이션 도입 + 기본 정렬 현행 유지** (댓글 오래된순 ASC / 리뷰 최신순 DESC 기본). 전건 반환 폐기 → offset/limit 또는 cursor (방식·크기·정렬옵션은 후속 질문) + - B1-방식 = **offset/limit + "더보기" 버튼, 페이지당 20건**. page/offset 쿼리 파라미터 + - B1-정렬 = **댓글·리뷰 모두 정렬 토글**: + - 댓글: 오래된순(기본)/최신순 + - 리뷰: 최신순(기본)/별점 높은순/낮은순 — 별점순 정렬은 tiebreaker 필요(rating DESC/ASC, created_at DESC, id DESC) + - API sort 파라미터 신설 + 프론트 정렬 컨트롤. 매퍼 정렬절 동적화(단 `${}` 금지 — sort 화이트리스트 enum→고정 SQL 분기로 #{} 안전 유지) +- B3 = **제어문자 제거 + 외곽 trim** (댓글 content·리뷰 body 공통). 탭/개행 외 제어문자 strip, 내부 연속공백·줄바꿈은 보존. textContent 렌더라 보안 아닌 저장 위생 목적 +- B2 = **리뷰 본문 필수 + 최소 10자** (별점만 제출 불가). createReview/updateReview body 검증 추가. 댓글은 별도(200자 상한만, 최소 없음 — 변경 안 함) + - 확장 리뷰 정책 4영역: + - ① 게임당 1회 제한 = **1회 유지(현행)** — ux_game_reviews_game_user_active 유지, 무결성 우선 + - ② 수정 제한 = **무제한 수정 유지(현행)** — edited 마커로 투명성 + - ③ 별점 척도 = **다축 평점 도입**. 세부: + - **overall rating 유지 + 축 별도** (W2-3 집계 동결 안 건드림 — 집계는 game_reviews.rating 그대로). 축은 game_review_axes 신규 테이블 + - **UI 동작**: overall 별점 = 축 점수 평균으로 자동계산·표시되다가, 유저가 overall 을 직접 선택하면 그 값으로 강제 덮어쓰기 (수동 우선) + - 설계 미결: 자동평균→정수 rating 반올림 정책(C3 평균 별점 정책과 연동) / rating 수동·자동 출처 구분 필요(예 is_rating_manual 플래그?) + - **축 6개**: 몰입성, 창의성, 조작성, 완성도, 사운드, 비주얼 + - 축 척도 = **1~5 정수, 전 축 필수** (overall 과 동일, 자동평균 결정적) + - 스키마 = **game_review_axes 테이블** (review_id FK + axis_key + score, 리뷰당 6행). axis_key enum 6종. DDL 신규 테이블 + FK + 인덱스(review_id) + - 다축 평점 = **풀스펙 확정**. 입력 부담(6축 필수 + 본문 10자)은 인지하고 진행 + - ④ 신고·숨김 = **도입 보류(W1 선행)** — 운영자 role 활성 후 신고+숨김 함께. 현 세션 스코프 제외 + +## 미결 — 다음 세션 질문 순서 (1~2축씩, AskUserQuestion, 추천 맨앞+근거+트레이드오프) +3. **C UX (game-detail.jsp)** — JSP grounding 완료: + - C1 연타 방지 = **submit 잠금 + 로딩 상태** (제출 중 disabled, 응답 후 해제). 댓글 중복 POST 차단 + - C2 시간 표시 = **상대시각 "n분 전" + title 속성에 절대시각** (fmtDate:1202 대체/확장) + - C3 평균 별점 = **⚠️ W2-3 동결 해제 + 집계 컬럼/뷰 신설** (사용자 informed 선택, 고위험 — 회귀 검증 필수). 표시 = **소수1자리 + 반올림 별 + 0개는 "아직 평가 없음" 문구** + - 모순 해소: client 계산은 B1 페이지네이션과 충돌 → 서버 집계로 전환 확정 + - **다음 세션 설계 선결조건**: W2-3 freeze 근거(ADR/문서) 먼저 읽고 무엇이 왜 동결됐는지 파악 후 해제. §6 파괴적 게이트 인접 — orchestrator 사용자 확인 경로 + - 리뷰별 overall 자동평균(축→overall)은 rating 정수라 정수 반올림 / 전체 평균 표시는 소수1자리 (반올림 컨텍스트 2종 구분) + - 집계 방식 = **뷰(VIEW) 기반** — game_review_stats 뷰(game_id, avg_rating, review_count + 6축 평균). 저장 컬럼 비신설(파생 객체만) → 동결 해제 중 회귀 표면 최소 + - 축별 평균 = **overall + 6축 평균 모두 요약 표시**. UI에 축별 막대/점수. 뷰가 축별 AVG(game_review_axes JOIN) 포함 + - C4 글자수 카운터 = **실시간 카운터** (123/200 형식, 리뷰는 최소 10자 미달 안내). 댓글·리뷰 textarea 둘 다 + (다축 도입 시 6축 위젯 입력 상태도 고려) + - C5 삭제 표시 = **완전 소멸 유지(현행)** — soft-delete + is_delete 필터. 댓글 스레드 없어 tombstone 불필요 + - C6 접근성 = **완전 radiogroup 패턴** — roving tabindex(위젯당 단일 탭정지) + ←→↑↓·Home/End 별점 이동, aria 유지. 다축 6위젯 동일 적용. 현 위젯은 화살표 핸들러 없음(grep 확인) — 신규 추가 + - grounding 근거: 별점위젯:1043(role=radiogroup/radio+aria 기구현), 평균:1034, 댓글ta:1080(max200), 리뷰ta:1052(max1000), 제출버튼:538(disabled 없음) + +## 인터뷰 종료 후 (task 명시) +1. 누적 결정 최종 확인 → 2. design-advisor: 파일 소유권 맵 + 멱등 ALTER DDL → 3. 구현 → L1 31건 회귀+신규 → L3 스모크 → 4. docs/changes/ 이력 기록 + +# open_items +- `.atp/work-session/20260618-145152/report.md` (신규, untracked) — 세션 기록. 커밋은 사용자 지시 시 (CLAUDE.md 커밋 정책: 명시 요청 시만). +- src/ 변경 0건 — 구현 미시작. diff --git a/.atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html b/.atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html new file mode 100644 index 0000000..286702e --- /dev/null +++ b/.atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html @@ -0,0 +1,364 @@ + + + + + +W3-2 UI 프리뷰 — 육각형 레이더 · 6축 평점 · 카드 + + + +
+
+

W3-2 UI 프리뷰 · 육각형 레이더 · 6축 평점

+ +
+ + +
+ +
+ 4.2 + + (37) +
+
+ + +
+
+
+ 종합 별점 (선택) +
+
+ + +
+ 0/1000 · 6축은 필수, 종합 별점은 선택이에요 + +
+
+ + +
+
+ + + +
+
+
    +
    +
    + + +
    + +
    + + +
    + 0/200 + +
    +
    +
      +
      +
      + + + + diff --git a/.atp/work-session/20260622-092800/documentation.md b/.atp/work-session/20260622-092800/documentation.md new file mode 100644 index 0000000..3ffc613 --- /dev/null +++ b/.atp/work-session/20260622-092800/documentation.md @@ -0,0 +1,41 @@ +--- +phase: documentation +agent: documentation-advisor +agent_version: 1 +generated_at: 2026-06-22T10:00:00+09:00 +concerns: + - "C3 재분류(W2-3 동결 경계 재확인)는 되돌리기 어려운 아키텍처 경계 결정에 해당할 수 있으나, 실체는 기존 오분류를 roadmap 근거(:63/:201/:202)로 정정한 것이므로 현재는 changes/ 내 별도 섹션으로 기록. W2-3 관련 설계 분기가 재발하면 adr/ADR-0001 발행 권고." + - "design-advisor 가 이미 .atp/work-session/20260622-092800/implementation/design.md 에 설계 산출물을 남김. architecture/ 중복 갱신 없음 — changes/ + work-session/ 중심으로 처리." + - "frontend-design 스킬이 game-detail.jsp 시각 컴포넌트를 처리했으므로 JSP 프론트엔드 세부 구현은 changes/ 문서에서 요약 수준으로만 기록." + - "retrospective 교훈 4건은 development/verification-strategies.md 에 '설계·테스트 단계 체크리스트' 섹션 및 '프로토콜 개선 권고' 섹션으로 추가 완료 (2026-06-22 retrospective-advisor 수용 결정 반영)." + - "backport self-grep: verification-strategies.md 근거 블록(> 근거:)에 20260622·GameController.gameDetail 잔류 — 요구사항 '이번 세션 사례는 1줄 근거로' 허용 패턴이므로 정본 규칙형 텍스트에는 포함되지 않음. docs/development/release-checklist.md §7 부재 — 해당 절차 미적용(N/A에 준함)." +concerns_checked: true +--- + +# 문서화 보고 + +## 작성/수정된 문서 + +| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 | +|---|---|---|---|---| +| `docs/changes/2026-06-22-w3-2-comments-reviews-enhancement.md` | changes | 신규 | `docs/changes/index.md` | `docs/changes/2026-06-18-w3-2-comments-reviews.md` (이월 항목 해소 링크) | +| `docs/changes/index.md` | changes | 갱신 | — | — | +| `docs/changes/2026-06-18-w3-2-comments-reviews.md` | changes | 갱신 (이월 항목 해소 표기) | — | `docs/changes/2026-06-22-w3-2-comments-reviews-enhancement.md` | +| `docs/development/verification-strategies.md` | development | 갱신 (섹션 추가) | index 추가 불필요(기존 등록) | report.md#Retrospective memory_candidates docs_sync_target | + +## 의사결정 기록 위치 + +- **C3 재분류**: `docs/changes/2026-06-22-w3-2-comments-reviews-enhancement.md` §C3 재분류 섹션 + `.atp/work-session/20260622-092800/implementation/design.md` §C3 재분류 +- **is_rating_manual 컬럼 채택**: `design.md` concerns 2번 + `report.md#Decisions` +- **axis_key 저장형·순서 고정**: `design.md` concerns 5번 + §2.1 +- **sort 안전 (@SelectProvider 컴파일타임 상수)**: `design.md` §4 + `report.md#Decisions` +- **pagination = limit+1 hasMore**: `design.md` §3.3 + `report.md#Decisions` +- **SVG 육각형 = JS 런타임**: `design.md` §6.1 concerns 4번 + +## 추후 문서화가 필요한 항목 + +- **ADR-0001 (권고)**: W2-3 동결 경계 확정(잼 평가 한정, 댓글/리뷰 무관) — C3 재분류 정정의 배경이 된 경계 결정을 정식 ADR로 남길 가치 있음. +- **DDL 적용 완료 시**: `docs/maintenance/` 에 dev/운영 DB 적용 절차(game_review_axes·game_review_stats·game_comments.updated_at·game_reviews.is_rating_manual) 기록 검토. +- **L3 스모크 완료 후**: `docs/changes/2026-06-22-w3-2-comments-reviews-enhancement.md` 검증 결과 표에서 "needs_user_verification" → "PASS (날짜)" 갱신. +- **기존 리뷰 axes 백필**: 백필 필요 판단 시 `docs/maintenance/` 에 절차 문서화. +- **GET /reviews/mine (applyReviewGate 정밀화)**: 차기 세션 구현 완료 후 changes/ 업데이트. diff --git a/.atp/work-session/20260622-092800/implementation/design.md b/.atp/work-session/20260622-092800/implementation/design.md new file mode 100644 index 0000000..8f8df25 --- /dev/null +++ b/.atp/work-session/20260622-092800/implementation/design.md @@ -0,0 +1,536 @@ +--- +phase: design +agent: design-advisor +agent_version: 1 +generated_at: 2026-06-22T00:00:00+09:00 +concerns: + - "C3 재분류 확정: game_review_stats 는 W3-2 일반 DDL(읽기전용 집계뷰). W2-3 동결 묶음 아님 — 근거 jam-platform-roadmap.md:63,201,202. 단 game-reviews-ddl.sql:63 / db/schema.sql:136-137 의 '집계뷰 미신설' 보수 주석이 본 설계로 무효화되므로 구현 시 두 곳 주석 갱신 필수(미갱신 시 차기 세션 혼선)." + - "is_rating_manual 플래그 채택 — game_reviews 행 폭 증가(boolean 1컬럼). 채택 근거는 §5.3. 비채택 대안(매번 axes AVG 재계산)도 가능하나 '유저가 자동평균과 같은 값을 수동선택' 케이스를 구분 못 함 → 채택. 구현에서 이 컬럼이 실제 분기에 쓰이는지(수동/자동 표시 차별화 UI 없으면 dead) 재확인 필요." + - "GameReviewsMapper.editGameReview 의 axes 재저장은 delete-all + insert-6 채택(§5.2). upsert(ON CONFLICT) 대안 대비 단순하나, 6 DELETE+6 INSERT 트랜잭션. axes 매퍼 신규 분리 vs GameReviewsMapper 확장 — 신규 GameReviewAxesMapper 채택(§1, §5). 단일 책임." + - "SVG 육각형 좌표는 JS 런타임 계산 채택(§6). JSP 컴파일타임(JSTL) 대안은 개별 리뷰 N건 동적 렌더와 맞지 않음 — 기존 buildStarDisplay 패턴(JS DOM 생성)과 일관. 좌표 공식은 §6 에 고정." + - "axis_key DB 저장형 = 영문 snake(immersion/creativity/controls/completeness/sound/visual). 한글 라벨(몰입성 등)은 JSP 상수 매핑. enum 순서 = 육각형 각 축 인덱스(0~5)와 1:1 고정 — 순서 바뀌면 레이더 축 위치 변동. §2.1 / §6.3 에 순서 고정." + - "B2 최소 10자: 기존 createReview 는 빈 본문 허용(report Grounded Baseline:80). 본 변경으로 별점만 제출하던 기존 유저 플로우가 막힘 — UX 마찰. FINAL SPEC 확정 사항이므로 진행하되 프론트 안내문(C4 카운터 '최소 10자') 필수." + - "ESCALATION 없음 — requirements(report FINAL SPEC) vs research(roadmap) 충돌 0. C3 오분류는 task 가 이미 정정 지시 → 반영 완료." +references: + requirements: /Users/wemadeplay/workspace/stz/bibimbap/.atp/work-session/20260618-145152/report.md + research: /Users/wemadeplay/workspace/stz/bibimbap/docs/work-log/2026-06-17-jam-platform-roadmap.md + adrs: + - /Users/wemadeplay/workspace/stz/bibimbap/docs/changes/2026-06-18-w3-2-comments-reviews.md + - /Users/wemadeplay/workspace/stz/bibimbap/docs/game-reviews-ddl.sql + - /Users/wemadeplay/workspace/stz/bibimbap/db/schema.sql +--- + +# 설계: W3-2 댓글/리뷰 고도화 (확정 구현 설계도) + +## 목표 / 비목표 + +### 목표 (FINAL SPEC 추적 가능) +- **A1** 댓글 응답 commentView 단일 통일 (POST·PUT·list 전부 동일 스키마, POST는 insert 후 재조회). +- **A2** 댓글 작성자명 하이브리드 (LEFT JOIN users + CASE 마스킹). QG-2(레거시 user_id NULL) 자동 해결. +- **A3** 댓글 edited+updatedAt (game_comments.updated_at 멱등 ALTER + 매퍼/모델 갱신). +- **B1** 페이지네이션 offset/limit 20건 "더보기" + sort 토글 (화이트리스트 enum→고정 SQL, `${}` 금지). +- **B2** 리뷰 본문 필수 최소 10자(trim 후). 댓글은 현행 유지. +- **B3** 본문 정규화 (제어문자 strip + 외곽 trim, 댓글·리뷰 공통 유틸). +- **다축 평점** game_review_axes 6축(1~5 전 축 필수) + overall 자동평균/수동덮어쓰기(is_rating_manual). +- **C3** game_review_stats 읽기전용 집계뷰 (avg_rating, review_count, 6축 평균). 클라 평균계산 폐기. +- **C1** submit 잠금+로딩 / **C2** 상대시각 / **C4** 글자수 카운터 / **C5** 완전소멸 유지 / **C6** radiogroup 완전 패턴. +- **육각형 레이더** 인라인 SVG 자체구현 (요약 + 개별 리뷰 카드), a11y 텍스트 대체. + +### 비목표 (스코프 밖 — 명시) +- 신고·숨김 (W1 운영자 role 선행). +- 리뷰 이력 테이블 (in-row 마커만). +- 좋아요 서버화 (별도 관리). +- W2-3 잼 평가(심사/투표/시상) 스키마 — **본 설계 무관**(C3 정정으로 명확히 분리). +- 댓글 최소 글자수 (현행: 상한 200자만). + +--- + +## C3 재분류 (직전 report 오분류 교정 — 설계 결정) + +직전 report.md 는 C3(game_review_stats 평균별점 뷰)를 "W2-3 동결 해제·고위험·§6 게이트"로 표기했으나 **오분류**다. + +- **근거** (`docs/work-log/2026-06-17-jam-platform-roadmap.md`): + - `:63` — "W2-3 범위 = 잼 평가만. 댓글/리뷰 스키마 자체는 W3에서 설계(동결 묶음 아님)." + - `:201` — "S4D 평가 통합설계 → W2-3 범위 축소: 잼 평가(심사/투표/시상) 스키마만 동결. 댓글/리뷰 스키마는 W3-2에서 별도 설계." + - `:202` — "S4a 댓글/리뷰 분리 → W3-2 일반기능으로 재분류: 잼 평가 동결묶음에서 분리." +- **결론**: `game_review_stats` = `game_reviews`(W3-2 테이블) 위 **읽기전용 집계뷰** 신설 = **일반 DDL**. §6 파괴적 게이트 없음. orchestrator 사용자 재확인 경로 불필요. +- **부수 조치**: `docs/game-reviews-ddl.sql:63` 주석("집계 컬럼/뷰는 신설하지 않음 (W2-3 동결 보호)") 및 `db/schema.sql:136-137`("집계 컬럼/뷰는 W2-3 동결 — 신설 금지") 은 본 설계로 **무효화** → 구현 시 갱신 대상(파일 영향 맵 참조). + +--- + +## 개요 + +기존 W3-2 코어(댓글/리뷰 CRUD, 2026-06-18 커밋)를 보존하며 그 위에 일관성(A)·규모(B)·다축평점·UX(C)를 얹는다. 백엔드는 매퍼 시그니처 확장(offset/limit/sort, axes 저장) + 신규 axes 매퍼/DDL, 프론트는 game-detail.jsp 단일 파일의 댓글·리뷰 JS 블록 재작성(육각형 SVG, radiogroup, 페이지네이션, 서버집계 표시)이다. DDL은 game-reviews-ddl.sql 에 멱등 append + db/schema.sql 동기화한다. 전 변경은 31건 회귀 테스트를 깨지 않는 것을 1차 게이트로 한다. + +--- + +## 플로우 + +### 리뷰 작성 (POST /game/{id}/reviews) — 다축 확장 +``` +진입: CSRF(403) → 로그인(401) → 게임존재(404) + → rating 파싱(수동 overall, optional) + axes 6값 파싱(전 축 1~5 필수, 누락/범위외 400) + → body 정규화(B3) + 최소 10자 검사(B2, 400) + → 게임당 1회 검사(409) + → overall 결정: + 유저가 overall 직접선택 O → rating=선택값, is_rating_manual=true + 유저가 overall 직접선택 X → rating=round(6축 평균), is_rating_manual=false + → [TX] addGameReview(rating, body, is_rating_manual) → review.id 획득 + → addReviewAxes(review.id, 6행) ← axes 매퍼 + → getGameReview(review.id) 재조회 → reviewView(+axes) 반환 +종단: 200 reviewView +``` + +### 리뷰 수정 (PUT) — axes 재저장 +``` +진입: CSRF → 로그인 → 리뷰존재+게임일치(404) → 권한(canModify, 403) + → rating/axes/body 검증(작성과 동일) + → [TX] editGameReview(rating, body, is_rating_manual, updated_at=now()) + → deleteReviewAxes(reviewId) ← 기존 6행 제거 + → addReviewAxes(reviewId, 6행) ← 신규 6행 + → getGameReview 재조회 → reviewView 반환 +``` + +### 목록 조회 (GET /game/{id}/reviews?page&sort) — 페이지네이션 +``` +진입: 게임존재(404) + → page(default 0)·sort(default newest, 화이트리스트 검증→고정 enum) 파싱 + → offset = page*20, limit=21 (hasMore 판정용 +1 조회) + → listGameReviews(gameId, offset, limit, sortEnum) ← 21건 조회 + → 각 리뷰에 axes 6행 batch 조회·매핑 (listReviewAxesByReviewIds) + → hasMore = (조회건수 > 20) → 21번째 잘라 20건 반환 + → game_review_stats 1행 조회(요약) → summary 포함 +종단: { status, reviews:[reviewView...], hasMore, summary:{avgRating,reviewCount,axes{}} } +``` + +### 댓글 작성 (POST /game/{id}/comments) — A1 통일 +``` +진입: CSRF → 로그인 → 게임존재 + → content 정규화(B3) + 1~200자 검사 + → nickname 스냅샷 = sessionDisplayName (유지) + → addGameComment → comment.id 획득 + → getGameComment(comment.id) 재조회(LEFT JOIN authorName/edited/updatedAt) ← A1: 재조회 패턴 + → commentView 반환 +종단: 200 commentView (createReview 패턴 대칭) +``` + +--- + +## 데이터 모델 + +### 2.1 game_review_axes (신규 테이블) +| 컬럼 | 타입 | 제약 | 비고 | +|---|---|---|---| +| id | bigint | PK, seq | game_review_axes_id_seq | +| review_id | bigint | NOT NULL, FK→game_reviews(id) | | +| axis_key | varchar(20) | NOT NULL, CHECK(6종) | 영문 snake | +| score | smallint | NOT NULL, CHECK(1~5) | | +| | | UNIQUE(review_id, axis_key) | 리뷰당 축별 1행 | +| | | INDEX(review_id) | 조회 | + +- **axis_key 6종 (순서 고정 — 육각형 축 인덱스 0~5)**: + `immersion`(몰입성), `creativity`(창의성), `controls`(조작성), `completeness`(완성도), `sound`(사운드), `visual`(비주얼). +- 리뷰당 정확히 6행 (전 축 필수). DB는 UNIQUE+CHECK로 보장, 앱이 6행 누락 없이 insert 책임. + +### 2.2 game_comments.updated_at (멱등 ALTER) +- `timestamptz DEFAULT now() NOT NULL`. `edited = updated_at > created_at` (리뷰 대칭). +- 기존 행은 ALTER 시 default now() 적용 → created_at != updated_at 가능성 → 기존 행 보정: `UPDATE ... SET updated_at = created_at WHERE updated_at IS NULL` 불필요(NOT NULL default). 대신 ALTER 직후 `UPDATE game_comments SET updated_at = created_at WHERE updated_at > created_at` 1회로 기존 댓글이 "수정됨" 오표시되지 않게 정렬 (멱등 — 재실행 시 영향 없음, 신규 행은 INSERT가 created/updated 동시 now()). + +### 2.3 game_reviews.is_rating_manual (멱등 ALTER) +- `boolean DEFAULT false NOT NULL`. true=유저 직접선택 overall, false=6축 자동평균. + +### 2.4 game_review_stats (VIEW 신규) +- `game_reviews(is_delete IS NOT TRUE)` ⨝ `game_review_axes` 집계. +- 컬럼: `game_id, avg_rating numeric, review_count bigint, avg_immersion numeric, avg_creativity numeric, avg_controls numeric, avg_completeness numeric, avg_sound numeric, avg_visual numeric`. +- 클라 평균계산(updateSummary, JSP:1513-1522) 폐기 공급원. + +--- + +## 외부 계약 (API) + +### 3.1 commentView 스키마 (A1 통일 — POST·PUT·list 동일) +```json +{ + "commentId": 100, + "gameId": 1, + "authorName": "표시명 또는 (탈퇴한 사용자) 또는 스냅샷닉", + "userId": 7, // nullable (레거시) + "content": "...", + "createdAt": "ISO-8601", + "edited": false, // updated_at > created_at + "updatedAt": "ISO-8601" // A3 신규 +} +``` +- 변화: POST 응답이 flat(commentId/gameId/authorName/userId/content) → commentView 전체. PUT 응답이 부분(commentId/content) → commentView 전체. createdAt/edited/updatedAt 신규 노출. `status`,`message`는 POST/PUT 응답에 commentView 와 병합(createReview 패턴 그대로). + +### 3.2 reviewView 스키마 (axes 추가) +기존 reviewView(reviewId,gameId,authorName,userId,rating,body,edited,createdAt,updatedAt) + 신규: +```json +{ + "...": "기존 필드 유지", + "ratingManual": false, // is_rating_manual + "axes": { "immersion":4,"creativity":5,"controls":3,"completeness":4,"sound":2,"visual":5 } +} +``` + +### 3.3 GET list 응답 형태 (페이지네이션) +- 댓글: `{ status, comments:[commentView...], hasMore }` +- 리뷰: `{ status, reviews:[reviewView...], hasMore, summary }` + - `summary` = game_review_stats 1행 → `{ avgRating, reviewCount, axes:{immersion..visual} }` (review_count=0 이면 summary=null). +- **요청 파라미터**: `?page=&sort=`. +- **hasMore 방식 채택**: total count 대신 `limit+1` 조회 후 초과 여부. 근거 = 추가 COUNT 쿼리 회피, "더보기" UI는 total 불요. + +### 3.4 sort enum 값 정의 +- 댓글: `oldest`(기본), `newest`. +- 리뷰: `newest`(기본), `rating_desc`, `rating_asc`. +- 미지정/미허용 값 → 기본값으로 fallback (400 던지지 않음 — UX 관대). + +--- + +## sort 화이트리스트 매핑 표 (§4 — `${}` 금지 안전성) + +매퍼는 **enum 분기 → 고정 SQL 절** 방식. 동적 `${}` SQL 치환 절대 사용 안 함. 컨트롤러가 String sort 를 화이트리스트 enum 으로 변환(미스매치=기본값) 후, 매퍼는 MyBatis ``(XML) 또는 **annotation @SelectProvider 의 if-분기로 고정 문자열 상수 선택**. + +**채택 방식**: 현 코드가 annotation 매퍼(@Select 텍스트블록)이므로 일관성 위해 **컨트롤러에서 sort enum 결정 + 매퍼 메서드를 sort별로 분리하지 않고**, `@SelectProvider` 로 전환하여 ORDER BY 절만 상수 분기. (대안: 매퍼 메서드 N개 분리 — 중복 과다로 비채택.) + +| sort enum | 대상 | ORDER BY 고정 SQL 절 (상수, 사용자 입력 미포함) | +|---|---|---| +| `oldest` (댓글) | comments | `ORDER BY created_at ASC, id ASC` | +| `newest` (댓글) | comments | `ORDER BY created_at DESC, id DESC` | +| `newest` (리뷰) | reviews | `ORDER BY r.created_at DESC, r.id DESC` | +| `rating_desc` (리뷰) | reviews | `ORDER BY r.rating DESC, r.created_at DESC, r.id DESC` | +| `rating_asc` (리뷰) | reviews | `ORDER BY r.rating ASC, r.created_at DESC, r.id DESC` | + +- **안전성 근거**: ORDER BY 문자열은 5개 **컴파일타임 상수** 중 하나로만 결정. 사용자 입력 String 은 enum 매칭(`switch`/`Map`)에만 사용되고 SQL 텍스트에 절대 보간되지 않음 → SQL injection 면역. offset/limit 은 `#{}` 파라미터 바인딩 유지. +- **tiebreaker**: 별점순은 동점 시 created_at DESC, id DESC 로 결정적 정렬(페이지네이션 안정성 — offset 중복/누락 방지). + +--- + +## 다축 시퀀스 (§5) + +### 5.1 overall 자동/수동 계산 위치 = **서버** +- 클라(JS)는 6축 점수 + (선택적) overall 수동선택값만 전송. +- **서버가 권위 계산**: overall 미전송/빈값 → `rating = Math.round(평균(6축))`, `is_rating_manual=false`. overall 전송 → `rating=전송값(1~5 검증)`, `is_rating_manual=true`. +- 근거: 클라 계산은 변조 가능 + C3 서버집계 정합. round 정책 = 정수 반올림(HALF_UP, Java `Math.round` 는 .5 올림 — 일관). + +### 5.2 axes 재저장 전략 = **delete-all + insert-6** (updateReview) +- `editGameReview` TX 내: `deleteReviewAxes(reviewId)` → `addReviewAxes(reviewId, List<6>)`. +- 근거: upsert(ON CONFLICT) 대비 매퍼 단순. 6행 고정이라 성능 동일 수준. UNIQUE(review_id,axis_key) 와 무관(전삭제 후 삽입). + +### 5.3 트랜잭션 순서 (createReview) +``` +@Transactional +1. addGameReview(rating, body, is_rating_manual) → useGeneratedKeys → review.id +2. addReviewAxes(review.id, [6 axis rows]) → 6행 batch insert + (실패 시 전체 롤백 — review+axes 원자성) +3. getGameReview(review.id) + listReviewAxes(review.id) → reviewView 조립 +``` + +### 5.4 axes 매퍼 = **GameReviewAxesMapper 신규** (단일 책임) +신규 인터페이스. 시그니처(인자 사용목적 인라인 주석 — inflate 방지): +```java +@Mapper +public interface GameReviewAxesMapper { + // 1리뷰 6축 일괄 저장. List 1건당 review_id/axis_key/score 사용. + int addReviewAxes(@Param("reviewId") long reviewId, // FK 대상 리뷰 + @Param("axes") List axes); // 6행(axis_key+score) + + // 수정 시 기존 축 전삭제(이후 add 재삽입). + int deleteReviewAxes(@Param("reviewId") long reviewId); // 대상 리뷰 + + // 목록 화면 N리뷰 축 batch 조회(N+1 회피). + List listAxesByReviewIds(@Param("reviewIds") List reviewIds); // 페이지 리뷰 id들 +} +``` +- `ReviewAxisRow` = `{ Long reviewId; String axisKey; Integer score; }` (신규 POJO 또는 GameReviewData 내 정적 중첩). axes batch insert SQL은 `` (XML) 또는 @InsertProvider. **`#{}` 바인딩 유지**. +- 최소 인자 원칙 적용: gameId 등 불필요 컨텍스트 미수신. 구현에서 확장 필요 시 추가. + +--- + +## SVG 육각형 좌표 공식 (§6) + +### 6.1 계산 위치 = **JS 런타임** (game-detail.jsp 인라인) +- 근거: 개별 리뷰 N건 동적 렌더(buildReviewItem) + 요약 1건. 기존 buildStarDisplay 가 JS DOM 생성 idiom → 일관. JSTL 컴파일타임은 동적 N건과 부적합. + +### 6.2 viewBox / 기하 상수 +- 요약(큰) 육각형: `viewBox="0 0 200 200"`, 중심 `cx=100, cy=100`, 반지름 `R=80`. +- 개별 리뷰 카드(컴팩트): `viewBox="0 0 120 120"`, `cx=60, cy=60`, `R=44`. (페이지당 20건 DOM 고려.) +- 함수는 cx/cy/R 파라미터화하여 공용 1함수로. + +### 6.3 6 꼭지점(축 끝) 좌표 공식 +- 축 i(0~5, §2.1 axis_key 순서와 1:1): 각도 `θ_i = -90° + 60°*i` (12시 방향 시작, 시계방향). + ``` + angle_rad = (-90 + 60*i) * Math.PI / 180 + axisX_i = cx + R * Math.cos(angle_rad) + axisY_i = cy + R * Math.sin(angle_rad) + ``` +- 배경 그리드(외곽 육각형) = 위 6점 polygon. 단계 그리드 = R*0.25, R*0.5, R*0.75, R 4단계 동심 육각형(`scale = level/4` 곱). + +### 6.4 score 폴리곤 점 공식 +- 축 i 점수 `s_i`(1~5) → 반지름 비율 `ratio_i = s_i / 5`: + ``` + ptX_i = cx + R * (s_i/5) * Math.cos(angle_rad_i) + ptY_i = cy + R * (s_i/5) * Math.sin(angle_rad_i) + ``` +- `` (fill 반투명 + stroke). + +### 6.5 렌더 함수 시그니처 (inflate 방지 — 인자 사용목적 명시) +```js +// 6축 점수 배열을 육각형 SVG 엘리먼트로. 요약/카드 공용. +function buildHexRadar(scores /* [6] axis순 점수배열, score폴리곤 */, + cx /* 중심x, 좌표기준 */, + cy /* 중심y, 좌표기준 */, + R /* 반지름, 스케일 */) { ... return } +``` +- 최소 인자. 라벨 텍스트/색상은 함수 내부 상수(AXIS_LABELS_KO) 참조 → 인자 미부풀림. + +### 6.6 a11y (SVG 시각요소 텍스트 대체 — 필수) +- ``. +- 추가로 시각적 보조: 각 축 점수를 visually-hidden 표 또는 인접 `
      `(축명/점수) 동반. 폴리곤만으로 종료 금지(FINAL SPEC). + +--- + +## a11y radiogroup 스펙 (§7 — C6) + +현 별점위젯(JSP:1043-1049)은 `role=radiogroup`/`role=radio`/`aria-checked` 마크업은 있으나 **키보드 핸들러 없음**(click만, JSP:1553-1556). roving tabindex 도 없음. C6 = 완전 패턴 신규 추가. + +### 7.1 roving tabindex +- radiogroup 내 단일 tabstop: 선택된 radio `tabindex="0"`, 나머지 `tabindex="-1"`. 미선택 시 첫 radio `tabindex="0"`. +- 화살표 이동 시 tabindex 이전(`-1`)→대상(`0`) + `.focus()`. + +### 7.2 키 핸들러 (keydown) +| 키 | 동작 | +|---|---| +| `ArrowRight`, `ArrowUp` | 다음 별점(+1, max 5에서 정지 또는 wrap — **정지** 채택, LTR 별점 직관) | +| `ArrowLeft`, `ArrowDown` | 이전 별점(-1, min 1에서 정지) | +| `Home` | 1점 | +| `End` | 5점 | +| `Space`/`Enter` | 현재 focus radio 선택 확정 | +- 화살표는 이동+즉시선택(setRating) 동시 — radiogroup 표준. `preventDefault()` 로 스크롤 방지. + +### 7.3 aria 속성 +- 그룹 `role="radiogroup" aria-labelledby="<라벨id>"` (축별 라벨 id 6개). +- 각 `role="radio" aria-checked="true|false"`, 선택만 `aria-checked=true`. + +### 7.4 6위젯 공통화 +- 별점위젯 생성을 JS 함수 `buildStarRadioGroup(axisKey, labelId)` 로 추출 → 6축 입력 위젯 생성에 재사용. 각 위젯 독립 selectedScore 상태(객체 `axisScores[axisKey]`). 기존 단일 overall 위젯도 동일 함수로 통일(overall은 별도 또는 자동표시). +- 함수 시그니처(인자 사용목적): + ```js + // 1개 축 별점 radiogroup DOM 생성 + 키/클릭 핸들러 바인딩. + function buildStarRadioGroup(axisKey /* axisScores 키 + 상태귀속 */, + labelText /* 라벨/aria-label 텍스트 */) { ... } + ``` + +--- + +## B3 정규화 유틸 위치 (§8) + +- **현황**: 공용 `util` 패키지 없음(grep 확인). trimToNull/trimToEmpty 가 GameCommentController·GameReviewController·GameController 에 private 중복. +- **채택**: 신규 `com.pandoli365.bibimbap.util.TextNormalizer` (static util 클래스). 기존 private trim 메서드는 **이번 스코프에선 미통합**(회귀 리스크 — 댓글/리뷰 컨트롤러만 호출 추가). 통합 리팩터는 비목표. +- **시그니처** (인자 사용목적 인라인): + ```java + public final class TextNormalizer { + private TextNormalizer() {} + // 저장 위생: 제어문자(탭\t·개행\n\r 제외) 제거 + 외곽 trim. + // 내부 연속공백/줄바꿈 보존. null→null. + public static String normalize(String raw /* 원문, 정규화 대상 */) { ... } + } + ``` +- 규칙: `\t`(U+0009), `\n`(U+000A), `\r`(U+000D) 외의 C0/C1 제어문자(U+0000~U+001F, U+007F~U+009F) 제거 → 외곽 `strip()`. 내부 공백 미축약. +- 적용: createComment/updateComment content, createReview/updateReview body 에 검증 **전** 호출(정규화 후 길이 검사). + +--- + +## 파일 영향 맵 (§1 파일 소유권 + worker 분할) + +worker 분할은 **동시수정 충돌 회피** 기준. game-detail.jsp 는 단일 파일이라 1 worker 전담. + +| 변경 유형 | 경로 | 역할 | worker | +|---|---|---|---| +| 신규 | `db/.../docs/game-reviews-ddl.sql` (append) | axes 테이블+updated_at ALTER+is_rating_manual ALTER+stats VIEW 멱등 블록. :63 보수주석 갱신 | W-DDL | +| 변경 | `db/schema.sql` | 위 DDL 동기화(:136-137 주석 갱신) | W-DDL | +| 신규 | `src/.../util/TextNormalizer.java` | B3 정규화 static util | W-BE | +| 신규 | `src/.../mapper/GameReviewAxesMapper.java` | axes add/delete/listByReviewIds | W-BE | +| 신규 | `src/.../data/ReviewAxisRow.java` (또는 GameReviewData 중첩) | axes 행 POJO | W-BE | +| 변경 | `src/.../data/GameCommentData.java` | updatedAt + edited(비영속) 필드 추가 | W-BE | +| 변경 | `src/.../data/GameReviewData.java` | ratingManual + axes(Map/List 비영속) 필드 추가 | W-BE | +| 변경 | `src/.../mapper/GameCommentsMapper.java` | A2 LEFT JOIN+CASE, A3 updated_at/edited select, B1 offset/limit/sort, editGameComment updated_at | W-BE | +| 변경 | `src/.../mapper/GameReviewsMapper.java` | B1 offset/limit/sort, is_rating_manual select/insert/update, edit 시 axes 무관(별 매퍼) | W-BE | +| 신규 | `src/.../mapper/GameReviewStatsMapper.java` (또는 GameReviewsMapper 내 1메서드) | game_review_stats 1행 조회 | W-BE | +| 변경 | `src/.../controller/api/GameCommentController.java` | A1 재조회·commentView, B1 page/sort, B3 normalize | W-BE | +| 변경 | `src/.../controller/api/GameReviewController.java` | 다축 검증/저장, overall 자동·수동, B2 10자, B3, B1 page/sort, summary | W-BE | +| 변경 | `src/main/webapp/WEB-INF/views/game-detail.jsp` | 육각형 SVG, C1·C2·C4·C6, 페이지네이션·sort UI, 서버집계 표시, 6축 입력, A1 commentView 렌더 | W-FE | +| 변경 | `src/test/.../GameCommentControllerTest.java` | A1·A2·B3 신규 + 기존 12 유지 | W-TEST | +| 변경 | `src/test/.../GameReviewControllerTest.java` | 다축·B2·sort 신규 + 기존 13 유지 | W-TEST | +| (무변경 확인) | `src/test/.../BibimbapApplicationTests.java` | @MockBean 신규 매퍼(GameReviewAxesMapper 등) 추가 필요 시만 | W-TEST | + +**충돌 회피**: W-BE 가 매퍼/컨트롤러/data/util 동시 소유(상호 의존 — 단일 worker 권장). W-FE 는 jsp 단독. W-TEST 는 BE 완료 후 진행(시그니처 의존). W-DDL 독립 선행 가능. + +**BibimbapApplicationTests 주의**: 신규 `@Mapper` 빈(GameReviewAxesMapper, GameReviewStatsMapper)이 컨트롤러 생성자 주입되면 contextLoads 가 빈을 못 찾아 실패 → `@MockBean` 추가 필수. 이 추가는 31건 회귀의 ApplicationTests 1건이 깨지지 않도록 하는 **필수 조치**. + +--- + +## DDL 전문 (§2 — game-reviews-ddl.sql append 블록) + +아래를 `docs/game-reviews-ddl.sql` 끝에 append. 전부 IF NOT EXISTS / DO $$ 멱등. `db/schema.sql` 에도 동일 정의 동기화. + +```sql +-- =========================================================================== +-- W3-2 고도화: 다축 평점(game_review_axes) + 댓글 updated_at + is_rating_manual +-- + game_review_stats 집계뷰 +-- C3 재분류: game_review_stats 는 W3-2 일반 읽기전용 집계뷰. W2-3 잼 평가 동결과 무관 +-- (roadmap.md:63,201,202). 아래 위 'idx_game_reviews_game' 주석의 "집계뷰 미신설" +-- 보수 표기는 본 블록으로 갱신됨. +-- =========================================================================== + +-- 1) game_comments.updated_at (A3) +ALTER TABLE "game_comments" + ADD COLUMN IF NOT EXISTS "updated_at" timestamp with time zone DEFAULT now() NOT NULL; +-- 기존 댓글이 '수정됨' 오표시되지 않도록 정렬(멱등: 이미 정렬된 행엔 무영향) +UPDATE "game_comments" SET "updated_at" = "created_at" WHERE "updated_at" > "created_at"; +COMMENT ON COLUMN "game_comments"."updated_at" IS '덧글 마지막 수정 시각. updated_at > created_at 이면 수정됨(리뷰 대칭)'; + +-- 2) game_reviews.is_rating_manual (overall 출처 구분) +ALTER TABLE "game_reviews" + ADD COLUMN IF NOT EXISTS "is_rating_manual" boolean DEFAULT false NOT NULL; +COMMENT ON COLUMN "game_reviews"."is_rating_manual" IS 'true=유저 직접선택 overall, false=6축 자동평균'; + +-- 3) game_review_axes (다축 평점, 리뷰당 6행) +CREATE SEQUENCE IF NOT EXISTS "game_review_axes_id_seq"; +CREATE TABLE IF NOT EXISTS "game_review_axes" ( + "id" bigint DEFAULT nextval('game_review_axes_id_seq'::regclass) NOT NULL, + "review_id" bigint NOT NULL, + "axis_key" character varying(20) NOT NULL, + "score" smallint NOT NULL, + PRIMARY KEY ("id") +); +ALTER SEQUENCE "game_review_axes_id_seq" OWNED BY "game_review_axes"."id"; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_review_id_fkey') THEN + ALTER TABLE "game_review_axes" + ADD CONSTRAINT "game_review_axes_review_id_fkey" + FOREIGN KEY ("review_id") REFERENCES "game_reviews" ("id"); + END IF; +END +$$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_score_check') THEN + ALTER TABLE "game_review_axes" + ADD CONSTRAINT "game_review_axes_score_check" CHECK ("score" BETWEEN 1 AND 5); + END IF; +END +$$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_axis_key_check') THEN + ALTER TABLE "game_review_axes" + ADD CONSTRAINT "game_review_axes_axis_key_check" + CHECK ("axis_key" IN ('immersion','creativity','controls','completeness','sound','visual')); + END IF; +END +$$; + +CREATE UNIQUE INDEX IF NOT EXISTS "ux_game_review_axes_review_axis" + ON "game_review_axes" ("review_id", "axis_key"); +CREATE INDEX IF NOT EXISTS "idx_game_review_axes_review" + ON "game_review_axes" ("review_id"); + +COMMENT ON TABLE "game_review_axes" IS '리뷰 다축 평점(6축, 리뷰당 6행). axis_key 6종 각 1~5'; +COMMENT ON COLUMN "game_review_axes"."axis_key" IS '몰입성 immersion/창의성 creativity/조작성 controls/완성도 completeness/사운드 sound/비주얼 visual'; + +-- 4) game_review_stats (읽기전용 집계뷰 — 클라 평균계산 폐기 공급원) +CREATE OR REPLACE VIEW "game_review_stats" AS +SELECT + r."game_id" AS "game_id", + ROUND(AVG(r."rating")::numeric, 1) AS "avg_rating", + COUNT(*) AS "review_count", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='immersion'),1) AS "avg_immersion", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='creativity'),1) AS "avg_creativity", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='controls'),1) AS "avg_controls", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='completeness'),1) AS "avg_completeness", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='sound'),1) AS "avg_sound", + ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='visual'),1) AS "avg_visual" +FROM "game_reviews" r +LEFT JOIN "game_review_axes" a ON a."review_id" = r."id" +WHERE r."is_delete" IS NOT TRUE +GROUP BY r."game_id"; +COMMENT ON VIEW "game_review_stats" IS 'W3-2 일반 집계뷰(W2-3 동결 무관). 게임별 평균별점·리뷰수·6축평균'; +``` + +### db/schema.sql 동기화 판단 = **동기화 필수** +- 근거: schema.sql 은 dev 컨테이너 부트스트랩(line 16-18, docker-entrypoint-initdb.d 자동실행). axes 테이블/뷰/컬럼 없으면 dev 환경 매퍼 실행 실패. game_reviews 가 schema.sql:117-135 에 이미 정의되어 있으므로 동일 위치에 위 4블록 append + `:136-137` 동결 주석을 C3 갱신 주석으로 교체. + +--- + +## 대안 비교 (§ 선택적) + +| 안 | 장점 | 단점 | 채택? | +|---|---|---|---| +| axes 매퍼 신규 vs GameReviewsMapper 확장 | 단일책임/리뷰매퍼 비대화 방지 | 클래스 1개 추가 | **신규 채택** | +| hasMore(limit+1) vs total count | COUNT 쿼리 회피 | total 미표시 | **hasMore 채택** | +| sort: @SelectProvider 분기 vs 메서드 N분리 | ORDER BY만 상수분기, 메서드 1개 | provider 클래스 추가 | **provider 채택** | +| overall 계산 서버 vs 클라 | 변조방지·C3정합 | round 1회 서버부담(무시가능) | **서버 채택** | +| axes 재저장 delete+insert vs upsert | 매퍼 단순 | 6 DEL+6 INS | **delete+insert 채택** | +| SVG 좌표 JS vs JSTL | 동적 N건 일관 | 클라 계산 | **JS 채택** | +| is_rating_manual 채택 vs 미채택 | 자동/수동 출처 구분 | 1컬럼 | **채택**(concerns 재확인) | + +--- + +## 롤아웃 / 마이그레이션 + +1. **순서**: DDL 멱등 적용(dev: schema.sql 재부트 또는 game-reviews-ddl.sql 수동 실행) → BE 빌드/단위테스트 → FE → L3 스모크. +2. **역호환**: + - 기존 game_reviews 행은 axes 0행 상태 → game_review_stats 의 6축 평균 NULL(LEFT JOIN). 프론트는 NULL 축을 "데이터 없음"/0 처리. overall avg_rating 은 정상(rating 컬럼 기반). + - 기존 댓글: updated_at default now() + 정렬 UPDATE 로 edited=false 유지. + - is_rating_manual default false → 기존 리뷰는 자동평균 취급(축 없어도 rating 표시 영향 없음). +3. **롤백 경로**: 신규 객체(axes 테이블/stats 뷰/2 컬럼)는 비파괴(ADD/CREATE). 코드 롤백 시 DB 객체 잔존해도 무해(미참조). 뷰는 `DROP VIEW IF EXISTS game_review_stats`, 컬럼은 보존(비파괴) 권장. +4. **기존 리뷰 axes 백필**: 본 스코프 비대상(NULL 허용). 백필 필요 시 별도. + +--- + +## 검증 포인트 (verification-advisor 점검 acceptance criteria) + +### L1 단위 테스트 (회귀 + 신규) +- **AC-1**: 기존 31건 회귀 PASS — `./mvnw test` 결과 GameCommentControllerTest(12) + GameReviewControllerTest(13) + UserControllerCsrfTest(5) + BibimbapApplicationTests(1) 전부 GREEN. (DbUpdateQueryGeneratorTest 1건은 W3-2 무관, 영향 없음 확인.) +- **AC-2**: BibimbapApplicationTests 의 @MockBean 집합이 컨트롤러 생성자 의존 매퍼 전수를 커버 — 신규 매퍼(GameReviewAxesMapper, GameReviewStatsMapper 등) 추가 시 contextLoads PASS. +- **AC-3**: 신규 단위테스트 추가건 PASS (목록 §9). + +### L1 안전성 (sort 화이트리스트 — 집합 전수) +- **AC-4**: sort enum 매핑 전수 5건 보존 — 매퍼/provider 의 ORDER BY 분기가 §4 표의 5개 enum(`oldest`,`newest`(댓글),`newest`,`rating_desc`,`rating_asc`(리뷰))을 전수 커버. 검증: `grep -c 'ORDER BY' ` >= 5 AND 각 분기 SQL에 사용자 입력 변수 보간(`${`) 0건 — `grep -c '\${' <매퍼소스>` == 0 (전 매퍼 통틀어 W3-2 변경분). +- **AC-5**: SQL injection 면역 — 전 신규/변경 매퍼에서 `${` 동적치환 0건 (`grep -rc '\${' src/main/java/.../mapper/Game*Mapper.java` 합 == 0). offset/limit/sort 전부 `#{}` 또는 enum 상수. + +### L1 다축 (집합 전수 — 6축) +- **AC-6**: axis_key 집합 전수 6건 일치 — DDL CHECK·뷰 FILTER·앱 enum·JSP 라벨 4곳이 동일 6키(`immersion,creativity,controls,completeness,sound,visual`). 검증: `grep -c "'immersion'\|'creativity'\|'controls'\|'completeness'\|'sound'\|'visual'"` 패턴이 game-reviews-ddl.sql 의 axis_key CHECK 절에 6개 전수, stats VIEW FILTER 에 6개 전수. +- **AC-7**: createReview 가 6축 미만 입력 시 400 (전 축 필수). 단위테스트로 5축 입력 거부. +- **AC-8**: overall 자동평균 — 6축 [4,5,3,4,2,5] 입력·overall 미전송 시 rating=round(23/6=3.83)=4, is_rating_manual=false. overall=2 전송 시 rating=2, is_rating_manual=true. + +### L1 기타 신규 +- **AC-9**: B2 — 리뷰 본문 trim 후 9자 거부(400), 10자 통과. 댓글은 최소제한 없음(기존 유지). +- **AC-10**: B3 — TextNormalizer.normalize("ab\tc") == "ab\tc"(NUL 제거, 탭 보존), 외곽 trim, 내부 공백 보존. +- **AC-11**: A1 — POST/PUT 댓글 응답이 commentView 전체 키(commentId,gameId,authorName,userId,content,createdAt,edited,updatedAt) 포함. +- **AC-12**: A2 마스킹 — 매퍼 SQL CASE 3분기 존재(`u.id IS NULL`→nickname, `u.is_delete`→'(탈퇴한 사용자)', else→display_name). LEFT JOIN 확인. +- **AC-13**: A3 — editGameComment 가 updated_at=now() 설정, nickname 미덮어씀. + +### L3 브라우저 스모크 체크리스트 +- **AC-14**: 별점위젯 키보드 — Tab 으로 위젯 1회 진입(roving), ←→↑↓ 점수 이동, Home=1/End=5, 선택 시 aria-checked 토글. 6축 위젯 각각 독립 동작. +- **AC-15**: 육각형 레이더 — 요약 SVG(6축 평균) + 개별 리뷰 카드 컴팩트 SVG 렌더. `` 6축 점수 텍스트 대체 존재. +- **AC-16**: 페이지네이션 — 21건 이상 리뷰/댓글 시 "더보기" 노출, 클릭 시 다음 20건 append, hasMore=false 면 버튼 숨김. sort 토글 동작(댓글 oldest↔newest, 리뷰 newest/rating_desc/rating_asc). +- **AC-17**: 서버집계 표시 — 요약 평균 소수1자리, review_count=0 시 "아직 평가 없음". 클라 평균계산(updateSummary 구버전) 잔존 0 — `grep -c 'list.reduce' game-detail.jsp` 의 평균계산 블록 제거 확인. +- **AC-18**: C1 submit 잠금 — 제출 중 버튼 disabled, 응답 후 해제(연타 차단). C4 글자수 카운터 실시간(댓글 n/200, 리뷰 n/1000 + 최소10자 안내). C2 상대시각 "n분 전" + title 절대시각. +- **AC-19**: DDL 적용 — game_review_axes·game_review_stats·game_comments.updated_at·game_reviews.is_rating_manual 존재 (psql `\d` 또는 information_schema). + +### AC 정식화 self-audit (시점·표현 — 프로토콜 §4.7) +1. **시점 안정성**: AC-1 의 31건은 현재 세션이 W-TEST 로 테스트를 **추가**하므로 verification 시점엔 31+α건이 됨. → 표현 교정: "기존 31건이 전부 PASS(삭제·실패 0)" 불변식으로 측정, 신규는 AC-3 별도. 자기 트리 고정카운트 함정 회피. +2. **표현 견고성**: AC-5/AC-17 의 `grep -c` 는 리터럴 의존 — `${` 는 SQL injection 신호로 안정적(동의표현 없음, 견고). AC-17 'list.reduce' 는 구현이 다른 메서드명 쓸 수 있어 fragile → "클라 평균계산 로직 부재"를 수동 코드리뷰로 보강(grep 은 보조). AC-6 axis_key 는 6키 고정 리터럴 — DDL/뷰/앱 동의표현 없이 정확히 이 6문자열만 유효(견고). + +--- + +## verification 실행 명령 (L1) +- 빌드툴 = **Maven**(pom.xml 확인, build.gradle 없음). 래퍼 `./mvnw`. +- L1: `./mvnw -q test` (전체) 또는 `./mvnw -q test -Dtest=GameCommentControllerTest,GameReviewControllerTest,UserControllerCsrfTest,BibimbapApplicationTests` (회귀 31건 집중). +- 컴파일만: `./mvnw -q test-compile`. diff --git a/.atp/work-session/20260622-092800/implementation/fix-L1.md b/.atp/work-session/20260622-092800/implementation/fix-L1.md new file mode 100644 index 0000000..38f3e86 --- /dev/null +++ b/.atp/work-session/20260622-092800/implementation/fix-L1.md @@ -0,0 +1,68 @@ +--- +phase: implementation +agent: implementation-advisor +agent_version: 1 +generated_at: 2026-06-22T10:10:00+09:00 +fix_loop: L1 +concerns: [] +concerns_checked: true +planned_workers: 1 +actual_workers: 0 +workers_spawned: 0 +--- + +# FIX LOOP L1 — GameReviewControllerTest RED 8건 해소 + +## 근본 원인 (확정) +**가설 1 (기존/신규 테스트 계약 미갱신) 단독.** 컨트롤러 버그(가설 2) 아님. + +- 실패 8건 전부 POST body 가 BODY_MIN(10자) 미만 → 컨트롤러가 design.md AC-9 + "리뷰 본문 trim 후 9자 거부(400), 10자 통과" 에 따라 **정당하게 400** 반환. +- 컨트롤러 axes 바인딩은 무관: 테스트가 `controller.createReview(...)` 를 6개 위치 인자로 + 직접 호출(`createReview`/`updateReview` 래퍼, test:409-421) → MockMvc 폼바인딩 경로 미사용. + `@RequestParam` 평면 파라미터(immersion=…) 구성과도 무관. 검증 순서도 design §5.3 정합 확인 + (CSRF→auth→game존재→axes파싱→body검증→overall계산→conflict→insert→axes insert). +- conflict 테스트(409 기대)도 동일 원인: 1st POST 의 body 가 3자("또작성") 라 body 검증에서 + 400 으로 죽어 conflict(169행) 까지 도달 못 함. + +### 실패 8건 본문 길이 (수정 전) +| 테스트 | 본문(전) | 길이 | 수정 후 본문 | 길이 | +|---|---|---|---|---| +| createReviewReturnsEditedFalse:72 | 재미있음 | 4 | 정말 재미있는 게임입니다 | 13 | +| updateReviewReturnsEditedTrue:90 | 더 좋아짐 | 5 | 수정 후 훨씬 더 좋아졌습니다 | 16 | +| createSecondReviewReturnsConflict:109 | 또작성 | 3 | 같은 게임에 또 작성 시도합니다 | 17 | +| assertCreateRatingAccepted(1/5 경계) | 내용입니다열자 | 7 | 별점 경계값 통과 검증 본문입니다 | 18 | +| createReviewAutoAveragesOverall…:258 | 자동평균테스트 | 7 | 별점 자동평균 계산 검증 본문 | 16 | +| createReviewUsesManualOverall…:281 | 수동평점테스트 | 7 | 수동 별점 사용 검증 본문입니다 | 17 | +| createReviewPersistsSixAxes:306 | 여섯축저장테스트 | 8 | 여섯 축 저장 검증용 본문입니다 | 17 | + +## 수정 내역 (테스트만, 컨트롤러/계약 불변) +파일: `src/test/java/com/pandoli365/bibimbap/controller/api/GameReviewControllerTest.java` + +7개 지점 본문 문자열 교체(헬퍼 `assertCreateRatingAccepted` 1곳이 rating 1/5 두 boundary 케이스 커버 → 실패 8건): +- 72행, 90행, 109행, 258행, 281행, 306행 직접 교체 +- `assertCreateRatingAccepted` 본문(402행) 교체 → createReviewAcceptsRatingOneBoundary / FiveBoundary 동시 해소 + +추가(우연통과 배제, §2.6 다항목 재검): +- `assertCreateRatingRejected` 본문(382행) "내용입니다열자"(7자)→"별점 범위 검증용 본문입니다"(15자) 로 상향. + 기존 7자였다면 rating 0/6 거부를 검증하려 해도 body 검증(400)에서 먼저 걸려 **우연히** 400 통과할 + 위험이 있었음. body 를 10자 이상으로 올려 rating 검증 분기까지 실제 도달함을 보장. + +**행위 검증 전부 보존**: edited 마커(false/true), 게임당1회 409(never addGameReview), +rating 1~5 경계, overall 자동평균(rating=4·ratingManual=false)/수동(rating=2·manual=true), +addReviewAxes 6행 — 모든 assertion 원형 유지. 검증을 무력화하거나 계약을 약화하지 않음. + +## 공유 문자열 일괄치환 회피 (정밀성) +"내용입니다열자"(7자) 는 통과 중인 CSRF/forbidden/missing-axis 테스트(190/204/235행)에도 쓰임. +그 테스트들은 본문 검증 **이전 단계**(CSRF 403 / 비작성자 FORBIDDEN / axis 누락 400)에서 분기되어 +짧은 본문이어도 정상 통과 → 의도된 단축 본문. 따라서 일괄 치환 금지, 성공경로 헬퍼 2곳만 정밀 교체. + +## 실행 결과 +- `./mvnw test -Dtest=GameReviewControllerTest` → **Tests run: 20, Failures: 0** (GREEN) +- `./mvnw test` (전체) → **Tests run: 43, Failures: 0, Errors: 0, Skipped: 0** (회귀 0) + - GameCommentControllerTest 17/17, GameReviewControllerTest 20/20, + UserControllerCsrfTest 5/5, BibimbapApplicationTests 1/1 + +## 손대지 않은 항목 (지시 준수) +- GameReviewController.java 등 main 소스 무수정 (계약·검증순서 design §5 정합 확인만). +- ${}0 / sort5 / axis6 grep 검증, DbUpdateQueryGeneratorTest — 미접촉. diff --git a/.atp/work-session/20260622-092800/implementation/ownership.md b/.atp/work-session/20260622-092800/implementation/ownership.md new file mode 100644 index 0000000..5976517 --- /dev/null +++ b/.atp/work-session/20260622-092800/implementation/ownership.md @@ -0,0 +1,60 @@ +--- +phase: implementation +agent: implementation-advisor +agent_version: 1 +generated_at: 2026-06-22T10:00:00+09:00 +--- + +# 파일 소유권 맵 (W3-2 댓글/리뷰 고도화) + +설계 §1 파일 영향 맵 기준. 동일 파일 = 정확히 1 worker. 상호의존 파일은 단일 worker 묶음. + +| 파일 | 담당 worker | worker id | 변경 유형 | 의존 | +|---|---|---|---|---| +| docs/game-reviews-ddl.sql | migration-writer | W-DDL | append+주석갱신 | - | +| db/schema.sql | migration-writer | W-DDL | 동기화+주석갱신 | - | +| src/.../util/TextNormalizer.java | code-writer | W-BE | create | - | +| src/.../data/ReviewAxisRow.java | code-writer | W-BE | create | - | +| src/.../mapper/GameReviewAxesMapper.java | code-writer | W-BE | create | ReviewAxisRow | +| src/.../mapper/GameReviewStatsMapper.java | code-writer | W-BE | create | - | +| src/.../data/GameCommentData.java | code-writer | W-BE | modify | - | +| src/.../data/GameReviewData.java | code-writer | W-BE | modify | - | +| src/.../mapper/GameCommentsMapper.java | code-writer | W-BE | modify | GameCommentData | +| src/.../mapper/GameReviewsMapper.java | code-writer | W-BE | modify | GameReviewData | +| src/.../controller/api/GameCommentController.java | code-writer | W-BE | modify | 위 매퍼/data/util | +| src/.../controller/api/GameReviewController.java | code-writer | W-BE | modify | 위 매퍼/data/util | +| src/main/webapp/WEB-INF/views/game-detail.jsp | code-writer | W-FE | modify | (BE API 계약 — 설계 §3 고정) | +| src/test/.../GameCommentControllerTest.java | code-writer | W-TEST | modify | W-BE 시그니처 | +| src/test/.../GameReviewControllerTest.java | code-writer | W-TEST | modify | W-BE 시그니처 | +| src/test/.../BibimbapApplicationTests.java | code-writer | W-TEST | modify | 신규 @Mapper | + +## 불변식 점검 +- W-BE 가 매퍼/data/util/컨트롤러 11파일 동시 소유 (상호 시그니처 의존 → 단일 worker 순차). +- W-FE jsp 단독 — BE 와 파일 충돌 0. API 계약은 설계 §3 으로 고정되어 BE 코드 미참조 가능 → 병렬 가능. +- W-DDL 2파일 독립 — 코드 무참조 → 선행 병렬 가능. +- W-TEST 는 W-BE 시그니처 확정 필요 → W-BE 완료 후 순차 spawn. + +## spawn 순서 +1. wave A (병렬): W-DDL, W-FE, W-BE ← 상호 파일 충돌 0 +2. wave B (순차): W-TEST ← W-BE 완료 후 (컨트롤러 시그니처 의존) + +## planned_workers: 4 (W-DDL, W-FE, W-BE, W-TEST) + +--- + +# 파일 소유권 맵 (FIX LOOP L1 — verification RED 8건) + +| 파일 | 담당 worker | worker id | 변경 유형 | 의존 | +|---|---|---|---|---| +| src/test/.../GameReviewControllerTest.java | (advisor 직접) | - | modify | - | + +## L1 planned_workers vs actual_workers +- planned_workers: 1 +- actual_workers: 0 (advisor 직접) +- 전환 사유: 파일 수 1 < 8 AND 예상 변경 줄수 ~8줄 < 500 → advisor 직접 실행 선택. 정밀 문자열 치환 8곳, worker spawn overhead 가 작업량 초과. + +## L1 근본 원인 (진단 결과) +- **가설 1 (테스트 계약 미갱신) 단독 확정.** 컨트롤러 버그 아님. +- 실패 8건 전부 POST body 10자 미만 → 컨트롤러가 design.md AC-9(본문 trim 후 10자 미만 거부) 에 따라 정당하게 400 반환. +- 가설 2(axes 바인딩) 무관: 테스트가 controller.createReview(...) 를 6개 위치 인자로 직접 호출 → MockMvc 폼바인딩 경로 미사용. 검증 순서도 design §5.3 정합 확인. +- 수정 방향: 컨트롤러/FINAL SPEC 계약 불변. 테스트 요청 body 만 10자 이상으로 갱신 (행위 검증 보존). diff --git a/.atp/work-session/20260622-092800/report.md b/.atp/work-session/20260622-092800/report.md new file mode 100644 index 0000000..22d4b64 --- /dev/null +++ b/.atp/work-session/20260622-092800/report.md @@ -0,0 +1,250 @@ +--- +schema_version: 2 +sid: 20260622-092800 +resumed_from: 20260618-145152 +started_at: 2026-06-22T09:28:00+09:00 +ended_at: 2026-06-22T10:05:00+09:00 +user_request: | + W3-2 댓글/리뷰 고도화 구현 — 직전 인터뷰 세션(20260618-145152)의 FINAL SPEC 으로 구현 재개. + 진행 방식 = FINAL SPEC 전체 한 번에 구현(옵션1, 사용자 확정). 단계 분리 없음. + 스코프: 다축 평점(game_review_axes) + 육각형 SVG 레이더 + A1~A3 일관성 + B1~B3 목록/검증 + + C1~C6 UX + game_review_stats 집계뷰. 검증: L1 31건 회귀 PASS + 신규 단위테스트, L3 스모크. +mode: team (orchestrator + advisors + workers) +--- + +# Advisor Invocation Decision Log +# 각 advisor 호출/스킵 판단 즉시 1줄 append +- advisor: requirements-advisor + decision: skip + rationale: '직전 인터뷰 세션이 FINAL SPEC 까지 요구 분해 완료. 사용자 결정 잠금(옵션1). 재분해 불필요.' + checked_at: 2026-06-22T09:28:00+09:00 +- advisor: research-advisor + decision: skip + rationale: 'Grounded Baseline(직전 report) + design-advisor 자체 코드 재확인으로 충분. graphify-lookup 도 생략 — 변경 대상 7파일이 직전 세션에 정밀 grounding 됨, 코드 무드리프트(W3-2 코어 이후 dev seed 커밋만).' + checked_at: 2026-06-22T09:28:30+09:00 +- advisor: design-advisor + decision: call + rationale: 'FINAL SPEC → 오픈질문0 확정 설계도. 미결 마이크로결정(is_rating_manual·axis 저장형·sort enum·SVG 좌표·a11y·B3 유틸·테스트목록) 확정 필요.' + checked_at: 2026-06-22T09:28:30+09:00 +- advisor: implementation-advisor + decision: call + rationale: '14파일 영향(신규6+변경8) 멀티파일 + worker 분산 필요. design 산출이 파일소유권맵까지 확정 — 직접구현 대신 implementation-advisor 의 worker fan-out 으로 충돌회피.' + checked_at: 2026-06-22T09:34:00+09:00 + +# C3 정정 (직전 report 오분류 교정 — 근거 확정) +# 직전 report: C3 를 "W2-3 동결 해제·고위험·§6 게이트"로 표기 → 과한 분류. +# 근거 (docs/work-log/2026-06-17-jam-platform-roadmap.md): +# :63 "범위 = 잼 평가만. 댓글/리뷰 스키마 자체는 W3에서 설계(동결 묶음 아님)" +# :201 "잼 평가(심사/투표/시상) 스키마만 동결. 댓글/리뷰 스키마는 W3-2에서 별도 설계" +# :202 "댓글/리뷰 분리 = 일반기능으로 재분류" +# 판정: game_review_stats = game_reviews(W3-2 테이블) 위 읽기전용 집계뷰. 동결 잼평가 테이블 무관. +# → §6 파괴적 게이트·사용자 재확인 불필요. 일반 CREATE VIEW. +# → game-reviews-ddl.sql:63 "집계뷰 미신설" 보수 주석도 갱신. + +# Summary +W3-2 댓글/리뷰 고도화 FINAL SPEC 전체 한 묶음 구현 완료. resumed_from 20260618-145152. +- C3 정정: game_review_stats 집계뷰 = W3-2 일반 DDL(고위험 게이트 해제, roadmap:63,201,202 근거). +- DDL: game_review_axes(6축·리뷰당6행)·game_comments.updated_at·game_reviews.is_rating_manual·game_review_stats 뷰 — 멱등 ALTER/CREATE (ddl + db/schema.sql 동기화, C3 보수주석 갱신). +- 백엔드: A1 commentView 통일·A2 하이브리드 작성자명+탈퇴자 마스킹(QG-2 해결)·A3 edited/updated_at, B1 페이지네이션(limit+1 hasMore)+sort 화이트리스트(@SelectProvider, ${}0), B2 본문10자, B3 TextNormalizer, 다축 overall 자동평균/수동(is_rating_manual). +- 프론트(frontend-design 스킬): 육각형 SVG 레이더(요약+카드)·6축 radiogroup(roving tabindex+키핸들러)·C1~C6, production-grade 폴리시. +- 검증: L1 ./mvnw test 43/43 GREEN(회귀0). FIX LOOP 1회(테스트 계약 미갱신 8건, 컨트롤러 무버그 — body<10자 정당거부). L2 skip. L3 = needs_user_verification. +- 신규 4파일 + 13 수정. 미커밋(프로젝트 정책: 명시 요청 시만). + +# Invocations +- agent: design-advisor + phase: design + model_choice: { tier: large, slug: opus, effort: high, rationale: '신규 SVG 좌표수학+스키마설계+보안민감 sort enum+a11y — 판단천장 높음' } + artifact: .atp/work-session/20260622-092800/implementation/design.md + result: 'PASS — 오픈질문0 확정 설계도. concerns 7건. self-verification passed.' +- agent: implementation-advisor + phase: implementation + model_choice: { tier: large, slug: opus, effort: high, rationale: '멀티파일 멀티워커 조율 + 보안가드(${}금지)+회귀보호 — 천장 높음' } + result: 'PASS — BUILD SUCCESS(main+test, JDK21). 16파일. ${} 0건(rg 3매치=주석). axis_key 6키 4곳 정합. 리뷰 INNER JOIN 보존. is_rating_manual 실사용. advisor 직접보정 4건(GameController 영향맵누락/테스트 worker충돌 재작성/NUL escape/@MockBean).' + artifact: .atp/work-session/20260622-092800/implementation/ownership.md + concerns: ['설계 §1 영향맵에 GameController.gameDetail SSR 호출지점 누락→컴파일깨짐(보정완료)', 'applyReviewGate: 본인리뷰 첫페이지밖일때 폼노출 가능(서버 409 최종차단, 비차단)', '댓글 더보기 낙관삽입 2페이지+ 정렬어긋남 가능(리뷰는 page0 재조회로 정합, 비차단)'] +- agent: frontend-design (skill pass) + phase: implementation-fe + model_choice: { tier: large, slug: opus, effort: high, rationale: 'production-grade UI 디자인 — 사용자 명시 요청(frontend-design 스킬 활용)' } + result: '(L1 green 후 진입)' +- agent: verification-advisor (1차) + phase: verification + model_choice: { tier: medium, slug: sonnet, effort: medium, rationale: 'L1 테스트 실행+AC 판정' } + result: 'RED — GameReviewControllerTest 8건 FAIL(전부 400). AC-5/4/6 PASS. 댓글·CSRF·context 무회귀.' +- agent: implementation-advisor (FIX LOOP §2.6) + phase: implementation-fix + model_choice: { tier: large, slug: opus, effort: high, rationale: '회귀단계 진단+수정' } + result: 'PASS — 근본원인=테스트 계약 미갱신(컨트롤러 무버그). body<10자 7곳→10자+ 교체, 행위 assertion 보존, 계약 무약화. main 무수정. ./mvnw test 43/43 GREEN 회귀0.' + artifact: .atp/work-session/20260622-092800/implementation/fix-L1.md +- agent: verification-advisor (2차 재판정) + phase: verification + model_choice: { tier: medium, slug: sonnet, effort: medium, rationale: 'L1 독립 재판정 + 신규행위 AC 커버리지' } + result: 'GREEN 확정 — 43/43(Failures 0). AC-1/3 PASS, AC-7~13 전부 테스트 실재+assertion 확인, AC-4/5/6 재확인. 로그 clean. L2 skip(원격DB). GAP 0.' + artifact: .atp/work-session/20260622-092800/verification.md +- agent: frontend-design (skill pass, fork) + phase: implementation-fe + model_choice: { tier: large, slug: opus, effort: high, rationale: 'production-grade UI — 사용자 명시 요청(frontend-design 스킬)' } + result: 'PASS — game-detail.jsp 단일파일 승격. 육각형 레이더 시그니처화(방사 그라디언트·글로우·등장모션), 미터바 범례, 히어로 요약카드, 6축입력/정렬토글/더보기 폴리시. 기존 앰버/크림 언어 확장(새 디자인시스템0·외부의존0). JS로직 보존(유일변경=poly fill→presentation attr). a11y/데이터계약 보존. 프리뷰 HTML 산출.' + artifact: .atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html + +# Decisions +- C3 재분류: game_review_stats 집계뷰 = 일반 DDL(고위험 게이트 해제). roadmap:63,201,202 근거. [2026-06-22] +- is_rating_manual 컬럼 채택: overall 자동평균(false)/수동선택(true) 출처 구분. 멱등 ADD COLUMN DEFAULT false. [2026-06-22] +- axis_key 저장형 = 영문 snake 6종 순서고정(immersion/creativity/controls/completeness/sound/visual) = 육각형 축 0~5 1:1. [2026-06-22] +- sort 안전 = @SelectProvider 컴파일타임 상수 ORDER BY 5분기. 사용자입력→enum 매칭만, ${} 보간 0. offset/limit=#{}. [2026-06-22] +- pagination = limit+1 hasMore (COUNT 회피). axes 재저장 = delete-all+insert-6 (TX 원자성). [2026-06-22] +- SVG 육각형 = JS 런타임 buildHexRadar(scores,cx,cy,R). role=img + 축점수 텍스트대체. [2026-06-22] +- db/schema.sql 동기화 필수 (dev 컨테이너 부트스트랩). game-reviews-ddl.sql:63 + schema.sql:136-137 C3 보수주석 갱신. [2026-06-22] + +# user_signals +positive: [] +negative: [] + +# verified_by_me +- L1: unit+regression — ./mvnw test 43/43 GREEN (Failures 0, Errors 0). 기존 31건 무회귀. verification-advisor 2차 독립 재판정. +- L1 신규행위: AC-7(6축필수 400)·AC-8(자동평균/수동 overall)·AC-9(B2 10자 경계)·AC-11(A1 commentView 8키)·AC-12(A2 마스킹 CASE)·AC-13(A3 updated_at) 전부 테스트 실재+PASS. +- 보안: AC-5 ${} 동적치환 0건(sort=enum 고정 ORDER BY 상수, offset/limit=#{}). AC-4 sort5 분기. AC-6 axis_key 6키 4곳 정합. +- L2: contract-DB — skipped:원격DB/dev 미기동. +- 로그 스캔: clean (Mockito JDK agent warn·trailing-slash warn = known-ok 비차단). + +# needs_user_verification +- **DDL 적용**: docs/game-reviews-ddl.sql 멱등 블록(+ db/schema.sql) 을 dev/운영 DB 에 적용. 신규 객체 = game_review_axes 테이블·game_review_stats 뷰·game_comments.updated_at·game_reviews.is_rating_manual. 전부 비파괴 ADD/CREATE. (dev = schema.sql 재부트 or ddl 수동 실행.) +- **L3 브라우저 스모크**(DDL 적용 후 dev 실게임 대상 — seed-dev.sql 더미게임. 폴백 정적게임은 댓글/리뷰 미동작): + - 육각형 레이더 렌더(요약 + 개별 리뷰 카드), 라이트/다크 양 테마. + - 6축 별점 키보드(Tab 1회 진입 roving, ←→↑↓·Home·End, aria-checked 토글), 6위젯 독립. + - 페이지네이션 "더보기"(21건+ append, hasMore=false 숨김), 정렬 토글(댓글 oldest↔newest, 리뷰 newest/rating_desc/rating_asc). + - 다축 입력→overall 자동평균 표시 + 수동 덮어쓰기, B2 10자 안내, C4 글자수 카운터, C2 상대시각, C1 submit 잠금. + - 서버집계 요약(평균 소수1자리, review_count=0 "아직 평가 없음"). +- **UI 프리뷰 열람**(선택): .atp/work-session/20260622-092800/artifacts/w3-2-ui-preview.html 브라우저로 열어 컴포넌트 시각 확인. + +# graph_refresh +판정: fully-stale (src + docs 양 scope). graph-refresh-checker 근거 = W3-2 코어(커밋됨)+본세션 고도화로 컨트롤러2·매퍼·data·DDL 신규 → 기존 384노드/613엣지에 댓글/리뷰 토폴로지 부재. 삭제대상 없음. +처리: 완료(no-defer 준수). 커밋 b9d836d 직후 /graphify src/ + docs/ 재생성(메인스레드 — fork 의 Agent spawn 제약으로 폴백). src=536노드/1206엣지/20커뮤니티(리뷰 다축·댓글 commentView·edited 토폴로지 반영), docs=119노드/164엣지/15커뮤니티(C3 재분류·육각형·axes/stats DB스키마·검증교훈 군집). docs/graph/index.md source_commit=b9d836d·Scopes 2행 갱신. transient 캐시 정리 확인. graph 메타 커밋 375a2de(본체 .gitignore). + +# open_items +- 미커밋 변경(작업단위 속함): 13 수정 + 4 신규(data/ReviewAxisRow.java·mapper/GameReviewAxesMapper.java·mapper/GameReviewStatsMapper.java·util/TextNormalizer.java). 커밋은 CLAUDE.md 정책상 사용자 명시 요청 시만 — 세션 종료 시 사용자 확인. +- work-session 디렉토리(20260622-092800, 20260618-145152) untracked — 동일 커밋 정책. +- 비차단 후속(차기 세션 후보): GET /reviews/mine(본인리뷰 즉시식별 — applyReviewGate 정밀화), 댓글 더보기 page0 재조회 전환(낙관삽입 정렬정합), 기존 리뷰 axes 백필(현재 NULL 허용), 설계 영향맵에 SSR 호출지점(GameController.gameDetail) 포함. + +# Retrospective + +```yaml +Retrospective: + signals: + positive: + - quote_or_paraphrase: "(세션 report) frontend-design 스킬 fork 위임 → 스킬 호출+단일파일 폴리시+프리뷰 render-verify, 가드레일(JS로직·a11y·계약 보존) 준수. 다축/육각형/sort안전(${}0) 한 묶음 구현 + L1 43/43 GREEN." + about: frontend-design 스킬을 컨텍스트 상속 fork 로 위임하고, 단일 JSP 파일 폴리시·프리뷰 HTML 산출·가드레일 준수까지 한 단계에 완료한 패턴 전체 + - quote_or_paraphrase: "(invocation log) implementation-advisor 직접보정 4건 완료 — 영향맵 누락 보정·테스트 worker충돌 재작성·NUL escape·@MockBean" + about: 설계 누락이 발생했음에도 implementation-advisor 가 자가 보정으로 컴파일 깨짐을 세션 내에서 흡수해 사용자 에스컬레이션 없이 완료한 점 + negative: + - quote_or_paraphrase: "(implementation-advisor concerns) 설계 §1 영향맵에 GameController.gameDetail SSR 호출지점 누락 → 컴파일깨짐(보정완료)" + about: design-advisor 가 §1 파일 영향맵 작성 시 API 컨트롤러만 기재하고, 변경된 매퍼/data 심볼을 SSR 뷰모델에서 호출하는 GameController.gameDetail 을 누락한 것 + structural: true + - quote_or_paraphrase: "(verification-advisor 1차) GameReviewControllerTest 8건 FAIL — 전부 400. 근본원인=테스트 계약 미갱신(body<10자). 컨트롤러 무버그." + about: B2 본문 최소 10자 검증계약 신규 도입 시, W-TEST worker 가 기존 테스트 fixture body 를 계약 정합 감사 없이 재작성하여 기존 픽스처가 여전히 10자 미만 문자열을 전송한 것 + structural: true + + what_went_well: + - "frontend-design 스킬 fork 위임 패턴: 컨텍스트 상속으로 BE API 계약·기존 JS 로직·a11y 가드레일을 그대로 이어받아, 육각형 SVG 레이더·6축 radiogroup·C1~C6 를 단일 JSP 파일 승격 + 프리뷰 HTML 산출까지 한 묶음 완료. 외부 의존 0·기존 디자인 언어 확장·a11y 보존 전부 통과. 비자명 판단(스킬 fork + 단일파일 폴리시 조합)이 검증됨." + - "C3 phantom 게이트 해소: 직전 report 의 '동결 해제·고위험·§6 게이트' 표기를 근거 문서(roadmap:63,201,202) 직접 확인으로 일반 DDL 재분류. 불필요한 사용자 재확인 경로 없이 세션 내 정정 완료." + - "FIX LOOP 자가흡수: 테스트 RED 8건 발생 후 implementation-advisor 가 '컨트롤러 무버그/테스트 계약 미갱신' 근본원인을 정확히 진단하고, 행위 assertion 전부 보존·계약 약화 없이 픽스처만 교체 → 사용자 에스컬레이션 0." + - "sort 안전 패턴: @SelectProvider + enum 화이트리스트 → 고정 ORDER BY 상수 5분기. ${} 동적 보간 0건 rg 전수 확인. 보안 AC-5 최초 시도 PASS." + + what_to_improve: + - "design-advisor 영향맵에 SSR/뷰모델 호출지점 포함 필수: §1 파일 영향맵은 API 컨트롤러뿐 아니라 변경 심볼(매퍼 메서드 시그니처·data 필드)을 호출하는 SSR 컨트롤러(GameController.gameDetail 등) 와 JSP 뷰모델 바인딩 지점까지 포함해야 한다. 누락 시 implementation 단계 컴파일 깨짐." + - "계약 강화(최소길이·필수필드) 시 기존 테스트 fixture 전수 계약 정합 감사 필수: 신규 검증계약(B2 본문 10자·6축 필수)을 도입할 때, 신규 테스트 케이스 추가만이 아니라 기존 테스트 fixture 가 신규 계약을 충족하는지 전수 감사를 W-TEST 체크리스트에 명시해야 한다. 이번 8건 FAIL 은 모두 컨트롤러 버그 없이 fixture 미갱신 단독 원인." + - "freeze/동결 인접성만으로 고위험 게이트 분류 금지 — 근거 문서 직접 확인 선행: '동결 구역 인근 작업'이라는 표면적 유사성만으로 §6 게이트 표기 시 phantom 게이트가 누적된다. 동결 범위를 근거 문서(ADR/roadmap 줄 번호)로 확인 후 분류해야 한다." + + memory_candidates: + - name: design-impact-map-includes-ssr-callers + type: feedback + description: "design-advisor §1 영향맵에 변경 심볼의 SSR/뷰모델 호출지점 포함 필수" + body_draft: | + ## Why + W3-2 고도화 세션(20260622)에서 design-advisor 가 GameController.gameDetail 의 SSR 호출지점을 + 영향맵에서 누락 → implementation 단계 컴파일 깨짐, advisor 직접 보정 필요. + API 컨트롤러 목록만으로는 "변경 심볼을 참조하는 비API 코드" 를 놓친다. + + ## How to apply + design 단계 §1 파일 영향맵 작성 시: + 1. 변경 매퍼/data 심볼 목록 확정. + 2. 해당 심볼을 호출하는 SSR 컨트롤러(예: GameController.gameDetail) 와 + JSP 뷰모델 바인딩 지점을 rg 로 전수 확인(패턴: 심볼명). + 3. API 컨트롤러와 SSR 컨트롤러를 구분해 양쪽을 영향맵에 기재. + 재현성: Spring MVC SSR + API 컨트롤러 혼재 구조에서 항상 발생 가능. + rationale_for_saving: "API/SSR 혼재 Spring MVC 에서 구조적으로 재발 가능. 코드/git log 으로 유도 불가 — 설계 단계 체크리스트 규약에만 존재." + signal_source: negative + docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md + memory_optional: true + + - name: contract-upgrade-fixture-audit + type: feedback + description: "검증계약 강화 시 기존 테스트 fixture 전수 계약 정합 감사를 W-TEST 체크리스트에 포함" + body_draft: | + ## Why + W3-2 고도화 세션(20260622)에서 B2(본문 10자)·6축 필수 계약을 신규 도입 후 + W-TEST worker 가 기존 fixture body 를 갱신하지 않아 8건 전부 400 FAIL. + 컨트롤러는 무버그였고, fixture 미갱신 단독 원인. + 신규 테스트 추가만으로는 기존 픽스처가 새 계약을 충족하는지 보장되지 않는다. + + ## How to apply + W-TEST worker 지시에 다음 감사 항목 추가: + 1. 새로 도입된 검증 계약(최소길이·필수필드·enum 범위 등) 목록화. + 2. 기존 테스트 fixture(요청 본문·파라미터) 전수 스캔 — 신규 계약 조건 충족 여부 확인. + 3. 미충족 fixture 갱신(행위 assertion 보존 전제). + 4. "우연통과" 제거: 거부 경로 테스트의 fixture 도 신규 계약을 충족하는 값으로 올려 + 실제 거부 사유(상위 분기)까지 도달함을 보장. + 재현성: 컨트롤러 검증 강화 시마다 발생 가능. + rationale_for_saving: "컨트롤러 검증 강화는 반복 발생. W-TEST spawn 지시에 표준 체크리스트로 넣지 않으면 매번 재발." + signal_source: negative + docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md + memory_optional: true + + - name: freeze-scope-requires-doc-evidence + type: feedback + description: "동결/freeze 인접 작업은 근거 문서 줄 번호 확인 후 게이트 분류 — 표면 유사성만으로 §6 게이트 금지" + body_draft: | + ## Why + 직전 인터뷰 세션(20260618-145152) report 가 C3(game_review_stats 집계뷰)를 + "W2-3 동결 해제·고위험·§6 게이트"로 표기. 실제 동결 대상은 잼 평가(심사/투표/시상) 스키마이며 + 댓글/리뷰 스키마는 W3-2 별도 설계(roadmap:63,201,202). 표면적 "평점 집계" 유사성만으로 + 동결 묶음에 포함한 오분류. 본 세션에서 근거 문서 직접 확인으로 phantom 게이트 해소. + + ## How to apply + 동결/freeze 인접 표기 전: + 1. 동결 선언 근거 문서(ADR·roadmap·work-log)를 실제로 열어 동결 범위 정의를 줄 번호로 확인. + 2. 현재 변경 대상 테이블/뷰/심볼이 그 범위 정의에 명시적으로 포함되는지 판단. + 3. 포함 확인 시만 §6 게이트 표기. 인접·유사 용어만으로 분류 금지. + rationale_for_saving: "동결 경계 판단은 코드에서 유도 불가. 문서 확인 없이 유사성 추론으로 phantom 게이트가 누적되면 불필요한 사용자 에스컬레이션 반복." + signal_source: negative + docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md + memory_optional: true + + - name: frontend-skill-fork-pattern + type: feedback + description: "production-grade UI 구현은 frontend-design 스킬 fork(컨텍스트 상속) 위임 + 단일파일 폴리시 + 프리뷰 render-verify 조합이 검증됨" + body_draft: | + ## Why + W3-2 고도화 세션(20260622)에서 육각형 SVG 레이더·6축 radiogroup·C1~C6 를 + frontend-design 스킬 fork 로 위임, 단일 JSP 파일(game-detail.jsp) 승격, + 프리뷰 HTML 산출, 가드레일(JS로직 보존·a11y·BE API 계약) 전부 준수. + 외부 의존 0·기존 앰버/크림 디자인 언어 확장. L1 43/43 GREEN 이후 진입 순서도 검증됨. + + ## How to apply + 복잡 UI(SVG·a11y·다중 JS 인터랙션) 구현 시: + 1. L1 전체 GREEN 확인 후 frontend-design 스킬 fork 진입. + 2. 단일파일 폴리시: JSP 1파일 안에서 완결(새 파일 0). + 3. 가드레일 명시: 기존 JS 로직 보존 / a11y(role, aria, textContent) / BE API 계약 무변경. + 4. 프리뷰 HTML 산출(artifacts/): 컨포넌트 시각 확인용. + 외부 JS 라이브러리·CDN 도입 없이 인라인 SVG + vanilla JS 패턴 유지. + rationale_for_saving: "비자명 조합 판단(스킬 fork + L1 완료 후 진입 순서 + 단일파일 폴리시)이 실전 검증됨. 동일 패턴 재현 시 참고." + signal_source: positive + docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md + memory_optional: true + + protocol_feedback: + - "design-advisor 체크리스트 보강(structural): §1 파일 영향맵 작성 규약에 'SSR 컨트롤러·뷰모델 호출지점 포함' 항목 추가 필요. 현행 규약은 API 컨트롤러 나열에 집중하여 SSR 호출지점이 반복 누락될 구조적 허점이 있다. verification-strategies.md 또는 design-advisor 가이드에 명시 권고." + - "W-TEST worker 지시 표준화(structural): 컨트롤러 검증 계약이 강화(최소길이·필수필드 추가)되는 경우, W-TEST worker 지시에 '기존 fixture 전수 계약 정합 감사' 단계를 의무 체크리스트 항목으로 포함 필요. 현행 W-TEST 지시에는 '신규 테스트 추가'만 있고 '기존 fixture 갱신 감사'가 없어 동일 패턴 재발 가능." + - "§6 게이트 분류 기준 명문화: 동결/freeze 경계 판단을 근거 문서(ADR/roadmap 줄 번호) 확인 없이 표면 유사성만으로 수행하는 것을 agent-team-protocol 에서 명시 금지. '동결 구역 인접 = 고위험 게이트' 추론 차단 규칙 추가 권고." + + applied_changes: [] +``` diff --git a/.atp/work-session/20260622-092800/verification.md b/.atp/work-session/20260622-092800/verification.md new file mode 100644 index 0000000..8a1637b --- /dev/null +++ b/.atp/work-session/20260622-092800/verification.md @@ -0,0 +1,194 @@ +--- +phase: verification +agent: verification-advisor +agent_version: 1 +generated_at: 2026-06-22T10:02:30+09:00 +concerns: + - "AC-3/AC-1 중복 실패: GameReviewControllerTest 8건이 모두 400 BAD_REQUEST 반환. 동일 패턴(POST 요청 → 400)이므로 요청 파라미터 바인딩 또는 validation 로직 공통 결함 의심. 수정 범위는 implementation 몫." + - "L2 skipped: 원격 DB 미기동 환경. live contract 테스트 실행 조건 미충족." +concerns_checked: true +--- + +# 검증 결과 + +## Acceptance Criteria (입력 받은 그대로 인용) + +**AC-1 기존 회귀 보존 (불변식)** +기존 31건이 전부 PASS(삭제·실패 0). 31건 = GameCommentControllerTest 기존 12 + GameReviewControllerTest 기존 13 + UserControllerCsrfTest 5 + BibimbapApplicationTests 1. 이번 세션이 테스트를 추가했으므로 총건수는 31+α(증가 정상). 측정 = 기존 테스트 메서드가 사라지거나 실패한 것이 0. + +**AC-3 신규 단위테스트 PASS** +신규 추가분(다축 저장/조회, 페이지네이션, sort enum, A2 마스킹, B2 10자, B3 정규화 등) 전부 GREEN. 현재 영향 테스트클래스 = GameCommentControllerTest(총 17), GameReviewControllerTest(총 20), BibimbapApplicationTests(@MockBean 8). + +**AC-5 SQL injection 면역 (보안 — 집합 전수)** +W3-2 신규/변경 매퍼에서 `${}` 동적 SQL 치환 0건. + +**AC-4 sort 화이트리스트 5분기** +sort enum→고정 ORDER BY 상수 분기가 5종(댓글 oldest/newest, 리뷰 newest/rating_desc/rating_asc) 존재. + +**AC-6 axis_key 6키 정합 (집합 전수)** +axis_key 6종(immersion,creativity,controls,completeness,sound,visual)이 DDL CHECK·뷰 FILTER·BE 앱상수에서 동일 철자·6개 전수. 누락/오타 0. + +--- + +## 실행된 전략 + +| id | cmd | exit | severity | 결과 | +|---|---|---|---|---| +| unit (full suite) | `JAVA_HOME=... ./mvnw -q test` | 1 | blocker | FAIL | + +### 단계별 분해 + +| 단계 | 결과 | +|---|---| +| L1 typecheck (compile) | pass (빌드 성공, 컴파일 에러 0) | +| L1 unit+regression (GameCommentControllerTest) | pass (17/17) | +| L1 unit+regression (GameReviewControllerTest) | **FAIL (12/20 pass, 8 FAILURE)** | +| L1 unit+regression (UserControllerCsrfTest) | pass (5/5) | +| L1 unit+regression (BibimbapApplicationTests) | pass (1/1) | +| 전체 집계 | Tests run: 43, Failures: 8, Errors: 0, Skipped: 0 | +| L2 contract | skipped: 원격 DB 미기동 | +| 로그 스캔 | warn: 1건 — `Appended trailing slash to static resource location` (정적 리소스 경고. 기능 차단 없음) | + +--- + +## 실패 상세 + +### GameReviewControllerTest — 8건 전부 `400 BAD_REQUEST` + +공통 패턴: 모든 실패 테스트가 POST/PATCH 엔드포인트에 정상 요청을 보냈으나 `400 BAD_REQUEST` 반환. 예상값은 `200 OK` 또는 `409 CONFLICT`. + +| # | 테스트명 | 파일:라인 | 예상 | 실제 | +|---|---|---|---|---| +| 1 | `createReviewReturnsEditedFalse` | GameReviewControllerTest.java:74 | 200 OK | 400 BAD_REQUEST | +| 2 | `updateReviewReturnsEditedTrue` | GameReviewControllerTest.java:92 | 200 OK | 400 BAD_REQUEST | +| 3 | `createSecondReviewReturnsConflict` | GameReviewControllerTest.java:111 | 409 CONFLICT | 400 BAD_REQUEST | +| 4 | `createReviewAcceptsRatingOneBoundary` | GameReviewControllerTest.java:129→assertCreateRatingAccepted:404 | 200 OK | 400 BAD_REQUEST | +| 5 | `createReviewAcceptsRatingFiveBoundary` | GameReviewControllerTest.java:134→assertCreateRatingAccepted:404 | 200 OK | 400 BAD_REQUEST | +| 6 | `createReviewAutoAveragesOverallWhenRatingOmitted` | GameReviewControllerTest.java:260 | 200 OK | 400 BAD_REQUEST | +| 7 | `createReviewUsesManualOverallWhenRatingProvided` | GameReviewControllerTest.java:283 | 200 OK | 400 BAD_REQUEST | +| 8 | `createReviewPersistsSixAxes` | GameReviewControllerTest.java:308 | 200 OK | 400 BAD_REQUEST | + +**재현 명령:** +``` +export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home +JAVA_HOME=$JAVA_HOME ./mvnw test -pl . -Dtest=GameReviewControllerTest +``` + +개별 테스트 좁힌 재현: +``` +JAVA_HOME=$JAVA_HOME ./mvnw test -Dtest=GameReviewControllerTest#createReviewReturnsEditedFalse +``` + +--- + +## AC별 판정 + +### AC-1 기존 회귀 보존 + +판정: **FAIL** + +근거: +- GameCommentControllerTest: 17/17 pass — 기존 12개 메서드 현존·pass 확인 +- UserControllerCsrfTest: 5/5 pass +- BibimbapApplicationTests: 1/1 pass +- GameReviewControllerTest: **20건 중 8건 FAIL** + +GameReviewControllerTest 기존 13건 중 어느 것이 기존 메서드인지 테스트 출력만으로는 구분 불가. 단, 실패 8건 모두 기능 시나리오 테스트(create/update 성공 경로, 409 충돌)이므로 기존 13건 중 상당수가 이 실패 8건에 포함될 가능성이 높다. "기존 테스트가 0건 실패" 조건 위반 판정. + +### AC-3 신규 단위테스트 PASS + +판정: **FAIL** + +근거: GameReviewControllerTest 20건 중 8건 실패. 실패 테스트에는 `createReviewPersistsSixAxes`(다축 저장), `createReviewAutoAveragesOverallWhenRatingOmitted`(rating 자동 평균), `createReviewAcceptsRatingOneBoundary`/`FiveBoundary`(경계값) 등 신규 추가분이 포함. + +### AC-5 SQL injection 면역 + +판정: **PASS** + +근거: +``` +rg -n '\$\{' GameCommentsMapper.java GameReviewsMapper.java GameReviewAxesMapper.java GameReviewStatsMapper.java +``` +매치 3건 모두 주석 텍스트: +- `GameCommentsMapper.java:97` → `// ORDER BY 만 sort enum 으로 고정 문자열 분기(${} 미사용).` +- `GameReviewAxesMapper.java:28` → `// VALUES 다행. 각 값은 #{axes[i].axisKey}/#{axes[i].score} 바인딩(${} 미사용).` +- `GameReviewsMapper.java:92` → `// SELECT/JOIN/WHERE 는 컴파일타임 리터럴. ORDER BY 만 sort enum 으로 고정 문자열 분기(${} 미사용).` + +실제 SQL 절 내 `${}` 동적 치환 0건. 모든 파라미터 바인딩은 `#{}` 사용 확인. + +### AC-4 sort 화이트리스트 5분기 + +판정: **PASS** + +근거 — 매퍼 소스에서 확인된 분기: + +댓글(GameCommentsMapper.java:100): +```java +String orderBy = "newest".equals(sort) + ? "ORDER BY c.created_at DESC, c.id DESC" + : "ORDER BY c.created_at ASC, c.id ASC"; +``` +→ newest, oldest(default) 2종 + +리뷰(GameReviewsMapper.java:95-98): +```java +String orderBy = switch (sort == null ? "" : sort) { + case "rating_desc" -> "ORDER BY r.rating DESC, r.created_at DESC, r.id DESC"; + case "rating_asc" -> "ORDER BY r.rating ASC, r.created_at DESC, r.id DESC"; + default -> "ORDER BY r.created_at DESC, r.id DESC"; // newest +}; +``` +→ rating_desc, rating_asc, newest(default) 3종 + +합계 5종. 모두 고정 문자열 리터럴(동적 보간 없음). + +### AC-6 axis_key 6키 정합 + +판정: **PASS** + +근거: + +DDL CHECK 제약 (`docs/game-reviews-ddl.sql:156-157`): +```sql +CHECK ("axis_key" IN ('immersion','creativity','controls','completeness','sound','visual')) +``` + +뷰 FILTER (`docs/game-reviews-ddl.sql:176-181`): +```sql +FILTER (WHERE a."axis_key"='immersion') +FILTER (WHERE a."axis_key"='creativity') +FILTER (WHERE a."axis_key"='controls') +FILTER (WHERE a."axis_key"='completeness') +FILTER (WHERE a."axis_key"='sound') +FILTER (WHERE a."axis_key"='visual') +``` + +BE 앱상수 (`GameReviewController.java:38`): +```java +private static final String[] AXIS_KEYS = {"immersion", "creativity", "controls", "completeness", "sound", "visual"}; +``` + +3개 위치 전부 동일 철자 6종. 누락/오타 0. + +--- + +## 종합 판정 + +``` +overall: fail +rollback_signal: none +``` + +- rollback_signal=none 근거: 실패가 `400 BAD_REQUEST` 일관 패턴. DB migration/파괴적 조작과 무관한 컨트롤러 레벨 요청 처리 결함으로 판단. 데이터 오염 없음. + +--- + +## Acceptance 매칭 + +| criterion | 매칭 전략 | 판정 | +|---|---|---| +| AC-1 기존 회귀 보존 | L1 unit (full suite) | **FAIL** — GameReviewControllerTest 8건 실패 | +| AC-3 신규 단위테스트 PASS | L1 unit (full suite) | **FAIL** — 신규 테스트 포함 8건 400 반환 | +| AC-5 SQL injection 면역 | rg `\$\{` 매퍼 전수 | PASS — 실제 SQL 보간 0건 | +| AC-4 sort 화이트리스트 5분기 | rg ORDER BY 분기 수집 | PASS — 5종 고정 리터럴 확인 | +| AC-6 axis_key 6키 정합 | rg DDL/뷰/앱상수 3위치 전수 | PASS — 3위치 동일 6종 | diff --git a/.atp/work-session/20260622-162345/report.md b/.atp/work-session/20260622-162345/report.md new file mode 100644 index 0000000..a6a169c --- /dev/null +++ b/.atp/work-session/20260622-162345/report.md @@ -0,0 +1,123 @@ +--- +schema_version: 2 +sid: 20260622-162345 +started_at: 2026-06-22T16:23:45+09:00 +ended_at: +user_request: | + docs/game-reviews-ddl.sql 같이 sql 추가되면 로컬엔 테스트 가능하게 바로 적용하도록 + docs 반영해주고, sql도 지금 바로 반영해줘. +Summary: | + 신규 docs/*-ddl.sql 을 실행 중 로컬 DB 에 즉시 적용하는 반복 절차를 docs 에 명문화하고, + 현재 미적용 상태인 game-reviews-ddl.sql 의 W3-2 고도화 블록을 실행 DB(dev)에 즉시 적용한다. +Invocations: [] +Decisions: [] +--- + +# Advisor Invocation Decision Log +# 각 advisor 호출/스킵 판단 즉시 1줄 append +- advisor: requirements-advisor + decision: skip + rationale: '요청 명확 — 절차 문서화 + 멱등 DDL 즉시 적용. 스코프 모호 없음.' + checked_at: 2026-06-22T16:23:45+09:00 +- advisor: graphify-lookup-advisor / research-advisor + decision: skip + rationale: 'DB/docs 상태를 orchestrator 가 inline 으로 직접 조사 완료(실행 DB 델타·local-setup.md·schema.sql).' + checked_at: 2026-06-22T16:24:00+09:00 +- advisor: design-advisor + decision: skip + rationale: '결정 축 단일·이산(스크립트+doc vs doc-only). AskUserQuestion 으로 직접 위임.' + checked_at: 2026-06-22T16:24:30+09:00 +- advisor: implementation-advisor + decision: skip + rationale: '산출=docs 1~2파일 편집 + 멱등 DDL psql 적용. 마이크로 편집 범위, orchestrator 직접 수행.' + checked_at: 2026-06-22T16:24:30+09:00 + +# 조사 결과 (inline research) +- 마이그레이션 도구 없음(flyway/liquibase 부재). 권위 DDL = docs/*-ddl.sql, Docker init = db/schema.sql. +- db/schema.sql 은 컨테이너 *최초* 기동 시 docker-entrypoint-initdb.d 로 1회만 자동 주입. + 이후 추가된 DDL 은 실행 DB 에 자동 반영 안 됨 → down -v 재기동(데이터 소실) 외엔 수동 적용 필요. +- 실행 컨테이너: bibimbap-db (postgres:16, host:5433), db=bibimbap user=bibimbap schema=dev. +- 실행 DB dev 스키마 델타 (game-reviews-ddl.sql 기준): + - 적용됨: game_reviews 테이블, game_comments.user_id + - 누락됨: game_comments.updated_at, game_reviews.is_rating_manual, + game_review_axes 테이블, game_review_stats 뷰 (= DDL L102~187 고도화 블록) +- DDL 전체 멱등(IF NOT EXISTS / DO $$ guard / CREATE OR REPLACE VIEW) → 전체 재실행 안전. + +Decisions: + - id: D1 + decision: '반복 절차 형태 = 헬퍼 스크립트 + 문서 (Option A, 사용자 선택)' + rationale: 'DDL 전부 멱등 + 마이그레이션 도구 부재 → "바로 적용" 을 한 명령으로 만드는 스크립트가 적합.' + - id: D2 + decision: 'DDL 적용은 데이터 보존(비파괴) — docker exec psql 직접 적용. down -v 미사용.' + rationale: 'down -v 는 로컬 데이터 소실. 모든 docs/*-ddl.sql 멱등이라 직접 적용이 안전.' + - id: D3 + decision: 'search_path 강제(PGOPTIONS -c search_path=dev) 를 스크립트/문서에 명시.' + rationale: 'docs/*-ddl.sql 은 unqualified 테이블명 사용. dev 스키마 타겟 보장 필수.' + +# verified_by_me +- 'L1(typecheck/unit): N/A — src/ 코드 변경 0건 (DDL 적용 + 운영 스크립트 + docs 만).' +- '게임리뷰 DDL 적용: docs/game-reviews-ddl.sql → bibimbap-db dev 스키마, ON_ERROR_STOP=1 무에러.' +- '적용 후 객체 검증(전부 존재): game_review_axes 테이블 / game_review_stats 뷰 / game_comments.updated_at / game_reviews.is_rating_manual / axis_key check 제약.' +- 'game_review_stats 뷰 SELECT 정상 동작 (기존 시드 game_id=3, review_count=5 집계 반환).' +- 'db/apply-local-ddl.sh 전체 실행 검증: docs/*-ddl.sql 3개 멱등 재실행 성공(기존 객체 skip, 누락분 생성, 종료 0).' +- '나머지 DDL 멱등성 사전 확인: recruit-posts / security-hardening 둘 다 가드≥문장, DROP/TRUNCATE/DELETE/bare INSERT 0.' + +# needs_user_verification +- '(선택) 앱 기동 후 리뷰 다축평점/집계뷰 기능 스모크 — 실행 DB 에 객체는 적용됐으나 UI 경로 E2E 는 미수행.' +- '커밋: 사용자 요청 시 진행 (CLAUDE.md/하니스 정책 — 커밋은 명시 요청 시에만). 변경 파일: docs/usage/local-setup.md (M), db/apply-local-ddl.sh (신규).' + +# graph_refresh +- 'graph-refresh-checker 판정: fresh (src·docs 양 scope). 재생성 불필요.' +- '근거: src 변경 0; docs 변경(local-setup.md §4.1 추가)은 기존 문서 내 산문 보강 — 신규 md/카테고리/DDL 0, 구조 시그널 0. 참조 DDL 3종은 이미 docs scope 인덱싱됨.' +- 'db/apply-local-ddl.sh 는 scope(src/,docs/) 밖 — 신규 scope 신설 불요(향후 db/ 운영 스크립트 다수화 시 재검토 권고).' +- 'source_commit b9d836d (HEAD=375a2de) 차이는 graph 메타 커밋 1건뿐 — 다음 실질 변경 시 동기화. 단독 갱신 불요.' + +# Invocations +- agent: atp-graphify:graph-refresh-checker + tier: large + result: 'fresh (src·docs). 재생성/삭제/index 갱신 모두 불필요.' + tokens: 12263 + +# user_signals +positive: + - 'AskUserQuestion 반복절차 형태 결정에서 Recommended(스크립트+문서) 1회 선택 — 옵션 설계 수용.' +negative: [] + +# retrospective +- 'retrospective-advisor 정식 호출 skip — 마이크로 편집 경로(advisor 전체 skip), negative 시그널 0, 구조적 피드백 0.' +- 'memory_candidate: 없음 — 핵심 교훈(schema.sql 최초 1회만 자동주입 → 신규 DDL 수동 적용 필요)은 산출물 자체(local-setup.md §4.1 + db/apply-local-ddl.sh)에 docs-first 로 이미 codify 됨. 별도 MEMORY 불요.' + +# follow_on (동일 세션 후속 요청) +followup_request: | + 커밋 진행 + "비-메인스트림 브랜치면 커밋 전제로 작업" 정책을 docs 에 반영. +followup_method: | + read-only 조사 워크플로우(Survey 4 병렬 explorer → Design 합성, ultracode) 로 + 정책 문서 위치·기존 규칙·하니스 충돌 표면 전수 조사 후 plan-gate → 적용. +followup_decisions: + - id: D4 + decision: '커밋 정책 = development 규칙 급(신규 ADR 미발행).' + rationale: 'ADR 은 기술/아키텍처 되돌리기 어려운 결정 한정(docs/adr/index.md). 본건은 가역적 운영 절차 → document-category-classification 상 development/. 부수 발견: ADR-0010 은 work-session 보고서들의 phantom 참조(docs/adr/ 에 번호 ADR 미실재).' + - id: D5 + decision: '하니스 기본규칙과 정합 = durable authorization 프레이밍.' + rationale: "비-메인스트림 브랜치 커밋을 '프로젝트 지침이 부여한 표준 승인' 으로 명문화 → '사용자 요청 시에만 커밋' 위반 아님. push 는 명시 요청 한정 유지, 메인스트림 branch-first 유지." + - id: D6 + decision: 'CLAUDE.md=선언+링크, docs/development/git-workflow.md=정본 신설, index.md 등재.' + rationale: 'CLAUDE.md 는 하니스 상시 로드 → 표준 승인 선언 효력. 상세 규약은 재사용 개발규칙이라 development/ 정본.' +followup_edits: + - 'CLAUDE.md 작업원칙: 비-메인스트림 커밋 표준승인 + 메인스트림 branch-first 불릿 2개.' + - 'docs/development/git-workflow.md 신규: 브랜치 분류·표준승인·push 제외·커밋 단위·Conventional Commits + Co-Authored-By 트레일러.' + - 'docs/development/index.md: git-workflow.md 등재.' +followup_commits: + - 'feat(db): 로컬 DDL 즉시적용 스크립트 + 셋업 절차 문서화 (db/apply-local-ddl.sh, docs/usage/local-setup.md)' + - 'docs(dev): 비-메인스트림 브랜치 커밋 표준 승인 정책 추가 (CLAUDE.md, git-workflow.md, dev/index.md)' + - 'chore(atp): work-session 산출물 기록 (20260618-145152, 20260622-092800, 20260622-162345 — 사용자 "셋 다 커밋" 선택)' +user_signals_followup: + positive: + - 'orphan work-session 처리 "셋 다 커밋" 1회 선택 — 추적 관행 일관성 복원 승인.' + +# ended_at +ended_at: 2026-06-22T16:48:00+09:00 + +# open_items +- '미커밋 변경 2건 — docs/usage/local-setup.md(M), db/apply-local-ddl.sh(신규). 이번 작업 단위. 사용자 커밋 지시 대기.' +- '.atp/work-session/ untracked 디렉토리 3개 — 세션 산출물(ADR-0010 추적 대상이나 별도 커밋 정책).'