Compare commits
No commits in common. "main" and "feat/local-dev-setup" have entirely different histories.
main
...
feat/local
|
|
@ -1,146 +0,0 @@
|
|||
---
|
||||
schema_version: 1
|
||||
sid: 20260617-094450
|
||||
started_at: 2026-06-17T09:44:50+09:00
|
||||
ended_at: 2026-06-17T10:05:00+09:00
|
||||
user_request: |
|
||||
PR #1 (docs/project-analysis → main, "docs: ATP 문서 체계 도입 및 프로젝트 종합 분석")
|
||||
당사자 승인 완료. 그러나 main 에 실수로 "일부 병합한" 커밋(f6f691c)이 들어가 PR 이
|
||||
mergeable:false 상태. main 으로 병합 + 충돌 해결 요청.
|
||||
PR URL: https://gitea.pandoli365.com/pandoli365/bibimbap/pulls/1
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
진단 (read-only):
|
||||
|
||||
- PR #1 head = `d297f31` (docs/project-analysis), base = `main`. gitea state=open, **mergeable:false**, merged:false.
|
||||
- merge-base = `40bde12` (단일).
|
||||
- 사고 커밋 `f6f691c "fix: harden auth csrf handling"` 가 두 가지를 한 커밋에 섞음:
|
||||
1. **정당한 csrf 보안 수정** (B1): UserController.java / login.jsp / signup.jsp / 테스트 2건
|
||||
2. **PR 문서의 부분·다른 버전 스냅샷**: CLAUDE.md, docs/analysis/..., docs/index.md, .serena/project.yml,
|
||||
.atp/work-session(파일형), 그리고 README.md 컨벤션 docs (PR 은 index.md 컨벤션)
|
||||
- 동일 경로가 양측에서 다르게 추가/수정 → gitea 충돌. 단 `.atp/work-session` 은 main=blob / PR=tree
|
||||
의 **파일/디렉토리 타입 충돌**이 gitea mergeable:false 의 핵심 원인.
|
||||
|
||||
위험 발견:
|
||||
|
||||
- **naive `git merge` 금지**: 로컬 ort 전략이 충돌 0으로 자동 병합하지만, 결과 트리가 **csrf 코드 5건을
|
||||
base 로 되돌리고 csrf 테스트 파일을 삭제**함 (블롭 해시로 검증). gitea Merge 버튼도 신뢰 불가.
|
||||
→ 결과 트리를 결정론적으로 구성해야 함.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
# 각 advisor 호출/스킵 판단 즉시 1줄 append
|
||||
- advisor: requirements-advisor
|
||||
decision: skip
|
||||
rationale: '요청 명확(병합+충돌해결). 모호점은 git 상태 자체이며 orchestrator 직접 조사가 효율적.'
|
||||
checked_at: 2026-06-17T09:45:00+09:00
|
||||
- advisor: research-advisor
|
||||
decision: skip
|
||||
rationale: 'git/gitea 상태 조사는 orchestrator 직접 Bash 가 적합. 외부 자료 불요.'
|
||||
checked_at: 2026-06-17T09:45:00+09:00
|
||||
- advisor: design/implementation-advisor
|
||||
decision: skip (pending user)
|
||||
rationale: 'git 병합 충돌해결은 §6 파괴적 게이트 + 단계별 명령. orchestrator 직접 수행이 적합. 사용자 결정 후 진행.'
|
||||
checked_at: 2026-06-17T09:45:00+09:00
|
||||
|
||||
# Invocations
|
||||
- inv-001: graph-refresh-checker (post-merge staleness 판정) — model_choice: phase=graphify-judgment, tier=large(inherit), rationale='no-defer 정책상 머지 직후 그래프 신선도 판정'
|
||||
|
||||
# Decisions
|
||||
- D1: csrf 보안 수정(f6f691c 코드분)은 정당 → main 버전 보존 (전제, 사용자 미반대)
|
||||
- D2: CLAUDE.md → main 프로젝트 지침 + PR ATP 블록 결합 (사용자 선택)
|
||||
- D3: 사고 커밋 README.md 5건 제거, security-remediation-checklist.md(고유) 유지 (사용자 선택)
|
||||
- D4: 반영 방식 → main 에 병합커밋(parents f6f691c+d297f31) + push (사용자 선택)
|
||||
- D5: 충돌 문서(project-analysis.md/docs.index.md/.serena.project.yml) → PR 정식본. db-update-query-generator.md·user-signup-schema.md 는 ort 자동병합 union 유지(링크 전부 유효, README 참조 없음).
|
||||
|
||||
# self_correction
|
||||
- 세션 초반 "naive git merge 가 csrf 코드를 base 로 되돌리고 테스트를 삭제한다"는 발견을 보고했으나, 이는 **로컬 main 이 stale(40bde12)** 상태에서 만든 temp 브랜치로 테스트해 발생한 오판. 올바른 base(origin/main=f6f691c)로 재실행하니 정상 충돌 표면화 + csrf 보존. 교훈: 머지 동작 추론 전 `git fetch` 후 로컬 브랜치==원격 동기화 여부 확인. (memory_candidate)
|
||||
|
||||
# user_signals
|
||||
positive:
|
||||
- '권장안 3건(CLAUDE 결합/README 제거/병합커밋+push)을 한 라운드에 모두 수락'
|
||||
negative: []
|
||||
|
||||
# verified_by_me
|
||||
- 'L1: 병합 결과 src/ 트리가 origin/main(f6f691c)과 바이트 동일 — git diff --cached f6f691c -- src/ 빈 결과. 코드 델타 0, csrf 보존. 신규 코드 없음 → 빌드 불요(docs-only 델타).'
|
||||
- 'L2: skip — 외부 의존성/계약 변경 없음(docs·설정 머지)'
|
||||
- '충돌 0 잔여, README 5건 제거 확인, .atp/work-session blob→dir 해소(4 entries)'
|
||||
- 'push: f6f691c..8c41b22 main->main 성공. d297f31 이 origin/main ancestor 재확인(=PR 내용 머지 완료)'
|
||||
|
||||
# graph_refresh
|
||||
- 판정: fresh (src·docs 모두 fresh; docs 는 partial-stale 경계 근접)
|
||||
- 미반영 항목: docs/security/security-remediation-checklist.md 1개 문서 노드(+analysis/schema 링크 엣지). src 는 구조 시그널 0(UserController 변경은 기존 CsrfTokens.isValid 호출 2줄, 신규 노드/엣지 없음).
|
||||
- 처리: 재생성 유보. 사유 = checker 가 docs 재생성을 "선택적·저효익(노드 1개)"으로 평가. /graphify 는 docs 그래프 전체 재생성 + 추가 push 를 유발하므로 PR 머지라는 작업 범위에 비해 과함. 사용자가 그래프 완전성을 원하면 `/graphify docs/` 로 트리거 가능 (open item).
|
||||
|
||||
# needs_user_verification
|
||||
- 'gitea PR #1 레코드: 직접 push 로는 자동 merged 전환 안 됨(state:open 잔존). gitea UI 에서 수동 close 하거나 API 토큰 제공 시 닫기 대행 가능. 내용상 머지는 완료.'
|
||||
- '세션 노트 .atp/work-session/20260617-094450/ 는 untracked. 영속화 원하면 별도 커밋 가능.'
|
||||
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: '권장안 3건(CLAUDE 결합 / README 5건 제거 / 병합커밋+push)을 한 라운드에 모두 수락'
|
||||
about: 'orchestrator 가 충돌 해소 방식을 옵션화해 한 번에 제시 — 재질의·재지시 없이 단발 수락. 비자명한 결정(병합커밋 parents 구성, 충돌 문서 정식본 채택)을 묶어 제시한 판단이 검증됨.'
|
||||
negative: []
|
||||
what_went_well:
|
||||
- 'self_correction 메커니즘이 작동: 세션 초반의 "naive git merge 가 csrf 코드를 base 로 되돌리고 테스트를 삭제한다"는 오판을 push 전에 스스로 발견·정정. 잘못된 결론이 사용자에게 확정 안내되거나 파괴적 push 로 이어지기 전에 차단됨.'
|
||||
- '파괴적 git 작업(병합 충돌 해결)을 advisor 우회 없이 orchestrator 직접 수행 — 프로토콜 §1 직접 수행 영역 판정이 정확. advisor 호출 스킵 사유를 Decision Log 에 즉시 기록.'
|
||||
- '결과 트리를 블롭 해시 수준으로 결정론적 검증(L1: src/ 트리가 origin/main 과 바이트 동일, csrf 보존 확인). gitea Merge 버튼·로컬 ort 자동병합을 신뢰하지 않고 직접 검증.'
|
||||
- 'graph staleness 를 no-defer 정책에 따라 머지 직후 판정하고, 재생성 효익(노드 1개) 대비 비용(전체 docs 그래프 재생성 + 추가 push)을 평가해 유보 + open item 으로 위임.'
|
||||
what_to_improve:
|
||||
- 'git 머지 동작을 추론·시뮬레이션하기 전 baseline ref 의 신선도를 확인하지 않았다. stale 로컬 main(40bde12) 기반 temp 브랜치로 머지를 테스트해 "csrf 파괴" 라는 오판을 생성·보고했다. fetch 후 로컬==원격 동기화 확인이 추론보다 선행해야 한다.'
|
||||
- '오판을 정정한 것은 self_correction 으로 회수됐으나, 그 오판이 한 번은 진단 보고(Summary 위험 발견 섹션)에 실렸다. 추론 기반 위험 경고를 보고에 싣기 전 baseline 전제(어느 ref 로 시뮬레이션했는지)를 명시하는 게이트가 있었다면 오판 단계에서 자가 포착 가능했을 것.'
|
||||
memory_candidates:
|
||||
- name: merge-reasoning-requires-fresh-baseline-ref
|
||||
type: feedback
|
||||
description: 'git 머지/충돌 동작을 추론·시뮬레이션하기 전 fetch 후 로컬 브랜치==원격 ref 동기화 여부를 먼저 확인. stale baseline 으로 시뮬레이션하면 잘못된 위험 결론이 나온다.'
|
||||
body_draft: |
|
||||
git 머지/충돌 결과를 추론하거나 temp 브랜치로 시뮬레이션하기 전에 `git fetch` 후
|
||||
시뮬레이션 baseline 으로 쓸 로컬 브랜치가 원격 ref(보통 origin/main)와 동기화돼 있는지
|
||||
먼저 확인한다. stale baseline 으로 머지를 테스트하면 실제와 다른 결과 트리가 나와
|
||||
"코드가 파괴된다" 류의 잘못된 위험 결론을 보고하게 된다.
|
||||
|
||||
**Why:** 세션 20260617-094450 에서 "naive git merge 가 csrf 코드를 base 로 되돌리고
|
||||
테스트를 삭제한다"는 위험을 진단 보고에 실었으나, 이는 로컬 main 이 stale(40bde12)
|
||||
상태에서 만든 temp 브랜치로 머지를 테스트해 발생한 오판이었다. 올바른 base
|
||||
(origin/main=f6f691c)로 재실행하니 정상적인 충돌이 표면화되고 csrf 가 보존됐다.
|
||||
오판은 push 전 self_correction 으로 회수됐지만, 그 전에 한 번 위험 경고로 보고됐다.
|
||||
관련: bump 커밋 *쓰기 대상* 선정 시 stale 브랜치 회피는
|
||||
[[feedback_bump_target_branch_must_be_consumer_tracked_ref]] — 본 항목은 *읽기/추론*
|
||||
baseline 의 신선도라는 별개 축.
|
||||
|
||||
**How to apply:**
|
||||
- 머지 동작 추론/시뮬레이션 전: `git fetch` → `git rev-parse <local> origin/<remote>` 로
|
||||
동기화 확인, 또는 시뮬레이션 base 를 `origin/<remote>` 로 직접 지정.
|
||||
- 추론 기반 위험 경고를 보고에 실을 때는 "어느 ref 를 baseline 으로 시뮬레이션했는가"를
|
||||
함께 명시 — 전제가 드러나면 stale 오판을 자가 포착하기 쉬워진다.
|
||||
- 충돌 머지 결과는 블롭 해시 수준으로 결정론 검증(git diff --cached <ref> -- <path>).
|
||||
rationale_for_saving: '재현 가능한 패턴(stale ref 기반 시뮬레이션 → 오판). 코드/커밋 로그로 유도 불가, 관찰로만 드러남. 기존 bump-target memory 와 인접하나 read/추론 baseline 신선도라는 별개 축으로 비중복.'
|
||||
signal_source: observation
|
||||
docs_sync_target: null
|
||||
conflicts_with: null
|
||||
- name: report-risk-claim-states-its-baseline-premise
|
||||
type: feedback
|
||||
description: '추론·시뮬레이션 기반 위험 경고를 진단 보고에 실을 때 baseline 전제(어느 ref·상태로 시뮬레이션했는지)를 함께 명시. 전제 누락 시 오판이 무검증으로 보고된다.'
|
||||
body_draft: |
|
||||
진단 보고(Summary "위험 발견" 등)에 추론·시뮬레이션 기반 경고를 실을 때는
|
||||
그 결론의 baseline 전제를 한 줄로 함께 기록한다 — 예: "origin/main(f6f691c) 기준
|
||||
ort 시뮬레이션 결과". 전제가 명시되면 검증자(및 작성자 자신)가 전제의 타당성부터
|
||||
점검할 수 있어 stale·잘못된 전제에서 나온 오판을 보고 단계에서 포착할 수 있다.
|
||||
|
||||
**Why:** 세션 20260617-094450 에서 stale 로컬 main 기반 머지 오판이 "위험 발견"
|
||||
섹션에 baseline 전제 없이 단정적으로 실렸다. push 전 self_correction 으로 회수됐으나,
|
||||
전제가 보고에 명시돼 있었다면 오판 단계에서 자가 포착 가능했을 것이다.
|
||||
|
||||
**How to apply:**
|
||||
- 진단 보고의 위험/경고 항목은 (관찰 사실 | 추론) 을 구분하고, 추론이면 전제 ref·상태를 병기.
|
||||
- "naive X 하면 Y 파괴된다" 류 강한 단정은 결정론 재현 검증(블롭 해시/diff) 후에만 단정형으로.
|
||||
rationale_for_saving: '위 머지 baseline 교훈의 메타 층위(보고 규약). orchestrator 의 보고 작성 습관에 재현 적용 가능. self_correction 이 회수한 경로를 한 단계 앞당기는 게이트로 비중복.'
|
||||
signal_source: observation
|
||||
docs_sync_target: null
|
||||
conflicts_with: null
|
||||
protocol_feedback:
|
||||
- '프로토콜 §1(orchestrator 직접 수행 영역) 또는 §6(파괴적 게이트)에 "git 머지/리베이스 동작을 추론·시뮬레이션하기 전 baseline ref 신선도 확인(fetch + 로컬==원격)" 선행 체크를 추가 권고. 이번 세션의 stale-baseline 오판은 단발 실수가 아니라 시뮬레이션 baseline 전제를 명시하지 않는 구조적 허점에 가깝다(structural 경계).'
|
||||
- '진단 보고의 "위험 발견" 류 항목에 (관찰 | 추론) 구분 + 추론 시 baseline 전제 병기를 권장 포맷으로 추가하면, self_correction 이 사후에 회수하던 오판을 보고 작성 시점에 자가 포착하도록 앞당길 수 있다.'
|
||||
applied_changes: []
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
schema_version: 1
|
||||
sid: 20260617-143315
|
||||
user_request: "바이너리 직접 실행(./mvnw -P dev spring-boot:run) 시 DB 연결 문제"
|
||||
started_at: 2026-06-17T14:33:15+09:00
|
||||
ended_at: 2026-06-17T14:34:00+09:00
|
||||
status: resolved
|
||||
code_change_lines: 0
|
||||
---
|
||||
|
||||
# Summary
|
||||
호스트 직접 실행(경로 B) DB 연결 실패 진단. 근본 원인: bibimbap-db postgres 컨테이너 미기동 → localhost:5433 connection refused. db 컨테이너 기동으로 해소.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
- advisor: ALL
|
||||
decision: skip
|
||||
rationale: '런타임 환경 진단 + 0줄 코드변경 마이크로 작업. 원인=컨테이너 미기동(설정 정상). orchestrator 직접 처리(§1 예외).'
|
||||
checked_at: 2026-06-17T14:33:20+09:00
|
||||
|
||||
# Decisions
|
||||
- 근본원인: docker-compose db 서비스(bibimbap-db) 미생성/미기동. db.properties(localhost:5433), .env(DB_PORT=5433) 는 정상.
|
||||
- kord-postgres(host 5432) 는 무관 — bibimbap 은 5433 사용.
|
||||
- 조치: `docker compose up -d db` (가역적 로컬 액션, §6 파괴게이트 비해당).
|
||||
|
||||
# verified_by_me
|
||||
- L1: n/a (코드변경 0)
|
||||
- 런타임: bibimbap-db Up(healthy), host 5433 LISTEN, dev 스키마 6테이블 자동주입(schema.sql), psql CONNECT_OK(dev.games 0행 무에러).
|
||||
|
||||
# needs_user_verification
|
||||
- `./mvnw -P dev spring-boot:run` 재실행 → Tomcat 8080 + 홈 / HTTP 200 1회 확인(앱→DB 라운드트립).
|
||||
|
||||
# graph_refresh
|
||||
skip: no-scope-change (코드 변경 0줄)
|
||||
|
||||
# open_items
|
||||
- (없음) — 미커밋 잔여는 본 세션 산출 아님(.serena/*, 이전 work-session dir).
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
---
|
||||
title: 게임잼 플랫폼 고도화 — 세션 로드맵
|
||||
status: draft (사용자 확정 대기)
|
||||
created_at: 2026-06-17
|
||||
session: 20260617-150635
|
||||
---
|
||||
|
||||
# 게임잼 플랫폼 고도화 — 세션별 로드맵
|
||||
|
||||
기존 bibimbap(게임 공유 + 팀원 모집, Spring Boot + MyBatis + JSP) 위에 **게임잼 운영/평가/리뷰/관리자/포스팅/검색** 신규 기능군을 얹는다. 메모를 "1항목=1세션"으로 쪼개면 공유 자원(게임잼 엔티티·RBAC·평가 데이터모델)에서 재작업이 폭증하므로(§2.7), **토대 → 결합 클러스터 → 표면 기능** 순으로 의존 순서를 강제한다.
|
||||
|
||||
## 의존 그래프
|
||||
|
||||
```
|
||||
S1 RBAC(권한) ─┬─> S2 게임잼 엔티티 ─┬─> S3 태그+검색
|
||||
│ ├─> S4D 평가 통합설계(스키마 동결)
|
||||
│ │ └─> S4a 댓글/리뷰분리 ─┬─> S4b 심사위원
|
||||
│ │ ├─> S4c 인기투표
|
||||
│ │ └─(4a·4b·4c)─> S4d 시상집계
|
||||
│ └─> S6 메인 통합(노출 대상 필요)
|
||||
└─> S5 포스팅 보드 (S4와 독립, S1 후 병렬 가능)
|
||||
└─> S6 메인 통합
|
||||
|
||||
차후: S7 Unity 빌드 업로드 파이프라인 (별도 조사)
|
||||
운영: Discord/뒤끝/상금 (코드 외 — 표시 필드만 S2 흡수)
|
||||
```
|
||||
|
||||
- **크리티컬 패스**: S1 → S2 → S4D → S4a → S4d → S6
|
||||
- **S1 직후 병렬 가능**: S5(포스팅)
|
||||
- **S2 직후 병렬 가능**: S3(태그)
|
||||
- **S4 분할 원칙**: S4D(통합설계) 가 평가/리뷰/투표/댓글/시상 **스키마를 한 번에 동결**. 이후 S4a~S4d 는 동결된 계약을 소비만 한다 — 구현 세션이 스키마를 바꾸면 형제 세션 재작업이므로, 스키마 변경은 S4D 로 회귀해 처리.
|
||||
|
||||
## 세션 카드
|
||||
|
||||
### S1 — 권한 체계(RBAC) 기반 ★최우선 토대
|
||||
- **목표**: 사이트 관리권한 모델 + 기능별 인증유저 플래그 스캐폴드.
|
||||
- **포함**: 관리자(전체)/부관리자(허용 권한만 = 권한부여형) 모델, 관리자 콘솔(부관리자 임명 + 권한 토글), 인증유저 플래그 정의·부여(리뷰어/포스터/심사위원/기술자), 권한 체크 인터셉터.
|
||||
- **제외**: 각 권한의 실제 사용처(후속 세션이 소비).
|
||||
- **의존**: 없음.
|
||||
- **공유 자원**: `users`, `security/`, 세션/인증.
|
||||
- **권장 진입**: requirements-advisor(권한 모델 정밀화) → design → implementation.
|
||||
- **산출**: 권한 스키마 마이그레이션, 관리자 콘솔, 권한 인터셉터.
|
||||
|
||||
### S2 — 게임잼 엔티티 + 라이프사이클 ★토대
|
||||
- **목표**: 게임잼 이벤트 엔티티 + 상태전이 + 관리 + 출품작 연결.
|
||||
- **포함**: `jams` 테이블, 상태전이(모집→개발→평가→종료), 개발기간/평가기간 필드, 관리자 게임잼 CRUD, 출품작 = `games`에 잼 연결(개인·팀 모두), 게임잼 목록/상세 페이지, (운영 표시) Discord 링크·상금·후원사 필드.
|
||||
- **제외**: 평가/투표/리뷰(S4), 검색(S3).
|
||||
- **의존**: S1(게임잼 생성 권한).
|
||||
- **공유 자원**: `games`, `recruit_posts`(팀 연결), 상태전이.
|
||||
- **산출**: `jams` 스키마, 게임잼 페이지, 라이프사이클 상태머신.
|
||||
|
||||
### S3 — 태그 + 검색
|
||||
- **목표**: 게임잼+게임 공통 태그, 게임잼 태그 검색.
|
||||
- **포함**: 공유 `tags` + 조인테이블(game/jam), 게임잼 태그 검색 UI/쿼리.
|
||||
- **의존**: S2.
|
||||
- **산출**: 태그 스키마, 검색 UI.
|
||||
|
||||
### S4 — 평가·리뷰·투표 통합 ⚠️결합 클러스터 → 분할 세션화
|
||||
> §2.7 주의: 댓글/리뷰/심사/투표/시상이 같은 데이터 도메인 + 평가기간 게이트를 공유. **S4D 가 스키마를 한 번에 동결**하고, S4a~S4d 는 그 계약을 소비만 한다. 스키마 변경 필요 시 S4D 로 회귀(형제 세션 재작업 방지).
|
||||
|
||||
#### S4D — 평가 통합설계 (설계 전용 세션, 코드 0)
|
||||
- **목표**: 리뷰/댓글/심사/투표/시상 데이터모델 + 계약 + 평가기간 게이트를 한 번에 확정·동결.
|
||||
- **포함**: `reviews`(게시물당 1회, 완성도+종합 평점), `game_comments` 200자 제약, 심사 점수 스키마, 잼 투표 스키마, 3트랙 집계 규칙, 상태/기간 게이트 계약.
|
||||
- **의존**: S1, S2.
|
||||
- **권장 진입**: design-advisor.
|
||||
- **산출**: design.md + DB 스키마(동결) + contracts.
|
||||
|
||||
#### S4a — 댓글/리뷰 분리 (구현)
|
||||
- **목표**: `game_comments` 200자 제한 + 리뷰(신규, 게시물당 1회, 완성도+종합 평점) 분리.
|
||||
- **의존**: S4D.
|
||||
|
||||
#### S4b — 심사위원 평가
|
||||
- **목표**: 심사위원 권한자 점수 입력 → 심사위원 대상.
|
||||
- **의존**: S4D, S4a, S1(심사위원 권한).
|
||||
|
||||
#### S4c — 인기투표
|
||||
- **목표**: 잼 전용 1인 1표(평가기간 한정) → 인기 대상.
|
||||
- **의존**: S4D.
|
||||
|
||||
#### S4d — 시상 집계/결과
|
||||
- **목표**: 3트랙(심사/유저평점/인기) 산정 + 수상 표시.
|
||||
- **의존**: S4a·S4b·S4c 산출.
|
||||
|
||||
### S5 — 정보성/홍보성 포스팅 보드 (S1 후 병렬 가능)
|
||||
- **목표**: 운영진 전용 공지·블로그 + 외부링크 큐레이션. 유저 작성 차단.
|
||||
- **포함**: `posts` 테이블, 포스터 권한자 작성, 카테고리(유니티블로그/세션/후기인터뷰/뒤끝 개발팀), 외부링크 타입, 유저작성 차단.
|
||||
- **의존**: S1(포스터 권한). S4와 독립.
|
||||
- **산출**: `posts` 스키마, 포스터 작성 UI.
|
||||
|
||||
### S6 — 메인페이지 통합 (거의 마지막)
|
||||
- **목표**: `index.jsp` 에 게임잼+게임+포스팅 통합 노출.
|
||||
- **의존**: S2, S5(노출 대상 존재).
|
||||
- **산출**: index 확장.
|
||||
|
||||
### S7 — (차후) Unity 빌드 업로드 파이프라인 ⏸deferred
|
||||
- **목표**: WebGL 빌드 업로드 → 검증 → `/game/{uuid}/` 배치 자동화.
|
||||
- **선행**: 별도 조사(빌드 포맷/검증 규칙/보안 — zip-slip 등).
|
||||
- **현재**: 스코프 인지만, 후속 결정.
|
||||
|
||||
## 운영(코드 외)
|
||||
- **Discord**: 공지·진행은 수동 운영. 코드는 게임잼 상세에 초대 링크 표시 정도(S2 흡수).
|
||||
- **뒤끝(Bekend)**: 파트너/콘텐츠 — 포스팅 카테고리로만 등장.
|
||||
- **상금·후원**(치킨/10만원/편의점 5000원권): 게임잼 상세 표시 필드(S2) + 실제 지급은 수동.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
phase: documentation
|
||||
agent: documentation-advisor
|
||||
agent_version: 1
|
||||
generated_at: 2026-06-17T15:30:00+09:00
|
||||
concerns: []
|
||||
concerns_checked: true
|
||||
---
|
||||
|
||||
# 문서화 보고
|
||||
|
||||
## 작성/수정된 문서
|
||||
|
||||
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|
||||
|---|---|---|---|---|
|
||||
| `docs/work-log/2026-06-17-jam-platform-roadmap.md` | work-log | 신규 생성 | `docs/work-log/index.md` | `docs/analysis/2026-06-16-project-analysis.md` 하단 관련 링크 섹션 |
|
||||
| `docs/work-log/index.md` | work-log | 기존 갱신 | — | — |
|
||||
| `docs/analysis/2026-06-16-project-analysis.md` | analysis | 기존 갱신 (교차 링크 추가) | — | — |
|
||||
|
||||
## 카테고리 판정 근거
|
||||
|
||||
**선택: `work-log/`**
|
||||
|
||||
`document-category-classification.md` 분류 기준:
|
||||
- `backlog/` 명시 제외 사항: "확정된 로드맵 → 사용하지 않음"
|
||||
- `work-log/` 빠른 결정 순서 §8: "진행 중인 작업 메모인가? → work-log/"
|
||||
- 이 로드맵은 S1~S7 순서로 진행될 다중 세션 작업의 실행 계획이며, 각 후속 세션 진입 시 의존 순서·스코프 경계를 확인하기 위해 참조되는 세션 간 handoff 기준 문서다.
|
||||
- 아키텍처 구조 설명(`architecture/`)이나 불변 기술 결정(`adr/`)이 아닌 실행 계획이므로 보수적으로 `work-log/`를 선택.
|
||||
|
||||
**신규 카테고리 생성**: 없음.
|
||||
|
||||
## 의사결정 기록 위치
|
||||
|
||||
- 사용자 확정 결정 8건: `.atp/work-session/20260617-150635/report.md` §Decisions
|
||||
- 결합 트랙 판정(§2.7): `.atp/work-session/20260617-150635/report.md` §Coupling Analysis
|
||||
- Advisor 호출 결정: `.atp/work-session/20260617-150635/report.md` §Advisor Invocation Decision Log
|
||||
|
||||
## 추후 문서화가 필요한 항목
|
||||
|
||||
- S1 세션 완료 시: RBAC 스키마 마이그레이션 → `changes/` + 권한 모델 설계 → `architecture/` 또는 `contracts/`
|
||||
- S4D 세션 완료 시: 평가 통합 데이터모델 동결 → `contracts/` (내부 스키마 계약)
|
||||
- S7 조사 완료 시: Unity WebGL 업로드 파이프라인 결정 → `adr/` (되돌리기 어려운 기술 선택)
|
||||
- 각 세션 완료 후: `work-log/2026-06-17-jam-platform-roadmap.md` 의 해당 세션 카드 status 갱신 권장
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
---
|
||||
schema_version: 2
|
||||
session_id: 20260617-150635
|
||||
resumed_from: null
|
||||
started_at: 2026-06-17T15:06:35+09:00
|
||||
ended_at: 2026-06-17T15:20:00+09:00
|
||||
user_request: |
|
||||
고도화 위한 계획 수립. 단위별로 끊어서(세션별) 작업할 수 있도록 분류·정리.
|
||||
사용자가 공유한 모호한 메모(게임잼 운영/평가/리뷰/관리자/포스팅/검색 등)를
|
||||
각 항목 의미를 확인하며 계획화. 이번 세션은 계획 수립 전용(코드 변경 0).
|
||||
---
|
||||
|
||||
# Summary
|
||||
게임잼 플랫폼 고도화 기획 세션. 기존 bibimbap(게임 공유 + 팀원 모집) 위에
|
||||
게임잼 운영/평가/리뷰/관리자(RBAC)/포스팅/태그검색 신규 기능군을 얹는 요청.
|
||||
메모가 무분별 → requirements 확정 전 orchestrator 가 직접 파싱 + 배치 질의로 의미 확정.
|
||||
세션 분할(단위별 새 세션) 요구 → §2.7 결합 트랙 판정이 핵심: 공유 자원(게임잼 엔티티/
|
||||
평가 데이터모델/RBAC) 기준으로 선후 순서를 강제해야 재작업 방지.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
- advisor: requirements-advisor
|
||||
decision: pending
|
||||
rationale: '메모가 극도로 모호 → advisor 디스패치 전 orchestrator 가 사용자 배치 질의로 의미 확정 선행. 확정 후 requirements-advisor 로 FR/NFR 분해 디스패치 예정.'
|
||||
checked_at: 2026-06-17T15:06:35+09:00
|
||||
- advisor: graphify-lookup-advisor / research-advisor
|
||||
decision: skip
|
||||
rationale: '코드 구조는 기존 분석 세션(20260616-111711) + 직접 grep 으로 충분 파악. 외부 연동 모두 운영/차후로 확정 → 이번 세션 외부 조사 불필요.'
|
||||
checked_at: 2026-06-17T15:06:35+09:00
|
||||
- advisor: design-advisor / implementation-advisor / verification-advisor
|
||||
decision: skip
|
||||
rationale: '사용자가 "계획만 확정, 종료" 선택 → 코드 변경 0. 설계·구현·검증은 각 후속 세션(S1~)에서 수행.'
|
||||
checked_at: 2026-06-17T15:16:00+09:00
|
||||
- advisor: documentation-advisor
|
||||
decision: call
|
||||
rationale: '확정된 로드맵을 docs/ 적절 카테고리에 영구 저장 + index 연결(docs-first 참조용). 카테고리 분류는 advisor 책임 영역.'
|
||||
checked_at: 2026-06-17T15:16:00+09:00
|
||||
|
||||
# Invocations
|
||||
- id: inv-001
|
||||
layer: orchestrator
|
||||
name: orchestrator
|
||||
parent_invocation_id: null
|
||||
input_digest: '게임잼 고도화 메모 24항목 파싱 + 기존 도메인 구조 대조'
|
||||
output_digest: '소프트웨어 기능 vs 운영 로지스틱 분리 + 결합 트랙 식별 + 배치 질의 설계'
|
||||
artifacts: []
|
||||
concerns: []
|
||||
|
||||
# Decisions
|
||||
- by: user
|
||||
at: 2026-06-17T15:10:00+09:00
|
||||
decision: '게임잼 = 기존 bibimbap 위 신규 대형기능. games/game_likes/game_comments 재사용·확장, 게임 공유/모집은 유지.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:10:00+09:00
|
||||
decision: '시상 3개 독립 트랙(상호 무관): ①심사위원 대상(심사위원 선정) ②유저평점 대상(유저 리뷰의 완성도+종합리뷰 평점 집계) ③인기투표 대상(최고 게임 투표). 스코어 대상 폐기.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:10:00+09:00
|
||||
decision: '권한 체계 = 사이트 관리권한. 관리자(전체) / 부관리자(허용된 권한만 열림 = 권한부여형). 심사위원은 관리권한과 별개. 향후 기능별 인증유저(리뷰어/포스터/기술자 등) 추가 예정.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:10:00+09:00
|
||||
decision: '포스팅 = 운영진 전용 공지·블로그 보드 + 외부링크 큐레이션. 유저 작성 절대 불가. 콘텐츠 카테고리: 유니티블로그/세션/게임잼대상 후기인터뷰/뒤끝 개발팀.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:14:00+09:00
|
||||
decision: '출품작 = 기존 games 재사용 + 잼 연결(jam_id/조인). 참가 단위 개인·팀 모두.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:14:00+09:00
|
||||
decision: '인기투표 = 게임잼 전용 1인 1표(평가기간 한정). game_likes 와 별개.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:14:00+09:00
|
||||
decision: '외부 연동: Unity WebGL 빌드 업로드 파이프라인만 in-scope(차후 단계). Discord/뒤끝 = 운영/링크/콘텐츠만, 코드 연동 없음.'
|
||||
- by: user
|
||||
at: 2026-06-17T15:14:00+09:00
|
||||
decision: '태그 = 게임잼 + 게임(출품작) 공통 태그 테이블. 검색은 게임잼 중심.'
|
||||
|
||||
# Coupling Analysis (§2.7)
|
||||
# 공유 자원 기준 결합 판정 — "1메모=1세션" 분할 시 재작업 폭증 방지.
|
||||
foundations:
|
||||
- RBAC 권한체계 (관리자/부관리자 + 기능별 인증유저 플래그) — 게임잼관리·포스팅·심사위원의 전제
|
||||
- 게임잼 엔티티 + 라이프사이클 (모집→개발→평가→종료) — 대부분 기능이 매달림
|
||||
- 공유 태그 모델 — 검색 + 게임잼/게임
|
||||
coupled_cluster:
|
||||
# 같은 데이터 도메인(평가/리뷰/투표/댓글) + 게임잼 평가기간 게이트 공유 → 한 설계로 동결 후 구현 분할
|
||||
- 댓글 vs 리뷰 분리 (댓글 200자 / 리뷰 게시물당1회+평점)
|
||||
- 심사위원 평가 (→ 심사위원 대상)
|
||||
- 인기투표 잼전용 1인1표 (→ 인기 대상)
|
||||
- 3트랙 시상 집계/결과
|
||||
relatively_independent:
|
||||
- 정보성/홍보성 포스팅 보드 (RBAC 포스터 권한만 의존, 평가 클러스터와 무관 → 병렬 가능)
|
||||
deferred:
|
||||
- Unity WebGL 빌드 업로드 파이프라인 (차후, 별도 조사 필요)
|
||||
operational_only:
|
||||
- Discord 공지·진행(수동, 링크 표시만) / 뒤끝 파트너·콘텐츠 / 상금·후원 표시 필드
|
||||
|
||||
# Open Items
|
||||
- 메모 24항목 의미 확정 — 완료
|
||||
- 세션 분할 단위 확정 — 완료 (S1~S7, S4 분할 세션화 확정)
|
||||
- **미커밋 잔여(이번 작업 단위)**: `docs/work-log/2026-06-17-jam-platform-roadmap.md`(신규), `docs/work-log/index.md`(링크), `docs/analysis/2026-06-16-project-analysis.md`(교차링크) — 사용자 커밋 확인 대기. 브랜치 feat/v2.
|
||||
- **이번 작업 단위 외 잔여(이전 세션)**: `.serena/project.yml`(M), `.serena/memories/serena_tooling.md`(??), `.atp/work-session/20260617-094450·143315`(??) — 본 세션 미관여, 별도 처리.
|
||||
- **다음 시작점**: S1(RBAC) — 사용자가 새 세션에서 `/atp:task S1` 류로 진입. docs/work-log 로드맵 참조.
|
||||
|
||||
# verified_by_me
|
||||
- L1/L2: n/a (코드 변경 0줄, 계획 수립 전용 세션)
|
||||
- 문서 배치 검증: docs/work-log/2026-06-17-jam-platform-roadmap.md 본문(S1~S7 카드 + S4D/S4a~d 분할 + 의존그래프 + 크리티컬패스) grep 보존 확인. work-log/index.md 링크 1건 추가 확인. frontmatter kind/status/source_session 정상.
|
||||
|
||||
# needs_user_verification
|
||||
- (없음) — 계획 문서 산출만. 단 미커밋 docs 3건 커밋 여부는 사용자 결정.
|
||||
|
||||
# graph_refresh
|
||||
- skip: no-scope-change (§3.2) — src/ scope diff 0, docs/ 전용 변경. fresh 취급. (atp-graphify add-on enable 상태이나 scope 변경 없어 호출 불필요)
|
||||
|
||||
# project_gate
|
||||
- skip: no-project-gate — 코드 변경 0, 런타임/배포 대상 없음.
|
||||
|
||||
# User Signals
|
||||
user_signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: '배치 질의 8건에 모두 명확·결단적으로 답변, 로드맵을 1라운드 만에 수락(S4 분할만 보강)'
|
||||
about: '모호 메모 → 기존 코드 대조 파싱 → 배치 AskUserQuestion → 결합 인지 로드맵 흐름'
|
||||
negative: []
|
||||
|
||||
# Retrospective (inline — retrospective-advisor dispatch skip)
|
||||
# skip 사유: 코드 변경 0 계획 세션 + 마찰 negative 시그널 0 + 신규 교훈(기존 §2.7/§4.4 적용 외) 없음.
|
||||
- what_went_well:
|
||||
- 메모 24항목을 기존 엔티티(games/game_comments/game_likes/recruit_posts) 대조로 신규 vs 기존 vs 운영 분류 → 헛스코프 방지
|
||||
- §2.7 결합 판정으로 "1메모=1세션" 재작업 함정 사전 차단, S4 통합설계(S4D) 동결 게이트 설계
|
||||
- §4.4 배치 질의(2라운드×4) + Recommended + 기존자산 인용으로 의미 확정 효율화
|
||||
what_to_improve:
|
||||
- 인기투표가 game_likes 재사용 아닌 잼 전용 투표로 밝혀짐 — 1차 파싱서 좋아요=인기로 단정했던 가정을 질의로 교정(시그널 세탁 없이 반영)
|
||||
memory_candidates: [] # 기존 프로토콜(§2.7/§4.4) 적용 외 신규·재현 교훈 없음 → MEMORY 반영 불필요
|
||||
protocol_feedback: []
|
||||
applied_changes: []
|
||||
|
||||
# Notes Parse (작업 입력)
|
||||
# 사용자 원문 메모를 소프트웨어 기능군 vs 운영 로지스틱으로 1차 분류.
|
||||
software_features:
|
||||
- 게임잼 엔티티/운영 (개발기간 1주 + 평가기간 3일~1주, 상태 전이)
|
||||
- 평가 체계 (심사위원제 완성도 / 인기=좋아요집계 / 스코어?)
|
||||
- 리뷰 vs 댓글 분리 (리뷰=게시물당 1회+평점, 댓글=200자)
|
||||
- 관리자/부관리자 RBAC
|
||||
- 정보성/홍보성 포스팅 (블로그/보드)
|
||||
- 태그 검색 (게임잼 검색)
|
||||
- 메인페이지 통합 (게임잼 포함 전부 노출)
|
||||
operational_logistics:
|
||||
- 상금/후원 (치킨3마리 / 총괄 10만원 / 음료수·편의점 5000원권) — 코드 아님(상금 표시 UI 는 별개)
|
||||
- Discord 공지·진행 (연동 깊이 미확정)
|
||||
- Unity 엔진 (제출 메타 vs WebGL 호스팅 — 이미 /game/** 호스팅 존재)
|
||||
- 뒤끝(Bekend BaaS) 개발팀 (파트너/후원 vs 기술연동 미확정)
|
||||
content_seeds:
|
||||
# "정보성/홍보성 포스팅" 의 예시 콘텐츠로 추정
|
||||
- 유니티 블로그 / 세션 / 게임잼 대상 후기인터뷰 / 뒤끝 개발팀
|
||||
|
||||
# User Signals
|
||||
user_signals:
|
||||
positive: []
|
||||
negative: []
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
phase: documentation
|
||||
agent: documentation-advisor
|
||||
agent_version: 1
|
||||
generated_at: 2026-06-17T17:10:00+09:00
|
||||
concerns:
|
||||
- "status: active 유지 — 재구조된 로드맵은 여전히 진행 중인 계획이므로 active 유지 적절."
|
||||
- "restructured_by 필드 신규 도입 — 표준 frontmatter에 없는 커스텀 필드이나, 세션 추적성을 위해 source_session 보완 용도로 허용. 과도한 필드 추가 아님."
|
||||
- "documentation-guidelines.md 미존재 — docs/development/documentation-guidelines.md 파일이 없어 참조 불가. document-category-classification.md로 카테고리 판별 수행."
|
||||
concerns_checked: true
|
||||
---
|
||||
|
||||
# 문서화 보고
|
||||
|
||||
## 작성/수정된 문서
|
||||
|
||||
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|
||||
|---|---|---|---|---|
|
||||
| `docs/work-log/2026-06-17-jam-platform-roadmap.md` | work-log | 기존 문서 전면 재작성 | `docs/work-log/index.md` 항목 설명문 갱신 | report.md(20260617-162536) → Decisions 섹션이 본 doc 재구조 트리거로 교차 참조 |
|
||||
| `docs/work-log/index.md` | work-log | 기존 index 항목 설명문 갱신 | — (index 자체) | — |
|
||||
|
||||
## 의사결정 기록 위치
|
||||
|
||||
- `.atp/work-session/20260617-162536/report.md` — Decisions 섹션에 목적 축 재분류 확정 전 과정(S1~S7 → W1~W4) 기록. Emergent Categories + final_workstreams + key_reframes 포함.
|
||||
- `docs/work-log/2026-06-17-jam-platform-roadmap.md` — 재구조된 로드맵 본문. "150635 대비 핵심 교정 5건" 섹션에 교정 이유 명시. S→W 매핑 표로 추적성 보존.
|
||||
|
||||
## 적용한 섹션 목록
|
||||
|
||||
1. frontmatter — title/description/kind/status/owner/created_at/source_session 유지 + `restructured_by: 20260617-162536` 신규 필드 추가
|
||||
2. 본문 도입 — 재구조 이유 + 4개 워크스트림 + 게임잼=W2 명시
|
||||
3. W1 — 거버넌스/RBAC (★토대)
|
||||
4. W2 — 게임잼 (★본체): W2-1~W2-6 세분화
|
||||
5. W3 — 사이트 플랫폼: W3-1~W3-5 세분화, 잼 일정 분리 가능 명시
|
||||
6. W4 — 유저 배지/평판 (★신규 워크스트림, 150635 누락)
|
||||
7. 운영 (코드 외) 섹션
|
||||
8. 의존성 그래프 / 빌드 순서 — ASCII 코드블록, 워크스트림 라벨로 재서술
|
||||
9. S→W 매핑 표 — 추적성 보존 (이전 handoff 호환)
|
||||
10. 150635 대비 핵심 교정 5건
|
||||
|
||||
## S→W 매핑 포함 여부
|
||||
|
||||
포함. `docs/work-log/2026-06-17-jam-platform-roadmap.md` 내 "S → W 매핑 (추적성)" 섹션에 표 형식으로 작성. 구 S1~S7 전체 매핑 + 교정 사항 명시.
|
||||
|
||||
## 추후 문서화가 필요한 항목
|
||||
|
||||
- W2-3 잼 평가 통합설계(스키마 동결) 세션 진입 시 → design.md + contracts/ 문서 신규 작성 필요 (동결 스키마 + 집계 계약)
|
||||
- W4 유저 배지/평판 — 신규 발굴 워크스트림이므로 상세 requirements 확정 시 별도 work-log 또는 domain/ 문서 권장
|
||||
- W3-5 Unity 업로드 조사 결과 → analysis/ 문서로 기록 필요 (빌드 포맷/검증/보안 조사)
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
---
|
||||
schema_version: 2
|
||||
session_id: 20260617-162536
|
||||
resumed_from: null
|
||||
started_at: 2026-06-17T16:25:36+09:00
|
||||
ended_at: 2026-06-17T17:16:00+09:00
|
||||
user_request: |
|
||||
고도화를 위해 세션을 나눴었는데, 기능 관련 계획은 나뉜 것 같다.
|
||||
그러나 각각 "왜 쓰고 무엇을 위해 존재하는지"(목적 축)는 명확하게 안 나뉜 것 같아
|
||||
하나씩 짚어보려 한다. 제공했던 메모가 모두 게임잼을 위해 존재했던 것은 아니다.
|
||||
→ 직전 세션(20260617-150635)이 메모 24항목을 전부 "게임잼 플랫폼 고도화" 프레임으로
|
||||
묶은 데 대한 목적 축 재분류 요청.
|
||||
---
|
||||
|
||||
# Summary
|
||||
직전 150635 세션 산출 로드맵(S1~S7)을 "목적(존재 이유)" 축으로 재검토하는 분석 세션.
|
||||
150635 는 기능 의존성(공유 자원 §2.7) 축으로는 잘 쪼갰으나, 모든 항목을 "게임잼" 목적
|
||||
프레임 아래 배치 → 사이트 전반용 기능(RBAC/포스팅/태그/리뷰·댓글/Unity업로드/메인)과
|
||||
게임잼 전용 기능(잼 엔티티/심사/인기투표/시상)이 목적상 섞여 보이는 framing gap.
|
||||
사용자 요청 = 항목별 "게임잼 전용 vs 사이트 플랫폼 vs 공유" 목적 재분류 후 하나씩 확정.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
- advisor: requirements-advisor
|
||||
decision: pending
|
||||
rationale: '본질이 목적 축 재분류 + 사용자와 1:1 대화형 확정. advisor 는 AskUserQuestion 불가 → orchestrator 직접 대화로 의미 확정 선행. 필요 시 확정 후 로드맵 재작성을 documentation-advisor 로 위임.'
|
||||
checked_at: 2026-06-17T16:26:00+09:00
|
||||
- advisor: graphify-lookup / research-advisor
|
||||
decision: skip
|
||||
rationale: '코드 신규 조사 불요 — 기존 엔티티(games/game_comments/game_likes/recruit_posts)는 150635·110836 세션에서 확인됨. 본 세션은 기존 산출물(로드맵+decisions) 해석.'
|
||||
checked_at: 2026-06-17T16:26:00+09:00
|
||||
- advisor: requirements-advisor
|
||||
decision: skip(완료)
|
||||
rationale: '목적축 의미확정을 orchestrator 가 사용자 1:1 배치질의(S1~S7 + 플래그)로 완료. advisor 재분해 불요 — 산출 결정이 이미 확정적.'
|
||||
checked_at: 2026-06-17T16:55:00+09:00
|
||||
- advisor: documentation-advisor
|
||||
decision: call
|
||||
rationale: '확정된 W1~W4 목적 워크스트림을 로드맵 doc 에 W중심 전면 재구조로 반영. docs 카테고리/index/guidelines 준수는 advisor 책임. 사용자 "W중심 전면 재구조" 선택.'
|
||||
checked_at: 2026-06-17T16:55:00+09:00
|
||||
|
||||
# Invocations
|
||||
- id: inv-001
|
||||
layer: orchestrator
|
||||
name: orchestrator
|
||||
started_at: 2026-06-17T16:25:36+09:00
|
||||
input_digest: '150635 로드맵 + report decisions + notes parse 수집'
|
||||
output_digest: '목적 축 재분류표 작성, 사용자 확정 대기'
|
||||
model_choice:
|
||||
phase: analyze
|
||||
dispatch_size: direct
|
||||
tier: large
|
||||
effort: medium
|
||||
resolved_model: inherit
|
||||
capped: false
|
||||
rationale: '프레이밍·목적 판정은 다항목 교차 추론(§5.4 axis4) → orchestrator 직접 보유. 코드 변경 0.'
|
||||
|
||||
# Decisions
|
||||
- by: user
|
||||
at: 2026-06-17T16:30:00+09:00
|
||||
decision: '진행 방식 = S1부터 하나씩 목적 확정 → 종료 시 로드맵 doc 에 목적 축 반영.'
|
||||
- by: user
|
||||
at: 2026-06-17T16:34:00+09:00
|
||||
decision: |
|
||||
S1 "RBAC" 은 성격이 다른 3개가 한 카드에 뭉쳐 있었음 (150635 오분류):
|
||||
1. 관리자/부관리자 = 운영 중심 설계 = 진짜 RBAC (사이트 거버넌스, 잼 무관)
|
||||
2. 심사위원 = 게임잼 전용 역할 권한
|
||||
3. 리뷰어 = 권한 아님, 유저 '배지/평판' 개념 (업로드 게임 인증 리뷰어) → RBAC 도메인 분리 필요
|
||||
포스터/기술자 = 미확정 (후속 질의).
|
||||
- by: user
|
||||
at: 2026-06-17T16:40:00+09:00
|
||||
decision: |
|
||||
S1 플래그 4종 최종 분해:
|
||||
- 포스터 = 운영 작성 권한(부관리자 토글) → S1 RBAC 흡수, 별도개념 아님
|
||||
- 심사위원 = 게임잼 전용 역할 권한 → 잼 도메인으로 이관
|
||||
- 리뷰어 = 유저 배지/평판 (업로드 게임 인증 리뷰어)
|
||||
- 기술자 = 유저 배지/평판 (개발정보 공유/적극 업로더 인정, 임시명칭)
|
||||
→ "리뷰어+기술자" = 제3의 기능군 '유저 배지/평판 시스템'. 게임잼·RBAC 와 별개.
|
||||
150635 가 RBAC '인증유저 플래그' 로 통째 오분류 → 누락된 워크스트림 발굴.
|
||||
|
||||
# Emergent Categories (목적 축 재분류 결과 — 누적)
|
||||
# 150635 의 게임잼-단일프레임을 목적별로 분해. 항목 확정될 때마다 갱신.
|
||||
purpose_axes:
|
||||
site_governance_rbac:
|
||||
- 관리자/부관리자 권한모델 + 권한 토글(게임잼관리/포스팅작성 등) + 인터셉터 (구 S1)
|
||||
game_jam_only:
|
||||
- 심사위원 역할 권한 (구 S1 일부)
|
||||
- 게임잼 엔티티 + 라이프사이클 + 출품작 연결 + 상세페이지 (S2)
|
||||
# 속성: 회차 독립(다중 인스턴스). 통합지점: 출품작이 잼뷰 + 일반 게임노출 페이지 이중 노출(S6 로 흐름).
|
||||
# 운영표시필드(Discord/상금/후원) = operational, 표시만.
|
||||
user_badge_reputation: # ★신규 발굴 — 150635 누락
|
||||
- 리뷰어 배지 (인증 리뷰어)
|
||||
- 기술자 배지 (기여 인정, 임시명칭)
|
||||
site_platform:
|
||||
- 태그 모델 + 검색 = 일반 게임 발견성 중심 (S3). 잼용도: 이전회차 검색 + 진행회차 강조(부수). 잼일정 분리 가능.
|
||||
- 댓글/리뷰 분리 (S4a) = 모든 게임 페이지 일반기능. 댓글 200자 / 리뷰 게시물당1회+평점.
|
||||
# 리뷰주체=아무나(게임당1회), 리뷰어 배지=평판인정(→ user_badge_reputation 연결).
|
||||
# 잼 게이트 없음. 잼 연결=시상(S4d)이 리뷰 평점을 단방향 집계. ★S4D 동결묶음에서 분리해야.
|
||||
game_jam_only_eval:
|
||||
- 심사위원 평가 (S4b) — 평가기간 게이트, S1 심사위원 역할 소비
|
||||
- 인기투표 (S4c) — 잼 1인1표 평가기간, game_likes 와 별개
|
||||
- 시상 집계 3트랙 (S4d) — 심사/유저평점/인기. 유저평점만 S4a 리뷰평점 단방향 집계
|
||||
# ★S4D 재프레임: 잼 평가스키마(심사/투표/시상) + S4a→S4d 집계계약만 동결. 댓글/리뷰 스키마는 일반(별도설계).
|
||||
site_platform_more:
|
||||
- 포스팅 보드 (S5) = 사이트 콘텐츠/마케팅(공지·블로그·외부링크). 포스터=S1 운영권한. 잼은 포스팅 주제 1개로 포함. 잼일정 완전분리.
|
||||
- 메인 = 게임 허브 노출 (S6) = 게임 + 잼출품작 모이는 공간. ★포스팅은 별도 메뉴(150635 "메인 통합" 교정).
|
||||
- Unity WebGL 업로드 자동화 (S7) = 일반 게임 호스팅 자동화(모든 제출). 현 수동 호스팅 대체. deferred(조사선행).
|
||||
|
||||
# 목적 축 종합 (전 항목 확정)
|
||||
# 150635 단일 "게임잼" 프레임 → 4개 목적 워크스트림 + 운영으로 분해.
|
||||
final_workstreams:
|
||||
W1_거버넌스_RBAC:
|
||||
- 관리자/부관리자 권한모델 + 권한토글(게임잼관리/포스터=포스팅작성) + 인터셉터 (구 S1)
|
||||
W2_게임잼:
|
||||
- 게임잼 엔티티/라이프사이클/출품작연결/상세 (S2)
|
||||
- 심사위원 역할 권한 (구 S1 일부)
|
||||
- S4D 잼평가 통합설계(심사/투표/시상 스키마 + S4a→S4d 집계계약 동결)
|
||||
- 심사위원평가(S4b) / 인기투표(S4c) / 시상집계(S4d)
|
||||
W3_사이트플랫폼:
|
||||
- 태그+검색 일반 게임 발견성 (S3)
|
||||
- 댓글/리뷰 분리 일반 게임기능 (S4a)
|
||||
- 포스팅 보드 별도 메뉴 (S5)
|
||||
- 메인 게임 허브 노출 (S6)
|
||||
- Unity 업로드 자동화 (S7, deferred)
|
||||
W4_유저배지평판: # ★신규 — 150635 누락
|
||||
- 리뷰어 배지(S4a 리뷰활동 기반) / 기술자 배지(기여인정, 임시명칭)
|
||||
operational_표시만:
|
||||
- Discord/뒤끝/상금·후원 (코드 외, 표시 필드만)
|
||||
key_reframes_vs_150635:
|
||||
- 댓글/리뷰(S4a) = 일반기능 → 잼 S4D 동결묶음에서 분리(시상은 단방향 집계만)
|
||||
- 포스팅(S5) = 별도 메뉴, 메인(S6) 통합 아님
|
||||
- 태그/검색(S3)·Unity(S7) = 사이트 일반 인프라(잼은 소비자)
|
||||
- 유저 배지/평판(W4) = 누락된 신규 워크스트림 발굴
|
||||
- S4D 동결 범위 = 잼 평가만으로 축소
|
||||
- 의존성 그래프(빌드순서)는 유효 — 목적 축은 "왜/독립출시여부"를 추가로 분리
|
||||
|
||||
# Regression (§2.6)
|
||||
- surfaced_at_stage: '본 세션 목적축 재검 (S1)'
|
||||
source_stage: '150635 requirements/notes-parse'
|
||||
defect: '"기능별 인증유저 플래그(리뷰어/포스터/심사위원/기술자)" 를 전부 RBAC 권한으로 동질 취급 → 리뷰어=배지(평판) 오분류'
|
||||
full_set_recheck: true
|
||||
downstream_rerun: ['로드맵 S1 카드 목적 재서술', '잔여 플래그(포스터·기술자) 전수 재확인']
|
||||
resolved_at: null
|
||||
|
||||
# Open Items
|
||||
- 항목별 목적 축(게임잼전용/사이트플랫폼/공유) 사용자 확정
|
||||
- 로드맵 doc 재프레이밍 여부 결정 (게임잼 = 워크스트림 1개 vs 전체 프레임)
|
||||
|
||||
# ── 세션 종료 (§9) ──
|
||||
|
||||
# Conflicts
|
||||
- (없음) — 단일 documentation-advisor, advisor 간 충돌 없음.
|
||||
|
||||
# verified_by_me
|
||||
- L1/L2: n/a — 코드 변경 0줄(docs 전용 재구조 세션).
|
||||
- 문서 검증(직접 Read): roadmap doc 본문에 W1~W4 + 운영 + 의존성 그래프(ASCII) + S→W 매핑표(구 S1~S7 전체) + 교정 5건 보존 확인. frontmatter title/description/restructured_by 갱신 확인. work-log/index.md 링크 설명문 W중심으로 갱신 확인.
|
||||
|
||||
# needs_user_verification
|
||||
- (없음) — 계획 문서 재구조만. 런타임/배포 대상 없음.
|
||||
|
||||
# graph_refresh
|
||||
- skip: no-scope-change (§3.2) — src/·pom.xml diff 0, docs/ + work-session 전용. fresh 취급.
|
||||
|
||||
# project_gate
|
||||
- skip: no-project-gate — 코드 변경 0.
|
||||
|
||||
# Open Items / 미커밋 잔여 (§9.5)
|
||||
# 이번 작업 단위 (162536):
|
||||
- docs/work-log/2026-06-17-jam-platform-roadmap.md — 전면 재작성(W중심). 직전 150635 untracked 위에 갱신.
|
||||
- docs/work-log/index.md (M) — 링크 설명문 갱신.
|
||||
- .atp/work-session/20260617-162536/ — 본 세션 durable history(추적 기본, ADR-0010).
|
||||
# 이번 단위 외 잔여 (이전 세션 — 본 세션 미관여):
|
||||
- .serena/project.yml (M), .serena/memories/serena_tooling.md (??)
|
||||
- .atp/work-session/{20260617-094450,143315,150635}/ (??) ← 150635 roadmap 원본 포함
|
||||
- docs/analysis/2026-06-16-project-analysis.md (M)
|
||||
# 커밋 정책: harness "사용자 요청 시에만 커밋". 자동 커밋 안 함 → 사용자 결정 대기. 브랜치 feat/v2.
|
||||
|
||||
# User Signals
|
||||
user_signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: 'S1~S7 + 플래그 8건 배치질의에 모두 결단적·구체 답변(리뷰어=배지, 기술자=배지, 포스터=운영권한, S6 포스팅 별도메뉴 등), 목적축 재분류 1회 수락'
|
||||
about: '목적축 1:1 워크스루 → W중심 재구조 흐름'
|
||||
negative: []
|
||||
|
||||
# Regression (§2.6) — 마감
|
||||
- surfaced_at_stage: '본 세션 목적축 재검 (S1~S7 전수)'
|
||||
source_stage: '150635 requirements/notes-parse (의존성축 분할 시 목적축 누락)'
|
||||
defect: '메모를 의존성/공유자원 축으로만 분할 → 단일 "게임잼" 목적 프레임이 비대상 기능(RBAC·태그·댓글리뷰·포스팅·메인·Unity)을 흡수. "인증유저 플래그"를 동질 취급해 리뷰어/기술자(배지) 오분류 → W4 워크스트림 누락.'
|
||||
full_set_recheck: true # S1~S7 전수 목적 재확인
|
||||
downstream_rerun: ['로드맵 doc W중심 전면 재구조', 'index.md 링크 갱신']
|
||||
resolved_at: 2026-06-17T17:15:00+09:00
|
||||
|
||||
# Retrospective (inline — retrospective-advisor dispatch skip)
|
||||
# skip 사유: 코드 변경 0 + 본 세션 마찰 negative 0. 단 150635 발원 구조결함 1건 표면화 → 재현성 교훈 존재 → memory_candidate 1건 제안.
|
||||
- what_went_well:
|
||||
- 의존성축(150635) vs 목적축(본 세션) 2축 분리 인지 → 사용자 ground truth 로 항목별 "왜 존재" 1:1 확정
|
||||
- 다항목(플래그 4종) 한 항목 결함(리뷰어=배지) 발견 시 §2.6 전수 재검 → W4 누락 워크스트림 발굴
|
||||
- documentation-advisor self-contained 브리프로 W중심 재구조 1회 완결
|
||||
what_to_improve:
|
||||
- 기획/세션분할 세션에서 의존성축만으로 쪼개면 목적축(왜/누구를 위한가)이 단일 프레임에 묻힘 — 분할 시 두 축 병행 확인 필요
|
||||
memory_candidates:
|
||||
- name: planning-purpose-vs-dependency-axis
|
||||
type: feedback
|
||||
description: '기능 요청을 세션/단위로 분할할 때 의존성 축(빌드순서)과 목적 축(왜 존재/누구를 위한가)을 분리 확인. 한 축만으로 쪼개면 비대상 기능이 단일 프레임에 흡수되고, 묶음 라벨("인증유저 플래그" 등) 동질 취급이 도메인 오분류(권한 vs 배지/평판)를 부른다.'
|
||||
rationale_for_saving: '150635(의존성축 우수) → 목적축 누락으로 4개 워크스트림이 단일 게임잼 프레임에 흡수된 실증. 기획 세션 재발 가능.'
|
||||
signal_source: positive
|
||||
docs_sync_target: null # 내부 기획 흐름 교훈 → MEMORY 단독. (프로토콜 §2.7/§4.2.2 와 인접하나 planning-split 특화)
|
||||
protocol_feedback: []
|
||||
applied_changes: []
|
||||
|
||||
# ended_at: 2026-06-17T17:16:00+09:00
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
---
|
||||
schema_version: 2
|
||||
session_id: 20260617-172407
|
||||
resumed_from: null
|
||||
started_at: 2026-06-17T17:24:07+09:00
|
||||
ended_at: 2026-06-17T17:40:00+09:00
|
||||
user_request: |
|
||||
W3에서 기능별로 골자는 정해두고 싶어. 당장 작업하려는건 아니야
|
||||
---
|
||||
|
||||
# Summary
|
||||
W3(사이트 플랫폼 워크스트림) 5개 서브기능(W3-1 태그+검색, W3-2 댓글/리뷰, W3-3 포스팅 보드, W3-4 메인 허브, W3-5 Unity 업로드 deferred)의 "골자"를 정의하는 기획 정리 세션. 사용자 명시: 당장 구현 아님(코드 0줄). 산출은 문서 only.
|
||||
|
||||
# Invocations
|
||||
- id: inv-001
|
||||
layer: orchestrator
|
||||
name: orchestrator
|
||||
started_at: 2026-06-17T17:24:07+09:00
|
||||
input_digest: "W3 기능별 골자 정의 요청 (구현 아님)"
|
||||
output_digest: "프로토콜+로드맵 로드, 골자 깊이/범위 미결 → 계획 게이트 진입 예정"
|
||||
artifacts: []
|
||||
concerns: []
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
# 각 advisor 호출/스킵 판단 즉시 1줄 append
|
||||
- advisor: requirements-advisor
|
||||
decision: call
|
||||
rationale: '골자=기능 윤곽+미결질문 도출 = requirements 도메인. 코드현황 대조 필요(컨텍스트 오염 방지 위임). tier=medium/effort=medium/s-batch'
|
||||
checked_at: 2026-06-17T17:24:07+09:00
|
||||
- advisor: design-advisor
|
||||
decision: skip
|
||||
rationale: '윤곽 카탈로그 깊이 — 미결질문을 남기는 게 목적, 오픈질문0 완결설계 아님. 본격 design 은 기능별 착수 시'
|
||||
checked_at: 2026-06-17T17:24:07+09:00
|
||||
- advisor: verification-advisor
|
||||
decision: skip
|
||||
rationale: '코드 변경 0줄(문서 only 기획) — 검증 대상 없음'
|
||||
checked_at: 2026-06-17T17:24:07+09:00
|
||||
- advisor: graph-refresh-checker
|
||||
decision: skip
|
||||
rationale: 'src/ scope 변경 없음(docs only) — no-scope-change'
|
||||
checked_at: 2026-06-17T17:24:07+09:00
|
||||
|
||||
# Decisions
|
||||
- by: user
|
||||
at: 2026-06-17T17:24:07+09:00
|
||||
decision: "골자 깊이=윤곽 카탈로그 / W3-5 deferred=포함(조사항목 스텁만)"
|
||||
rationale: "당장 구현 아님 — 미결질문 남기는 1페이지 카탈로그"
|
||||
related_invocations: [inv-001]
|
||||
- by: orchestrator
|
||||
at: 2026-06-17T17:24:07+09:00
|
||||
decision: "산출 카테고리 backlog → work-log 정정"
|
||||
rationale: "document-category-classification §backlog 은 '확정 로드맵 제외' 명시. W3 골자는 확정 로드맵(W1~W4) 구체화 = work-log(로드맵과 동일 카테고리, 시점성 기획). 자체 발견 정정(§2.3)"
|
||||
related_invocations: [inv-001]
|
||||
|
||||
# Conflicts
|
||||
|
||||
# Open Items
|
||||
- 미커밋 잔여: docs/work-log/2026-06-17-w3-feature-skeletons.md(신규), docs/work-log/index.md(수정), .atp/work-session/20260617-172407/(신규). 커밋 여부 사용자 판단 대기(CLAUDE.md 커밋 정책 = 명시 요청 시).
|
||||
- QG-3 (코드 잠재 버그): /game/** 정적 핸들러 미등록(UploadResourceConfig 는 /profile/** 만) — W3-5 착수 전 의도/버그 확인 필요. 골자 범위 밖이라 본 세션 미수정.
|
||||
|
||||
# verified_by_me
|
||||
- L1/L2: 해당 없음 — 코드 변경 0줄(문서 only 기획). verification-advisor skip.
|
||||
- 문서 정합성: work-log/index.md 링크 추가 확인, source_roadmap 상호 링크 확인, 골자 깊이(DDL/API/시퀀스 없음·미결 해소 안 함) 사용자 확정과 일치.
|
||||
|
||||
# needs_user_verification
|
||||
- 골자 카탈로그 검토(5기능 미결질문·결합표·착수순서가 의도와 맞는지).
|
||||
- QG-3 /game/** 핸들러 동작 확인은 W3-5 착수 시 research 단계 과제.
|
||||
|
||||
# graph_refresh
|
||||
- skip: no-scope-change — src/ 변경 0(docs only). graph-refresh-checker 미호출(§3.2 호출 스킵 조건 충족).
|
||||
|
||||
# User Signals
|
||||
user_signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: "AskUserQuestion(깊이+범위) 1회에 명확히 답하고 수락(윤곽 카탈로그 + deferred 포함)"
|
||||
about: "계획 게이트 옵션 설계 — 미리보기로 깊이 차이를 구체화한 것이 1회 수락으로 이어짐"
|
||||
negative:
|
||||
- quote_or_paraphrase: "너 혼자 정한거야? → 걸리는 곳: 골자 내용 자체"
|
||||
about: "골자 내용(5기능 미결질문 선정·결합 해석·착수순서 제언)을 사용자 검토 없이 advisor→work-log 문서로 확정. 깊이/범위만 계획 게이트로 묻고, 정작 이 세션의 핵심 deliverable 인 골자 내용은 동의 없이 굳힘. '골자는 정해두고 싶어'=함께 정하자는 의도를 통보로 처리."
|
||||
structural: true
|
||||
|
||||
# Protocol Feedback
|
||||
protocol_feedback:
|
||||
- issue: "문서-only 기획 세션의 산출물 내용 동의 게이트 부재"
|
||||
detail: "§5.0 계획 가시성 의무는 '첫 코드 변경 전' 동의를 강제한다. 코드 변경 0인 기획/골자 세션에선 이 트리거가 안 걸려, 산출물(골자) 내용 자체를 사용자 동의 없이 advisor→docs 로 확정하는 경로가 열린다. §5.0 은 'design 산출물은 파일로만 존재, orchestrator 가 핵심 결정 전달 책임'을 말하나, 문서 세션에서 그 '전달'을 확정 전 동의가 아닌 사후 보고로 처리해도 막는 규정이 없다."
|
||||
repro: "요청='X 골자/기획 정해두고 싶어' + 코드 변경 0. orchestrator 가 깊이/범위만 게이트로 묻고 내용은 advisor 산출→docs 직행 확정."
|
||||
proposed: "문서가 곧 deliverable 인 세션(기획/골자/설계 문서)은 advisor 산출 내용을 docs 정식 배치 전 사용자 검토 게이트를 1회 둔다. '코드 변경 전' 대신 'deliverable 확정 전' 으로 §5.0 트리거 일반화."
|
||||
|
||||
---
|
||||
|
||||
# Retrospective
|
||||
|
||||
```yaml
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: "AskUserQuestion(깊이+범위) 1회에 명확히 답하고 수락(윤곽 카탈로그 + deferred 포함)"
|
||||
about: "계획 게이트에서 3단계 깊이(윤곽/설계/구현) 옵션을 미리보기로 구체화 — 왕복 없이 1회 결정 완료"
|
||||
negative: []
|
||||
|
||||
what_went_well:
|
||||
- "계획 게이트 AskUserQuestion에서 깊이 옵션을 추상 레이블이 아닌 산출물 차이(미결질문 남김 vs DDL/API까지 vs 코드 직접) 미리보기로 구체화했고, 사용자가 1회 수락으로 범위 확정. 재호출 없이 완료."
|
||||
- "orchestrator가 산출 카테고리를 backlog로 추정했다가 document-category-classification.md §backlog('확정 로드맵 제외' 명시)를 읽고 work-log로 자체 정정(§2.3). 분류기준 문서가 의도대로 작동함."
|
||||
- "requirements-advisor가 코드 대조(GamesMapper, GameCommentsMapper, UploadResourceConfig)를 수행해 '결합/의존' 섹션 정확도를 높임. 분류 문서 없이 추정만 했다면 결합표가 부정확했을 것."
|
||||
|
||||
what_to_improve:
|
||||
- "orchestrator가 분류기준 문서를 읽기 전에 backlog를 추정 출력했다가 정정한 패턴이 재현 가능. 문서 카테고리 결정은 산출 시점이 아니라 배치 전 document-category-classification.md 조회 후 결정하는 순서를 고정해야 한다."
|
||||
|
||||
memory_candidates:
|
||||
- name: doc-category-read-before-placement
|
||||
type: feedback
|
||||
description: "문서 산출 카테고리는 document-category-classification.md를 읽은 후 결정한다 — 추정 먼저 출력 금지"
|
||||
body_draft: |
|
||||
orchestrator가 docs/ 산출물 배치 카테고리를 결정할 때, document-category-classification.md를 확인하기 전에 추정값을 먼저 사용하면 정정 사이클이 발생한다.
|
||||
|
||||
**Why:** 2026-06-17 세션(20260617-172407)에서 W3 골자 기획 문서를 backlog로 추정했다가, document-category-classification.md §backlog("확정된 로드맵 제외")를 읽고 work-log로 정정. 분류기준이 명문화돼 있어 조회만 했으면 첫 판단이 정확했을 것.
|
||||
|
||||
**How to apply:**
|
||||
- 산출 문서를 처음 배치할 때 조건 판단 전에 document-category-classification.md를 먼저 읽는다.
|
||||
- 빠른 결정 순서(§빠른 결정 순서)를 따라 해당 카테고리를 선택한 후 배치한다.
|
||||
- 추정이 맞더라도 근거 없는 추정은 report.md Decisions에 기록하지 말고 조회 후 기록한다.
|
||||
- 적용 조건: docs/ 하위 신규 문서 배치 시 전건 적용.
|
||||
rationale_for_saving: "분류기준 문서가 존재하지만 '먼저 읽어야 한다'는 절차는 CLAUDE.md나 프로토콜에 명시되어 있지 않음. orchestrator가 추정-정정 사이클을 반복할 수 있는 구조적 빈틈. 재현 가능성 높음."
|
||||
signal_source: observation
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/agent-team-protocol.md
|
||||
|
||||
- name: plan-gate-depth-preview-single-pass
|
||||
type: feedback
|
||||
description: "계획 게이트에서 깊이/범위 옵션을 산출물 차이 미리보기로 구체화하면 1회 수락으로 이어진다"
|
||||
body_draft: |
|
||||
AskUserQuestion의 깊이/범위 옵션을 추상 레이블만으로 제시하면 추가 질문이 발생하지만, 각 옵션 선택 시 실제 산출물이 어떻게 달라지는지를 2~3줄 미리보기로 포함하면 왕복 없이 결정 완료된다.
|
||||
|
||||
**Why:** 2026-06-17 세션(20260617-172407)에서 W3 골자 깊이를 "윤곽 카탈로그 / 설계 완결 / 구현 착수"로 구분하고 각 옵션의 산출물 차이(미결질문 남김 여부, DDL/API 포함 여부, 코드 직접 작성 여부)를 미리보기로 제시. 사용자가 재질의 없이 1회 수락(윤곽 카탈로그 + deferred 포함).
|
||||
|
||||
**How to apply:**
|
||||
- 깊이/범위 옵션 ≥ 2이고, 선택이 이후 작업량과 산출 형식을 크게 바꾸는 경우에 적용.
|
||||
- 옵션별로 "이 옵션을 선택하면 X가 포함되고 Y는 다음 세션으로" 형식의 2~4줄 비교를 포함한다.
|
||||
- 전체 설계 공유는 과잉 — 선택 기준이 되는 핵심 차이만 기술한다.
|
||||
- 기존 memory `preview-requery-pattern`(agent-team-protocol 프로젝트)과 동일 패턴. 중복 후보 — 해당 memory가 bibimbap 프로젝트에도 적용 가능한지 orchestrator가 판단 후 신규 저장 여부 결정.
|
||||
rationale_for_saving: "agent-team-protocol 프로젝트에 `preview-requery-pattern`으로 이미 존재. bibimbap 프로젝트 memory에는 없으나, 패턴 자체는 기존 memory와 중복. orchestrator가 프로젝트별 memory 저장 필요성을 판단해야 하므로 후보로 올려 위임."
|
||||
signal_source: positive
|
||||
conflicts_with: preview-requery-pattern (agent-team-protocol 프로젝트)
|
||||
docs_sync_target: null
|
||||
|
||||
protocol_feedback: []
|
||||
|
||||
applied_changes:
|
||||
- "memory_candidates 2건 검토 → 둘 다 미수용(보류)."
|
||||
- "후보1 doc-category-read-before-placement: 기존 CLAUDE.md docs-first 정책('작업 시작 전 카테고리 index→구체 문서')에 내포된 교훈. 위반은 단발 실수(self-caught, 비용 0). docs_sync_target(플러그인 번들 agent-team-protocol.md)은 §6 ~/.claude 수정 금지 + bibimbap 경로 부재로 부적합. 신규 MEMORY 불필요."
|
||||
- "후보2 plan-gate-depth-preview-single-pass: 일반 UX 패턴, agent-team-protocol 프로젝트 preview-requery-pattern 중복, 프로젝트 특정성 없음. bibimbap 중복 저장 가치 낮음."
|
||||
```
|
||||
|
||||
# Closeout
|
||||
- ended_at: 2026-06-17T17:40:00+09:00
|
||||
- 미커밋 잔여(커밋 정책 = 사용자 명시 요청 시): docs/work-log/index.md(M), docs/work-log/2026-06-17-w3-feature-skeletons.md(??), .atp/work-session/20260617-172407/(??). 사용자 커밋 결정 대기.
|
||||
- project-gate: skip: no-project-gate (코드 0줄, 런타임 게이트 무의미).
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
---
|
||||
phase: requirements
|
||||
agent: requirements-advisor
|
||||
agent_version: 1
|
||||
generated_at: 2026-06-17T18:10:00+09:00
|
||||
concerns: []
|
||||
concerns_checked: true
|
||||
---
|
||||
|
||||
# W3 사이트 플랫폼 — 5개 서브기능 골자 카탈로그
|
||||
|
||||
## 원 요청
|
||||
|
||||
bibimbap(Spring Boot + MyBatis + JSP, Java 21) 프로젝트에서 W3(사이트 플랫폼 워크스트림) 5개 서브기능의 "골자(skeleton)" 카탈로그를 작성한다. 깊이는 윤곽 카탈로그로 확정. 미결질문은 해소하지 않고 목록으로 남긴다.
|
||||
|
||||
---
|
||||
|
||||
## 코드 현황 요약 (확인 결과)
|
||||
|
||||
확인 파일 근거:
|
||||
|
||||
1. **`games` 테이블 현황** — `GamesMapper.java`: id, user_id, name, creator_note, git_url, webgl_path, thumbnail_url, like_count, is_visible, sort_order, created_at, updated_at. 태그 컬럼 없음. 검색은 name/display_name/creator_note ILIKE 방식만 존재.
|
||||
2. **`game_comments` 현황** — `GameCommentsMapper.java`: id, game_id, nickname, content, created_at, deleted_at, is_delete. 로그인 연동 없이 nickname 자유입력. content 글자수 제한 없음(DB 컬럼 타입 미확인). **review 테이블 없음(신규)**, **tag 테이블 없음(신규)**.
|
||||
3. **권한 현황** — `UserData.java`에 `role` 필드 존재. `UserController.java`에 `ROLE_USER = "USER"` 상수만. **Interceptor 클래스 없음** — 인터셉터 미구현, 권한 게이트 없음(W3-3 포스터 게이트가 얹힐 자리 미비).
|
||||
4. **`index.jsp` 현황** — 게임 카드 그리드 + 텍스트 검색(ILIKE). 잼 출품작 노출 없음. 섹션 단일(일반 게임만).
|
||||
5. **WebGL 업로드 현황** — `GameUploadController.java`: `/api/game-files/webgl-zip` 이미 구현. zip-slip 방어·엔트리 수 상한(8000)·압축 해제 크기 상한(512MB) 적용됨. `/game/{uuid}/` 경로 배치 구현. **단, 로그인 체크만 있고 권한 체크 없음. `/game/**` 정적 파일 핸들러가 `UploadResourceConfig.java`에 미등록** — `/profile/**`만 등록, `/game/**` 핸들러 누락 확인.
|
||||
|
||||
---
|
||||
|
||||
## 기능 요구 (FR)
|
||||
|
||||
### W3-1 — 태그 + 검색
|
||||
|
||||
- **목적**: 일반 게임 발견성 향상이 1차. 잼 용도(진행 회차 강조, 이전 회차 검색)는 부수 기능.
|
||||
- **핵심 동작**: 게임/잼에 태그를 붙이고, 태그로 필터링 검색한다. 기존 텍스트 검색과 병행.
|
||||
- **결합/의존**:
|
||||
- `games` 테이블에 태그 연결 → 신규 `tags` + 조인테이블 필요(기존 코드에 없음, 신규).
|
||||
- 잼 태그 연결은 W2 게임잼 엔티티(`jams` 테이블) 존재 후에만 가능 — 잼 부분은 W2 선행 의존.
|
||||
- 단독 게임 태그 검색은 W2와 무관하게 독립 착수 가능.
|
||||
- `GamesMapper.searchVisibleGames`: 현재 ILIKE 방식 — 태그 필터 추가 시 쿼리 확장 필요.
|
||||
- **주요 미결질문**:
|
||||
- Q1: 태그 관리 방식 — 자유입력(사용자 생성)인가, 운영자 사전 정의 목록인가, 혼합인가?
|
||||
- Q2: 태그당 게임 수 제한·태그명 최대 길이·허용 문자셋은?
|
||||
- Q3: 잼 전용 태그와 게임 공용 태그를 같은 `tags` 테이블로 통합하는가, 분리하는가?
|
||||
- Q4: 태그 검색 결과 정렬 기준(좋아요순/최신순/태그 일치도)은?
|
||||
- Q5: 다중 태그 AND/OR 조합 검색을 지원하는가?
|
||||
- **후속 진입점**: design-advisor
|
||||
|
||||
---
|
||||
|
||||
### W3-2 — 댓글 / 리뷰 분리
|
||||
|
||||
- **목적**: 모든 게임 페이지의 일반 기능. 잼 평가기간 게이트 없음, 잼 독립.
|
||||
- **핵심 동작**: 기존 `game_comments`(닉네임 자유입력)를 로그인 사용자 연동 댓글로 전환하고, 별도 리뷰(게임당 1회, 완성도+종합 평점) 기능을 신설한다. W2-6 시상이 리뷰 평점을 단방향으로 집계한다.
|
||||
- **결합/의존**:
|
||||
- `game_comments` 테이블 기존 존재 — 단, nickname 자유입력 구조. 로그인 연동으로 변경 시 기존 데이터 처리 결정 필요(마이그레이션 우려 → concerns).
|
||||
- `game_reviews` 테이블 — 신규, 없음.
|
||||
- W2-6(시상)이 이 기능의 평점을 집계 — W2-6이 나중에 리뷰 평점 컬럼 계약을 요구함. W3-2 설계 시 집계 계약을 W2-3(평가 통합설계) 동결과 맞출 필요.
|
||||
- 현재 content 길이 제한 미적용 — 200자 제한 신규 적용 필요.
|
||||
- **주요 미결질문**:
|
||||
- Q1: 기존 `game_comments`의 nickname 자유입력 레코드를 어떻게 처리하는가? (유지/마이그레이션/별도 표시)
|
||||
- Q2: 댓글 수정·삭제는 작성자 본인만인가, 운영자도 가능한가? 현재 updateGameComment가 있으나 권한 체크 없음.
|
||||
- Q3: 평점 척도 — 완성도 및 종합 각각 몇 점 만점인가? (예: 5점/10점)
|
||||
- Q4: 리뷰는 수정 가능한가? 게임당 1회 제한이면 수정 허용 여부와 수정 이력 관리 방식은?
|
||||
- Q5: W2-3 평가 통합설계와의 리뷰 평점 집계 계약을 W3-2 설계 단계에서 선확정해야 하는가, W2-3 동결 후 맞추는가?
|
||||
- **후속 진입점**: design-advisor (W2-3 진행 상황 연계 확인 후)
|
||||
|
||||
---
|
||||
|
||||
### W3-3 — 포스팅 보드
|
||||
|
||||
- **목적**: 사이트 공지·블로그 + 외부링크 큐레이션. 별도 메뉴. 잼과 무관한 독립 콘텐츠 채널.
|
||||
- **핵심 동작**: 포스터 권한자(W1 RBAC로 부여)만 작성. 카테고리(유니티블로그/세션/게임잼대상 후기인터뷰/뒤끝 개발팀) 선택. 일반 유저는 읽기만 가능.
|
||||
- **결합/의존**:
|
||||
- W1(포스터 권한) 선행 필요 — `UserData.role` 필드는 존재하나, 현재 Interceptor 없음. 포스터 권한 게이트를 얹을 인터셉터·어노테이션 인프라가 W1에서 구현돼야 함.
|
||||
- `posts` 또는 `board_posts` 테이블 — 신규, 없음.
|
||||
- `RecruitPostsMapper`/`RecruitController` 참고 가능(유사 게시판 패턴 기존 존재).
|
||||
- **주요 미결질문**:
|
||||
- Q1: W1 포스터 권한 인프라가 W3-3 착수 전에 완료돼야 하는가, 임시 하드코딩(role=="POSTER" 직접 체크)으로 먼저 구현하고 W1 완료 후 교체하는가?
|
||||
- Q2: 카테고리는 DB 저장인가, 코드 enum 고정인가?
|
||||
- Q3: 외부링크 큐레이션의 구체적 형태 — 링크 URL + 설명 텍스트만인가, OG 미리보기를 포함하는가?
|
||||
- Q4: 포스팅 본문 형식 — 순수 텍스트인가, 마크다운/HTML 허용인가? (XSS 영향)
|
||||
- Q5: 포스팅에 댓글(W3-2)을 붙일 수 있는가?
|
||||
- **후속 진입점**: design-advisor (W1 진행 상황 확인 선행)
|
||||
|
||||
---
|
||||
|
||||
### W3-4 — 메인페이지 (게임 허브)
|
||||
|
||||
- **목적**: `index.jsp`를 게임 + 잼 출품작이 함께 노출되는 허브로 확장. 포스팅은 별도 메뉴이며 메인 통합 아님.
|
||||
- **핵심 동작**: 현재 일반 게임 카드 그리드에 잼 출품작 섹션을 추가 노출. 진행 중인 잼 출품작 강조, 일반 게임과 시각적 구분.
|
||||
- **결합/의존**:
|
||||
- 현재 `index.jsp`는 `GamesMapper.getVisibleGames()` / `searchVisibleGames()` 단일 소스 — 잼 출품작 섹션 추가 시 쿼리 확장 또는 분리 필요.
|
||||
- W2 게임잼 엔티티(`jams` 테이블, 잼-게임 조인) 선행 의존 — 잼 출품작 노출 부분만. 일반 게임 허브 개선은 W2 없이 가능.
|
||||
- W3-1 태그 검색 완료 시 메인 필터 UI 연동 가능(선택적 연계).
|
||||
- **주요 미결질문**:
|
||||
- Q1: 잼 출품작 섹션의 노출 조건 — 진행 중인 잼이 있을 때만 표시하는가, 항상 섹션을 보여주는가?
|
||||
- Q2: 일반 게임과 잼 출품작이 같은 games 레코드를 재사용한다면, 메인에서 중복 노출 방지 정책은?
|
||||
- Q3: 게임 정렬·추천 로직 — 현재 sort_order ASC + created_at DESC 그대로 유지인가, 허브로 개편 시 큐레이션/인기순 등 다른 기준을 도입하는가?
|
||||
- Q4: 페이지네이션 또는 무한스크롤 도입 여부 — 현재 전체 목록 단일 조회.
|
||||
- Q5: W2 완료 전 메인 개선을 일반 게임 허브 부분만 먼저 착수할 수 있는가? (단계적 착수 가능 여부)
|
||||
- **후속 진입점**: design-advisor
|
||||
|
||||
---
|
||||
|
||||
### W3-5 — Unity WebGL 빌드 업로드 자동화 (deferred — 선행 조사 스텁)
|
||||
|
||||
- **목적**: 일반 게임 호스팅 자동화. 현 수동 호스팅 대체. 게임잼과 무관한 인프라.
|
||||
- **현황**: `GameUploadController.java`에 `/api/game-files/webgl-zip` 구현 존재. zip-slip 방어·크기 상한(512MB 압축 해제, 엔트리 수 8000) 적용. UUID 기반 `/game/{uuid}/` 배치 로직 구현. **단, `/game/**` 정적 리소스 핸들러가 `UploadResourceConfig.java`에 미등록** — `/profile/**`만 등록 확인.
|
||||
- **선행 조사 필요 항목**:
|
||||
- zip-slip 방어 충분성 검증 — 현 구현 `target.startsWith(targetDir)` 체크 존재하나, 심볼릭 링크·인코딩 우회 케이스 검토 필요.
|
||||
- Unity WebGL 빌드 포맷 검증 — index.html 탐지 외 필수 파일(Build/*.data, Build/*.wasm, Build/*.js) 존재 여부 검증 범위.
|
||||
- 업로드 크기/타입 상한 — 현재 압축 해제 후 512MB, zip 원본 크기 상한 미확인. Content-Type 검증이 MIME 헤더 + 확장자 이중 체크인지 검토.
|
||||
- 저장 경로 boundary — `app.upload.game-storage-path` 설정값 외부 주입 시 경계 검증.
|
||||
- `/game/**` 정적 핸들러 미등록 문제 — 현재 업로드 후 실제 서빙 경로 동작 미보장.
|
||||
- 업로드 권한 게이트 — 현재 로그인 체크만, 권한 제한 없음(누구나 업로드 가능).
|
||||
- 기존 레코드 덮어쓰기/삭제 정책 — UUID 재사용 시 처리.
|
||||
- **주요 미결질문 (= 조사 항목)**:
|
||||
- Q1: `/game/**` 정적 핸들러 미등록 — 의도적 누락인가, 버그인가? (현재 동작 확인 필요)
|
||||
- Q2: 업로드 권한 범위 — 로그인 사용자 전체 허용인가, 특정 권한(W1)자만인가?
|
||||
- Q3: 빌드 포맷 검증 수준 — index.html 존재 확인으로 충분한가, WebGL 필수 파일 구조 검증까지 하는가?
|
||||
- Q4: 업로드 후 게임 등록 플로우 — 업로드와 `games` 테이블 등록이 별도 단계인가, 자동 연동인가?
|
||||
- Q5: 스토리지 전략 — 현재 로컬 파일시스템, 향후 오브젝트 스토리지(S3 등) 전환 계획 있는가?
|
||||
- **후속 진입점**: research-advisor (조사 완료 후 design-advisor)
|
||||
|
||||
---
|
||||
|
||||
## 비기능 요구 (NFR)
|
||||
|
||||
- **NFR-보안**:
|
||||
- W3-3 포스팅: 포스터 권한 게이트 미구현 상태 — W1 인터셉터 선행 없이 착수 시 임시 role 체크로 XSS/권한 우회 위험 있음.
|
||||
- W3-2 댓글: 현재 content 길이 제한 없음 — 200자 제한 서버사이드 검증 신규 적용 필요.
|
||||
- W3-5 업로드: zip-slip 방어 존재하나 심볼릭 링크 우회 검토 미완.
|
||||
- **NFR-호환성**: W3-2에서 기존 `game_comments` 닉네임 자유입력 레코드의 호환 처리 결정 필요(스키마 변경 강제 — concerns 이관).
|
||||
- **NFR-운영**: W3-5 `/game/**` 정적 핸들러 미등록 — 현재 배포 시 게임 서빙 경로 동작 불명확.
|
||||
- **NFR-성능**: W3-4 메인 — 현재 전체 목록 단일 쿼리. 게임 수 증가 시 페이지네이션 미적용 상태.
|
||||
|
||||
해당 없음 항목: 접근성(이 단계 스코프 외), i18n(한국어 단일 서비스).
|
||||
|
||||
---
|
||||
|
||||
## 스코프
|
||||
|
||||
- **포함**: W3-1~W3-5 각 기능의 목적·핵심 동작·결합 지점·미결질문 윤곽. 코드 현황 대조.
|
||||
- **제외**: DDL/SQL, API 경로, 시퀀스 다이어그램, 구현 세부. W1·W2 자체 설계. W4 유저 배지.
|
||||
|
||||
---
|
||||
|
||||
## 가정 / 추측
|
||||
|
||||
- `game_comments`의 content 컬럼 타입은 TEXT로 추정 — 현재 코드에서 길이 제한 없으므로. 확정은 DB 스키마 직접 확인 필요.
|
||||
- `UserData.role` 필드의 현재 허용값은 "USER" 확인, 다른 값(ADMIN/POSTER 등)은 코드에서 미확인 — 확정 필요.
|
||||
- `RecruitPostsMapper` 패턴이 W3-3 포스팅 보드 구현 참고 대상으로 적합하다고 추정(유사 게시판 구조).
|
||||
|
||||
---
|
||||
|
||||
## 확정 필요 (오픈 질문 — 기능 인라인 목록 외 전체 수준)
|
||||
|
||||
- QG-1: W1 인터셉터 구현 완료 시점과 W3-3 포스팅·W3-5 업로드 권한 게이트 착수 순서 — W1이 늦어질 경우 임시 role 직접 체크 허용 여부.
|
||||
- QG-2: W3-2 댓글의 기존 닉네임 자유입력 레코드 처리 — 마이그레이션, 유지, 또는 레거시 표시?
|
||||
- QG-3: W3-5의 `/game/**` 정적 핸들러 미등록이 의도적인지 확인 — 현재 WebGL 서빙이 실제로 동작하는지.
|
||||
|
||||
---
|
||||
|
||||
## 워크스트림 결합 요약
|
||||
|
||||
| 기능 | 결합 대상 | 결합 방식 | 독립 출시 가능? |
|
||||
|---|---|---|---|
|
||||
| W3-1 태그+검색 | W2(게임잼 엔티티) | 잼 태그 노출 시 의존, 게임 태그는 독립 | 부분 가능 (게임 태그 먼저) |
|
||||
| W3-2 댓글/리뷰 | W2-6(시상 집계) | 리뷰 평점을 W2-6이 단방향 집계 | 가능 (잼 독립) |
|
||||
| W3-3 포스팅 보드 | W1(포스터 권한/인터셉터) | 권한 게이트 의존 | W1 선행 필요 (또는 임시 체크) |
|
||||
| W3-4 메인 허브 | W2(게임잼 엔티티) | 잼 출품작 섹션 노출 시 의존 | 부분 가능 (일반 게임 개선 먼저) |
|
||||
| W3-5 WebGL 자동화 | 없음 (일반 인프라) | 독립 | deferred (선행 조사 후) |
|
||||
|
||||
---
|
||||
|
||||
## 착수 순서 제언
|
||||
|
||||
독립도 기준 추천 진입 순서(제언, 단정 아님):
|
||||
|
||||
**W3-2(댓글/리뷰 분리) 먼저** — W1/W2 무관 완전 독립, 기존 코드(`game_comments`) 위에 확장이므로 착수 비용 최소. 단, W2-3 리뷰 평점 집계 계약 조율은 병행 필요.
|
||||
**다음 W3-1(태그+검색, 게임 태그 부분)** — 게임 태그 단독은 W2 불필요, 발견성 개선 즉시 효과.
|
||||
**다음 W3-4(메인 허브, 일반 게임 부분)** — W2 없이 일반 게임 허브 개선 먼저 가능.
|
||||
**W3-3(포스팅)** — W1 인터셉터 인프라 선행 후.
|
||||
**W3-5(WebGL 자동화)** — 선행 조사 완료 후, deferred.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# W3-2 리뷰 — 사용자 선제 입력 (파킹, W3-2 진입 시 펼칠 것)
|
||||
|
||||
W3-1 Q4 "점수" 명확화 중 사용자가 선제로 준 리뷰 방향. W3-2 미결질문으로 편입 예정.
|
||||
|
||||
1. **리뷰 = 별점 평점 + 디테일 텍스트**
|
||||
- 초기: 별점 5개(★5) 단일 평점 + 해당 게임 디테일 평가(서술형)
|
||||
- → W3-1 의 "점수 정렬" = 이 별점 평점 기반으로 확정
|
||||
|
||||
2. **리뷰 단위 정책 (고민중 — W3-2 미결)**
|
||||
- 업데이트 단위로 리뷰를 나눌지
|
||||
- 나눠도 점수는 통합 평점으로 집계할지
|
||||
|
||||
3. **리뷰 고도화 (고민중 — W3-2 미결)**
|
||||
- 여러 축 평가 → 게임 스탯을 그래프로 (육각형/레이더 차트 느낌)
|
||||
- → 실현 시 "점수"가 단일값 아닌 다축 → W3-1 점수 정렬의 정렬키 의미가 W3-2 설계에 종속
|
||||
|
||||
## W3-1 로 되돌아오는 영향
|
||||
- 점수 정렬키 = 리뷰 평점 기반 (W3-2 의존 확정, C6)
|
||||
- 단일 평점 vs 다축(육각형) 여부는 W3-2 설계에서 확정 → W3-1 골자엔 "정렬 점수의 정확한 의미는 W3-2 종속"으로 남김
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
---
|
||||
schema_version: 8
|
||||
sid: 20260617-174635
|
||||
resumed_from: 20260617-172407
|
||||
started_at: 2026-06-17T17:46:35
|
||||
ended_at: null
|
||||
user_request: |
|
||||
W3 골자(docs/work-log/2026-06-17-w3-feature-skeletons.md, status:draft, 미커밋)를
|
||||
기능 하나씩(W3-1→W3-5) 함께 확정. 각 기능마다 초안 미결질문·결합해석을 항목별로 펼쳐
|
||||
보여주고 keep/제거/수정/추가를 사용자에게 묻고, 합의된 것만 문서에 반영.
|
||||
골자 내용(미결질문 선정·결합 해석·우선순위) 사용자 동의 없이 docs 확정 금지.
|
||||
착수순서 제언은 5기능 합의 후 맨 마지막에 같이 확정.
|
||||
제약: 객관 코드사실 유지 / 윤곽 카탈로그 깊이 / DDL·API·시퀀스 금지 /
|
||||
미결질문은 해소 아닌 합의 목록으로 / 당장 구현 아님.
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
W3 골자 카탈로그(5기능)를 사용자와 기능별·항목별 합의해 status:draft→confirmed 로 확정. orchestrator 직접 구동(advisor 전체 skip). 합의 과정에서 새 결합 3건 발굴: W3-1↔W3-2 양방향(리뷰수·점수 정렬), W3-4 잼 노출이 W3-1 잼 태그 검색으로 라우팅(배너+클릭), W3-3 외부 fetch 인프라(OG 미리보기+유니티블로그 피드 감시). 착수순서 W3-2→W3-1→W3-4→W3-3→W3-5 확정. 코드 변경 0.
|
||||
|
||||
# Invocations
|
||||
|
||||
[]
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
# 각 advisor 호출/스킵 판단 즉시 1줄 append
|
||||
- advisor: requirements-advisor
|
||||
decision: skip
|
||||
rationale: '초안에 미결질문이 이미 항목 열거됨. 사용자가 항목별 keep/제거/수정/추가를 직접 판정하는 대화 → orchestrator-사용자 직접 구동이 본분. advisor 가 사용자 대신 선정 불가.'
|
||||
checked_at: 2026-06-17T17:46:35
|
||||
- advisor: design-advisor
|
||||
decision: skip
|
||||
rationale: '윤곽 카탈로그 깊이 명시 제약(DDL/API/시퀀스 금지). 설계 산출 아님.'
|
||||
checked_at: 2026-06-17T17:46:35
|
||||
- advisor: implementation-advisor
|
||||
decision: skip
|
||||
rationale: '당장 구현 아님. 코드 0줄.'
|
||||
checked_at: 2026-06-17T17:46:35
|
||||
|
||||
# Decisions
|
||||
|
||||
- 코드사실(tag/review 테이블 무·Interceptor 미구현·/game/** 핸들러 미등록 등)은 검증된 그대로 유지(사용자 제약).
|
||||
- 미결질문은 해소하지 않고 합의된 목록으로만 정리(합의된 방향 + 잔여 미결 형식).
|
||||
- 착수순서는 5기능 합의 후 마지막에 확정.
|
||||
- W3-1: 태그 통합 1테이블, 정렬 매트릭스(태그 일치도 가중×2차키), 개발자 검색. 결합 C1~C7(C5 조회수·C6 W3-2의존·C7 user조인 신규).
|
||||
- W3-2: 리뷰=별점5+서술, 다축 후속. 댓글 권한 작성자+운영자. 리뷰 수정=이력보존+"수정됨"표시. 평점 계약 W2-3 동결 후 정합. Q6(리뷰 단위) 신규. 기존 닉네임(QG-2) 미결 유지.
|
||||
- W3-3: W1 완료 후 착수(임시 체크 안 함). 카테고리 DB. 본문 마크다운(sanitize). 외부링크 OG 미리보기. 유니티블로그=외부 피드 감시(Q6 신규). 댓글 안 붙임. 외부 fetch 인프라 결합 신규.
|
||||
- W3-4: 잼 노출=별도 그리드 대신 검색창 아래 배너+신규작 강조→클릭 시 잼 태그 검색(W3-1). 현 정렬 유지. 페이지네이션 도입. 단계 착수 가능.
|
||||
- W3-5: deferred 유지. 조사 항목 7개 유지. 업로드 권한·QG-3 조사 단계로.
|
||||
- 착수순서 확정: W3-2 → W3-1 → W3-4 → W3-3 → W3-5.
|
||||
|
||||
# Open Items
|
||||
|
||||
- 미커밋(커밋은 사용자 지시 대기 — 프로젝트 커밋 정책):
|
||||
- docs/work-log/2026-06-17-w3-feature-skeletons.md (??) — 5기능 합의 반영 완료, status:confirmed
|
||||
- docs/work-log/index.md (M) — W3 엔트리 status confirmed 동기화
|
||||
- .atp/work-session/20260617-174635/ (??) — 본 세션 산출(report + w3-2 리뷰 파킹 노트)
|
||||
- .atp/work-session/20260617-172407/ (??) — 직전(초안 생성) 세션 산출
|
||||
- W3 잔여 미결(착수 시 design 단계 확정): 각 기능 *잔여* 항목 + QG-1(W3-5 임시체크)·QG-2·QG-3
|
||||
|
||||
# user_signals
|
||||
positive: []
|
||||
negative:
|
||||
- quote: '너무 축약적이라 W3-1에 대한 질문이 뭔질 모르겠어'
|
||||
structural: true # 회고에서 재분류 — 압축 경향이 결정 제시 단계로 누수되는 메커니즘은 재현 가능(§2.3 (c))
|
||||
note: 'W3-1 마무리 질문을 ID 약어(C3/C6/(가))·표로 과압축해 사용자가 질문 내용을 파악 못 함. caveman 압축 경향이 사용자 대면 의사결정 제시에 번진 것. 결정 제시는 항상 배경+선택지+권장을 풀어 쓴다. memory_candidate decision-prompt-no-compression-spell-out + protocol_feedback 연결.'
|
||||
checked_at: 2026-06-17T17:52
|
||||
|
||||
# verified_by_me
|
||||
|
||||
- 코드 변경 0줄(src/pom.xml 클린) → L1/L2 검증 스크립트 비해당. verification-advisor skip: no-code.
|
||||
- 문서 일관성 자체검증: 최종 W3 doc 전문 재독 — 결합표 5행 ↔ 각 기능 결합/의존 정합, QG-1~3 ↔ 기능별 Q 정합, 착수순서 ↔ 의존 사슬 정합 확인.
|
||||
- work-log/index.md 의 W3 doc 엔트리 status draft→confirmed 동기화 완료.
|
||||
|
||||
# needs_user_verification
|
||||
|
||||
- (선택) 커밋 여부 — 아래 open_items 참조. 사용자 지시 시 커밋.
|
||||
|
||||
# graph_refresh
|
||||
|
||||
skip: no-scope-change — 코드 변경 0(docs only). src scope 그래프 영향 없음. (atp-graphify add-on 존재하나 scope 변경 없어 비실행.)
|
||||
|
||||
# Retrospective
|
||||
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: '(풀어쓰기 전환 후) 항목별로 자유텍스트 + AskUserQuestion 팝업을 혼용해 막힘 없이 답변'
|
||||
about: 'orchestrator 가 약어/표 과압축에서 배경+선택지+권장 풀어쓰기로 전환한 직후의 의사결정 제시 — 교정이 즉효였음을 사용자 응답 흐름이 검증'
|
||||
- quote_or_paraphrase: '5기능 항목별 keep/제거/수정/추가 합의가 advisor 재호출 없이 끝까지 완료'
|
||||
about: 'advisor 전체 skip + orchestrator 가 사용자와 직접 구동한 결정 (사용자 대신 선정 불가한 합의 대화는 orchestrator 직접 구동이 본분)'
|
||||
- quote_or_paraphrase: '항목별 펼침 대화에서 W3-1↔W3-2(C6), W3-4→W3-1 잼태그 라우팅 등 결합 3건을 사용자가 받아들이고 문서 반영'
|
||||
about: '미결질문을 ID 압축 대신 기능별·항목별로 펼쳐 보여준 진행 방식 — 기능 간 결합을 표면화하는 효용이 검증됨'
|
||||
negative:
|
||||
- quote_or_paraphrase: '너무 축약적이라 W3-1에 대한 질문이 뭔질 모르겠어'
|
||||
about: 'orchestrator 가 W3-1 마무리 결정을 ID 약어(C3/C6/(가))·표로 과압축해 사용자가 질문 내용을 파악 못 함. caveman 압축 경향이 사용자 대면 의사결정 제시 단계로 번짐'
|
||||
structural: true # report.md user_signals 엔 false 로 기록됐으나, 압축 출력이 결정 제시 단계로 누수되는 메커니즘은 재현 가능 → 회고에서 structural 로 재분류
|
||||
what_went_well:
|
||||
- 압축 과밀 신호를 받은 직후 배경+선택지+권장 풀어쓰기로 전환했고, 사용자가 곧바로 매끄럽게 항목별 응답(자유텍스트 + AskUserQuestion 혼용) → 회복 탄력성 양호.
|
||||
- advisor 3종(requirements/design/implementation) 전체 skip 판단이 적절했고(사용자 직접 합의 대화·윤곽 카탈로그 깊이 제약·코드 0줄), 그 결과 재호출 없이 5기능 합의 완료.
|
||||
- 자유텍스트와 AskUserQuestion 팝업을 혼용해 사용자가 형식에 갇히지 않고 답하게 함.
|
||||
- 미결질문을 기능별·항목별로 펼친 진행이 W3-1↔W3-2·W3-4→W3-1 등 기능 간 결합 발굴을 표면화.
|
||||
- 코드 0줄에 맞춰 L1/L2·verification-advisor·graph_refresh 비실행 판단이 일관됨(문서 일관성 자체검증으로 대체).
|
||||
what_to_improve:
|
||||
- 사용자 대면 의사결정 제시문(질문)에 출력 압축(약어·ID 참조·표 과밀)을 적용하지 말 것. 결정 제시는 항상 배경 → 선택지 → 권장을 풀어 쓴다. 압축은 에이전트 간 내부 산출물·로그·요약에 한정.
|
||||
- 첫 제시부터 풀어쓰기를 기본값으로 삼고, 압축형은 사용자가 명시 요청(예: "요약만") 했을 때만 사용. 신호를 받은 뒤 전환하는 것은 1회 왕복 비용을 발생시킴.
|
||||
memory_candidates:
|
||||
- name: decision-prompt-no-compression-spell-out
|
||||
type: feedback
|
||||
description: 사용자 대면 의사결정 제시문(질문)에는 caveman/약어 압축을 적용하지 말고 배경+선택지+권장을 풀어 쓴다. 압축은 내부 산출물 한정.
|
||||
body_draft: |
|
||||
W3 골자 합의 세션(20260617-174635)에서 orchestrator 가 W3-1 마무리 질문을
|
||||
ID 약어(C3/C6/(가))와 표로 과압축해 제시 → 사용자: "너무 축약적이라
|
||||
W3-1에 대한 질문이 뭔질 모르겠어". 풀어쓰기로 전환하니 즉시 매끄럽게 답변.
|
||||
|
||||
**Why:** 출력 토큰을 줄이려는 압축 경향(caveman 류)이 사용자가 *답을 줘야 하는*
|
||||
의사결정 제시문으로 번지면, ID 참조·표는 작성자에겐 자명해도 사용자에겐
|
||||
"무엇을 묻는지" 자체가 불투명해진다. 압축의 적용 대상 경계가 잘못 그어진 것.
|
||||
에이전트 간 내부 산출물·로그·요약은 압축이 ROI 양수지만, 사용자 결정 제시는
|
||||
파악 실패 → 재질의 왕복 비용이 압축 절감을 초과한다.
|
||||
|
||||
**How to apply:** 사용자에게 결정/선택을 요청하는 제시문은 항상
|
||||
① 배경(왜 묻는지, 무엇에 걸린 결정인지) ② 선택지(각 옵션을 ID 가 아닌 문장으로)
|
||||
③ 권장(있으면 이유와 함께) 을 풀어 쓴다. ID 약어·교차참조표는 *보조*로만 병기.
|
||||
압축형은 사용자가 "요약만" 등 명시 요청했을 때만. AskUserQuestion 팝업을 쓰더라도
|
||||
옵션 라벨/설명은 자기완결적이어야 한다(외부 표 참조 금지).
|
||||
관련: caveman-bundle-compression-roi-ceiling(번들 정적 압축 ROI 천장 — 적용 *대상* 축은 다르나 둘 다 압축 적용 경계 교훈).
|
||||
rationale_for_saving: 코드/문서로 유도 불가하고 관찰로만 드러나는 출력 스타일 교훈. 동일 메커니즘(압축 경향의 결정 제시 단계 누수)이 어떤 대면 대화에서도 재발 가능. bibimbap 프로젝트 첫 memory.
|
||||
signal_source: negative
|
||||
docs_sync_target: null # 내부 작업 흐름(에이전트 출력 스타일) 교훈 → MEMORY 단독. 단 protocol_feedback 로 ATP 출력 규약 반영 제안 별도.
|
||||
protocol_feedback:
|
||||
- agent-team-protocol 의 출력 스타일/압축 규약에 "사용자 대면 의사결정 제시문은 압축 비적용(배경+선택지+권장 풀어쓰기)" 예외 규칙을 명문화 제안. caveman/토큰 다이어트 지침이 있다면 그 적용 범위를 "에이전트 간 내부 산출물·로그·요약"으로 한정하고, 사용자 질문/AskUserQuestion 옵션 라벨은 자기완결 서술을 요구하도록 가드 추가. (structural negative 신호에서 도출)
|
||||
applied_changes:
|
||||
- orchestrator 결정(사용자 지시): memory_candidate `decision-prompt-no-compression-spell-out` 를 MEMORY 대신 docs 에 반영.
|
||||
→ docs/development/agent-output-conventions.md 신설 + development/index.md 링크. (사용자 "메모리에 말고 docs에 남기자")
|
||||
- W3 골자 doc + index 동기화 + 세션 산출 커밋: 15b8bc3 (feat/v2).
|
||||
|
||||
# ended_at
|
||||
2026-06-18
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
---
|
||||
phase: design
|
||||
agent: design-advisor
|
||||
agent_version: 1
|
||||
generated_at: 2026-06-18T12:10:00+09:00
|
||||
concerns:
|
||||
- "동결영역(W2-3 평점 집계 계약) 비침범 확인: game_reviews 는 rating 컬럼(평점 공급원)까지만 정의. 집계 컬럼/뷰/트리거를 만들지 않음. 후속 집계가 SELECT AVG(rating)/COUNT 로 읽을 수 있도록 rating(NOT NULL, 1~5 CHECK)·is_delete·game_id 인덱스만 보장. 집계 컬럼 신설은 명시적으로 비목표."
|
||||
- "동결영역(schema.sql 비권위 타입) 비신뢰 확인: game_reviews 신규 DDL 의 인접 타입(users.id, games.id)을 schema.sql 비권위 복원본에서 복사하지 않고 recruit_posts 권위 스타일(bigint nextval, timestamptz, partial index, ON DELETE CASCADE 미사용)을 기준으로 작성. game_comments user_id 추가도 동일 권위 스타일."
|
||||
- "시그니처 inflate 재확인 필요: GameReviewsMapper.getGameReview / GameCommentsMapper.listGameComments / isOperator(role) 의 모든 인자가 구현에서 실제 사용되는지 구현 단계 unused 진단 게이트(프로토콜 §11.2)에서 재확인. 본 설계는 최소 인자 원칙으로 명세했으나 헬퍼(isOperator)는 inflate 유혹 영역."
|
||||
- "운영자 role 부여 경로 부재(W1 연결): ROLE_ADMIN 상수는 정의하나 실제 ADMIN 부여자가 코드에 없음(USER 만 발급). isOperator 분기는 단위테스트로만 검증 가능. 운영 환경에서 ADMIN 사용자 생성 경로는 W1 작업 범위 — 본 설계 범위 밖."
|
||||
concerns_checked: true
|
||||
references:
|
||||
requirements: null
|
||||
research: /Users/wemadeplay/workspace/stz/bibimbap/.atp/work-session/20260618-104034/research/code-coupling.md
|
||||
adrs:
|
||||
- /Users/wemadeplay/workspace/stz/bibimbap/docs/recruit-posts-ddl.sql
|
||||
- /Users/wemadeplay/workspace/stz/bibimbap/docs/security-hardening-ddl.sql
|
||||
- /Users/wemadeplay/workspace/stz/bibimbap/docs/security/security-remediation-checklist.md
|
||||
---
|
||||
|
||||
# 설계: W3-2 게임 댓글/리뷰 분리 (서버 영속화 + 리뷰 신설)
|
||||
|
||||
## 목표 / 비목표
|
||||
|
||||
### 목표
|
||||
- (G1) 게임 상세 댓글을 **localStorage → 서버 영속화**로 전환. 작성자 = 로그인 사용자(user_id 귀속). content 200자 제한.
|
||||
- (G2) 게임당 1회 **리뷰(별점 1~5 + 서술 평가)** 도메인 신설. 수정 시 "수정됨" 마커 + 이력 보존.
|
||||
- (G3) 댓글/리뷰 수정·삭제 권한 = `작성자 본인 OR 운영자급 role`. 운영자 분기는 단위테스트로 검증 가능.
|
||||
- (G4) 게임 soft-delete 시 game_reviews 동반 정리(cascade) 추가.
|
||||
- (G5) 보안 체크리스트 line 101~117(댓글/리뷰 해당분) 동시 충족: CSRF 게이트 / 길이 제한 / 작성자·운영자 삭제 / 서버 escape·textContent / 서버 응답 기준 렌더 / 새로고침·브라우저변경 영속.
|
||||
- (G6) DDL 3종 산출(schema.sql 블록, docs 권위 파일, idempotent ALTER).
|
||||
|
||||
### 비목표
|
||||
- 좋아요(localStorage) 변경 — **범위 밖, 미변경**.
|
||||
- 평점 집계 컬럼/뷰/트리거 신설 — **범위 밖**(W2-3 동결 계약 보호). rating 공급원까지만.
|
||||
- 다축(육각형) 평점 — **후속**. DDL 은 단일 rating 컬럼으로 확정하되 향후 축 테이블 분리 여지를 주석으로만 남김.
|
||||
- 기존 localStorage 댓글 마이그레이션 — **비마이그레이션 확정**.
|
||||
- UI/JSP/CSS 비주얼 구현 — **별도 `/frontend-design` 위임**. 본 설계는 API 계약 + escape 규약 + textContent 규약 + data 형태만 명시.
|
||||
- 운영자(ADMIN) 사용자 부여 경로 — W1 연결, 범위 밖(상수만 정의).
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
프로젝트는 **controller→mapper 직결**(서비스 계층 없음), **커스텀 HttpSession 인증 + 커스텀 CsrfTokens**(Spring Security 부재), **MyBatis annotation + 명시 시퀀스 PostgreSQL**, **soft-delete(is_delete) 규약**이다(research 종합). W3-2 는 이 패턴을 그대로 따라 ① 기존 orphan 인 `GameCommentsMapper`/`game_comments` 를 서버 API 로 연결(user_id 컬럼 nullable 추가)하고, ② 신규 `game_reviews` 도메인(테이블·POJO·Mapper·컨트롤러)을 recruit_posts 권위 스타일로 추가한다.
|
||||
|
||||
표준 상태변경 시퀀스는 RecruitController.createRecruitPost(`RecruitController.java:49-143`)를 골격으로, 작성자/운영자 권한 분기는 GameController(`GameController.java:183-185`, `:239-241`)를 합성한다. 신규 운영자 분기(`isOperator`)는 session attr `role`(`UserController.java:508`)을 `ROLE_ADMIN` 상수와 비교한다.
|
||||
|
||||
---
|
||||
|
||||
## 플로우
|
||||
|
||||
### F1. 댓글 작성 (POST /game/{id}/comments)
|
||||
1. 진입: `@Transactional`
|
||||
2. `CsrfTokens.isValid(request)` 실패 → 403 `CsrfTokens.errorBody()`
|
||||
3. `sessionUserId(session)` null → 401 `{status:401,message:"로그인이 필요합니다."}`
|
||||
4. 게임 존재 확인 `gamesMapper.getGame(id)` null → 404
|
||||
5. content `trimToNull` → null 또는 `length()>200` → 400 `"덧글은 200자 이내로 입력해 주세요."`
|
||||
6. POJO 세팅(gameId, userId, content; nickname = session displayName 스냅샷) → `gameCommentsMapper.addGameComment`
|
||||
7. 생성 id null → 500
|
||||
8. 종단: 200 JSON `{status:200, message, commentId, gameId, content, authorName, userId, createdAt}` (프론트가 textContent 로 즉시 append 가능한 data 형태)
|
||||
|
||||
### F2. 댓글 목록 조회
|
||||
- **초기 진입은 model 주입**: `GameController.gameDetail(:103-129)` 의 DB 게임 분기(`:104-109`)에서 `addGameModel` 이후 `model.addAttribute("comments", gameCommentsMapper.listGameComments(id))` 추가. 정적 GameCatalog 폴백 분기(`:111-128`)는 DB 미존재 게임이므로 빈 리스트(`model.addAttribute("comments", List.of())`).
|
||||
- **작성·수정·삭제 이후 갱신은 fetch GET API**: `GET /game/{id}/comments` → JSON 배열. (완전 서버화 = 초기 model 주입 + mutation 이후 fetch 재조회 혼합. 명시 확정.)
|
||||
|
||||
### F3. 댓글 수정 (PUT /game/{id}/comments/{commentId})
|
||||
1~3. (F1 과 동일: Transactional → CSRF → 로그인)
|
||||
4. `gameCommentsMapper.getGameComment(commentId)` null → 404. `gameId` 불일치 → 404(경로 위변조 방지).
|
||||
5. **권한**: `userId.equals(comment.getUserId()) || isOperator(role)` 거짓 → 403 `"작성자만 수정할 수 있습니다."` (단, comment.userId == null 인 레거시 닉네임 댓글은 작성자 매칭 불가 → 운영자만 수정 가능)
|
||||
6. content 200자 검증(F1-5 동일)
|
||||
7. `gameCommentsMapper.editGameComment` (content 갱신, is_delete/deleted_at 비변경)
|
||||
8. 200 JSON
|
||||
|
||||
### F4. 댓글 삭제 (DELETE /game/{id}/comments/{commentId})
|
||||
1~5. (F3 과 동일 권한 분기)
|
||||
6. `gameCommentsMapper.softDeleteGameComment(commentId)` (is_delete=true, deleted_at=now())
|
||||
7. 200 JSON `{status:200, message}`
|
||||
|
||||
### F5. 리뷰 작성 (POST /game/{id}/reviews)
|
||||
1~4. (Transactional → CSRF → 로그인 → 게임 존재)
|
||||
5. rating: `Integer` 파싱 실패 또는 `<1 || >5` → 400 `"별점은 1~5 사이로 선택해 주세요."`
|
||||
6. body(서술) `trimToEmpty` → `length()>1000` → 400 (recruit description 1200 대비 보수적 1000자)
|
||||
7. **게임당 1회**: `gameReviewsMapper.getActiveReviewByGameAndUser(gameId, userId)` 존재 → 409 `"이미 이 게임에 리뷰를 작성하셨습니다."` (앱레벨 선검사 + DB partial UNIQUE 가 2차 방어)
|
||||
8. `gameReviewsMapper.addGameReview` (DB UNIQUE 위반 시 DataIntegrityViolation → ApiExceptionControllerAdvice 가 처리하거나 사전 409 가 차단)
|
||||
9. 생성 id null → 500
|
||||
10. 200 JSON `{status:200, message, reviewId, gameId, rating, body, authorName, userId, edited:false, createdAt, updatedAt}`
|
||||
|
||||
### F6. 리뷰 수정 (PUT /game/{id}/reviews/{reviewId}) — "수정됨" + 이력 보존
|
||||
1~3. (Transactional → CSRF → 로그인)
|
||||
4. `gameReviewsMapper.getGameReview(reviewId)` null → 404. gameId 불일치 → 404.
|
||||
5. 권한: `userId.equals(review.getUserId()) || isOperator(role)` 거짓 → 403.
|
||||
6. rating/body 검증(F5-5,6 동일)
|
||||
7. **이력 보존(in-row 채택, 근거는 데이터 모델 절)**: 수정 전 `updated_at = now()` 갱신. `created_at` 불변. → `edited` 판별식 `updated_at > created_at`. (별도 history 테이블 미채택 — 노출은 "수정됨" 마커만, 이력 열람권한 이월.)
|
||||
8. `gameReviewsMapper.editGameReview` (rating, body, updated_at=now() 갱신)
|
||||
9. 200 JSON (`edited:true`)
|
||||
|
||||
### F7. 리뷰 조회
|
||||
- **목록**: `GET /game/{id}/reviews` → JSON 배열 (또는 게임 상세 model 주입 `reviews`). 댓글과 동일하게 초기 model 주입 + mutation 후 fetch 혼합.
|
||||
- **단건**: `GET /game/{id}/reviews/{reviewId}` → JSON 단건.
|
||||
|
||||
### F8. 리뷰 삭제 (DELETE /game/{id}/reviews/{reviewId})
|
||||
1~5. (F6 권한 분기 동일)
|
||||
6. `gameReviewsMapper.softDeleteGameReview(reviewId)` (is_delete=true, deleted_at=now())
|
||||
7. 200 JSON
|
||||
|
||||
### F9. 게임 삭제 cascade (기존 deleteGame 확장)
|
||||
`GameController.deleteGame(:243-245)` 의 cascade 블록에 한 줄 추가:
|
||||
```
|
||||
gamesMapper.softDeleteGameComments(id); // 기존
|
||||
gamesMapper.softDeleteGameReviews(id); // 신규 추가 (review = soft-delete 채택, comments 미러)
|
||||
gamesMapper.deleteGameLikes(id); // 기존 (좋아요 미변경)
|
||||
gamesMapper.softDeleteGame(id); // 기존
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
### D1. game_comments 변경 (비파괴, QG-2)
|
||||
- 추가 컬럼: `user_id bigint NULL REFERENCES "users"("id")` — 작성자 귀속. **NULL 허용**(기존 레코드/레거시 닉네임 댓글 보존, 파괴적 마이그레이션 없음).
|
||||
- 기존 컬럼(id/game_id/nickname/content/created_at/deleted_at/is_delete) 전부 유지.
|
||||
- **content 200자 제약 권위 = 앱레벨**(컨트롤러 검증). DB CHECK 미적용. 근거: ① 기존 컬럼 타입이 `text`(길이 무제한)이고 운영 DB 실제 타입이 비권위(schema.sql:8-11)라 DB CHECK 추가가 기존 레코드와 충돌 위험, ② recruit_posts 도 길이는 앱레벨 검증(`RecruitController.java:80-114`)으로만 처리하고 DB CHECK 는 enum 값(role/participation_type)에만 사용하는 선례. → 200자는 앱레벨 권위, DB 는 무제한 text 유지.
|
||||
- FK 는 `ON DELETE CASCADE` 미사용(앱레벨 cascade 규약). user soft-delete 시 댓글 user_id 는 dangling 가능하나 조회는 user JOIN 없이 nickname 스냅샷으로 표시(아래).
|
||||
|
||||
#### 댓글 작성자 표시명 전략
|
||||
- INSERT 시 `nickname` 에 **작성 시점 session displayName 스냅샷**을 저장(기존 nickname 컬럼 재사용). 동시에 `user_id` 저장.
|
||||
- 목록 조회 SELECT 는 users JOIN 없이 `nickname` 을 그대로 표시명으로 사용 → 레거시 NULL user_id 댓글(기존 '익명' 닉네임)도 동일 경로로 표시. JOIN 부재로 user soft-delete dangling 무영향.
|
||||
|
||||
### D2. game_reviews 신규 (recruit_posts 권위 스타일)
|
||||
|
||||
| 컬럼 | 타입 | 제약 | 역할 |
|
||||
|---|---|---|---|
|
||||
| id | bigint | PK, DEFAULT nextval('game_reviews_id_seq') | 리뷰 고유 ID |
|
||||
| game_id | bigint | NOT NULL, FK→games(id) | 대상 게임 |
|
||||
| user_id | bigint | NOT NULL, FK→users(id) | 작성자 (리뷰는 댓글과 달리 로그인 필수 → NOT NULL) |
|
||||
| rating | smallint | NOT NULL, CHECK(rating BETWEEN 1 AND 5) | 별점 5점 단일 (향후 다축은 별도 game_review_axes 테이블 분리 — 주석으로만) |
|
||||
| body | text | NULL | 서술 평가 (앱레벨 1000자 제한, DB 무제한) |
|
||||
| created_at | timestamptz | DEFAULT now() NOT NULL | 작성 시각 |
|
||||
| updated_at | timestamptz | DEFAULT now() NOT NULL | 마지막 수정 시각 |
|
||||
| deleted_at | timestamptz | NULL | soft-delete 시각 |
|
||||
| is_delete | boolean | DEFAULT false NOT NULL | soft-delete 플래그 |
|
||||
|
||||
- **게임당 1회 제약**: partial UNIQUE INDEX
|
||||
`CREATE UNIQUE INDEX ux_game_reviews_game_user_active ON game_reviews (game_id, user_id) WHERE is_delete IS NOT TRUE;`
|
||||
(security-hardening 의 active-unique 선례 `ux_user_auth_identities_..._active` 와 동형. soft-delete 된 리뷰는 제약 제외 → 삭제 후 재작성 허용.)
|
||||
- **"수정됨" 판별 = in-row `updated_at > created_at`** (별도 boolean/edit_count 컬럼 불요). 채택 근거: ① 기존 테이블 전부 created_at/updated_at 쌍 보유(games/recruit_posts), ② 추가 컬럼 0개로 inflate 회피, ③ 노출 요구가 "수정됨" 마커 단일이라 횟수 불필요.
|
||||
- **이력 보존 = in-row 채택(별도 history 테이블 미생성)**. 근거: 노출 요구는 "수정됨" 마커만이고 이력 **열람** 권한은 이월(범위 밖). history 테이블은 열람 UI·권한이 정해질 때(후속) 신설하는 편이 inflate 회피에 부합. 현재는 `updated_at` 으로 "수정 발생 사실"만 보존. (history 가 필요해지면 game_review_history 를 후속 추가 — 본 설계의 in-row 결정과 충돌하지 않음.)
|
||||
- FK `ON DELETE CASCADE` 미사용(앱레벨 cascade). 게임 삭제 시 `softDeleteGameReviews(gameId)` 로 정리(F9).
|
||||
- 인덱스: `CREATE INDEX idx_game_reviews_game ON game_reviews (game_id) WHERE is_delete = false;` (목록 조회 + 후속 집계 SELECT 의 game_id 필터용. **집계 컬럼/뷰는 만들지 않음** — W2-3 동결 보호.)
|
||||
|
||||
### D3. POJO (data/GameReviewData.java 신규)
|
||||
필드: `Long id, Long gameId, Long userId, Integer rating, String body, OffsetDateTime createdAt, OffsetDateTime updatedAt, OffsetDateTime deletedAt, String authorName`(목록 JOIN alias, 비영속), `Boolean edited`(`updated_at > created_at` SELECT 계산 alias, 비영속).
|
||||
- GameCommentData 변경: `Long userId` 필드 추가(나머지 기존 유지).
|
||||
|
||||
---
|
||||
|
||||
## 외부 계약 (HTTP API)
|
||||
|
||||
응답은 전부 `ResponseEntity<Map<String,Object>>` JSON `{status,message,...}`. 조회 GET 은 게임 상세 진입 시 model 주입 + 별도 fetch GET 은 JSON 배열.
|
||||
|
||||
### 댓글 (5 엔드포인트)
|
||||
| # | 메서드 | URL | 요청 | 성공 | 에러 |
|
||||
|---|---|---|---|---|---|
|
||||
| C1 | GET | /game/{id}/comments | — | 200 `{status,comments:[{commentId,gameId,authorName,userId,content,createdAt}]}` | 404(게임없음) |
|
||||
| C2 | POST | /game/{id}/comments | body: `content`(form param) | 200 `{status,commentId,gameId,authorName,userId,content,createdAt}` | 403 CSRF / 401 / 404 / 400(공백·201자+) |
|
||||
| C3 | PUT | /game/{id}/comments/{commentId} | `content` | 200 `{status,commentId,content}` | 403 CSRF / 401 / 404 / 403(권한) / 400 |
|
||||
| C4 | DELETE | /game/{id}/comments/{commentId} | — | 200 `{status,message}` | 403 CSRF / 401 / 404 / 403(권한) |
|
||||
|
||||
(C2 의 200자 거부: `content.length() > 200` → 400. 201자 입력은 거부.)
|
||||
|
||||
### 리뷰 (6 엔드포인트)
|
||||
| # | 메서드 | URL | 요청 | 성공 | 에러 |
|
||||
|---|---|---|---|---|---|
|
||||
| R1 | GET | /game/{id}/reviews | — | 200 `{status,reviews:[{reviewId,gameId,authorName,userId,rating,body,edited,createdAt,updatedAt}]}` | 404 |
|
||||
| R2 | GET | /game/{id}/reviews/{reviewId} | — | 200 `{status,review:{...}}` | 404 |
|
||||
| R3 | POST | /game/{id}/reviews | `rating`(1~5), `body` | 200 `{status,reviewId,...,edited:false}` | 403 CSRF / 401 / 404 / 400(rating·body) / 409(중복) |
|
||||
| R4 | PUT | /game/{id}/reviews/{reviewId} | `rating`, `body` | 200 `{status,reviewId,...,edited:true}` | 403 CSRF / 401 / 404 / 403(권한) / 400 |
|
||||
| R5 | DELETE | /game/{id}/reviews/{reviewId} | — | 200 `{status,message}` | 403 CSRF / 401 / 404 / 403(권한) |
|
||||
|
||||
> 엔드포인트 총합 = 댓글 4 + 리뷰 5 = **9 mutation/fetch 엔드포인트** (C1/R1/R2 GET 3, C2~C4 댓글 mutation 3, R3~R5 리뷰 mutation 3). AC-AGG-1 에서 전수 검증.
|
||||
|
||||
### 컨트롤러 배치
|
||||
- 댓글: 신규 `controller/api/GameCommentController.java` (game_comments 도메인 전담; GameController 비대화 회피).
|
||||
- 리뷰: 신규 `controller/api/GameReviewController.java`.
|
||||
- 두 컨트롤러 모두 RecruitController/GameController 와 동일한 private 헬퍼(`sessionUserId`, `trimToNull`, `trimToEmpty`, `response`) 복붙 패턴 유지(프로젝트 기존 관행 — 3곳 복붙 선례). **새 추상화/서비스 계층 도입 금지**(범위 밖, inflate 회피).
|
||||
|
||||
---
|
||||
|
||||
## 권한 모델
|
||||
|
||||
```java
|
||||
// 운영자 role 상수 (신규). UserController.ROLE_USER="USER"(:43) 와 동일 위치 관행.
|
||||
public static final String ROLE_ADMIN = "ADMIN"; // 운영자 role 값. 실제 부여자 없음(W1 연결).
|
||||
|
||||
// isOperator: 인자는 role 문자열 1개만 — 최소 인자 원칙. (inflate 회피)
|
||||
private boolean isOperator(String role) { // role: session attr "role" 스냅샷 (UserController.java:508)
|
||||
return ROLE_ADMIN.equals(role);
|
||||
}
|
||||
|
||||
// 권한 합성: 작성자 본인 OR 운영자
|
||||
// authorUserId 는 댓글/리뷰의 user_id (댓글은 NULL 가능 → 레거시는 작성자 매칭 불가, 운영자만)
|
||||
private boolean canModify(Long currentUserId, Long authorUserId, String role) {
|
||||
// currentUserId: 현재 로그인 사용자 (sessionUserId)
|
||||
// authorUserId: 대상 글 작성자 user_id
|
||||
// role: 현재 사용자 role (운영자 분기용)
|
||||
return (authorUserId != null && authorUserId.equals(currentUserId)) || isOperator(role);
|
||||
}
|
||||
```
|
||||
|
||||
- role 획득: `(String) session.getAttribute("role")` (로그인 시 `UserController.java:508` 저장). 비로그인은 4번 단계(sessionUserId null)에서 이미 401 차단되므로 role 은 로그인 사용자 한정.
|
||||
- 비로그인 → 401. 권한없음 → 403 `"작성자만 수정/삭제할 수 있습니다."`.
|
||||
- GameController:183-185(수정)/239-241(삭제) 작성자 패턴에 isOperator OR 분기를 합성한 것이 canModify.
|
||||
|
||||
---
|
||||
|
||||
## 게임 삭제 cascade
|
||||
|
||||
- **GamesMapper 신규 메서드** (comments soft-delete 미러):
|
||||
```java
|
||||
@Update("""
|
||||
UPDATE game_reviews
|
||||
SET is_delete = true, deleted_at = COALESCE(deleted_at, now())
|
||||
WHERE game_id = #{gameId} AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int softDeleteGameReviews(@Param("gameId") long gameId);
|
||||
```
|
||||
- **GameController.deleteGame** cascade 블록(`:243-245`)에 `gamesMapper.softDeleteGameReviews(id);` 한 줄 추가(F9 순서). 좋아요는 미변경.
|
||||
|
||||
---
|
||||
|
||||
## 파일 영향 맵
|
||||
|
||||
| 변경 유형 | 경로 | 역할 |
|
||||
|---|---|---|
|
||||
| 변경 | `db/schema.sql` | dev 블록에 game_comments user_id 컬럼 추가 + game_reviews CREATE 블록 추가 |
|
||||
| 신규 | `docs/game-reviews-ddl.sql` | game_reviews 권위 DDL (recruit-posts-ddl.sql 선례) |
|
||||
| 신규 | `docs/game-reviews-ddl.sql` 동봉 또는 별도 `docs/game-comments-user-id-ddl.sql` | 기존 DB 적용용 idempotent ALTER (game_comments user_id + game_reviews) — security-hardening-ddl.sql DO $$ 패턴 |
|
||||
| 변경 | `src/main/java/.../data/GameCommentData.java` | `Long userId` 필드 + getter/setter 추가 |
|
||||
| 신규 | `src/main/java/.../data/GameReviewData.java` | 리뷰 POJO |
|
||||
| 변경 | `src/main/java/.../mapper/GameCommentsMapper.java` | listGameComments / editGameComment / softDeleteGameComment 추가, getGameComment/addGameComment SELECT·INSERT 에 user_id 반영 |
|
||||
| 신규 | `src/main/java/.../mapper/GameReviewsMapper.java` | 리뷰 CRUD 매퍼 |
|
||||
| 변경 | `src/main/java/.../mapper/GamesMapper.java` | softDeleteGameReviews 추가 |
|
||||
| 신규 | `src/main/java/.../controller/api/GameCommentController.java` | 댓글 API (C1~C4) |
|
||||
| 신규 | `src/main/java/.../controller/api/GameReviewController.java` | 리뷰 API (R1~R5), ROLE_ADMIN 상수·isOperator·canModify |
|
||||
| 변경 | `src/main/java/.../controller/api/GameController.java` | gameDetail(:107) model 에 comments/reviews 주입 + deleteGame(:243) cascade 한 줄 |
|
||||
| 변경(위임) | `src/main/webapp/WEB-INF/views/game-detail.jsp` | 댓글 localStorage JS(:807-1009 중 댓글부) → fetch 교체, 리뷰 위젯 추가 — **/frontend-design 위임** |
|
||||
| 신규(test) | `src/test/.../GameCommentControllerTest.java` | CSRF/200자/권한/운영자 분기 |
|
||||
| 신규(test) | `src/test/.../GameReviewControllerTest.java` | 게임당1회/rating/수정됨/운영자 분기 |
|
||||
|
||||
### DDL 3종 골격
|
||||
|
||||
**① `db/schema.sql` (dev 블록 내 game_comments 직후 추가)**
|
||||
```sql
|
||||
-- game_comments user_id 컬럼 (W3-2: 작성자 귀속, nullable 비파괴)
|
||||
ALTER TABLE "game_comments" ADD COLUMN IF NOT EXISTS "user_id" bigint REFERENCES "users"("id");
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- game_reviews (권위 DDL — docs/game-reviews-ddl.sql 와 동일. W3-2 신규)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS "game_reviews_id_seq";
|
||||
CREATE TABLE IF NOT EXISTS "game_reviews" (
|
||||
"id" bigint DEFAULT nextval('game_reviews_id_seq'::regclass) NOT NULL,
|
||||
"game_id" bigint NOT NULL REFERENCES "games"("id"),
|
||||
"user_id" bigint NOT NULL REFERENCES "users"("id"),
|
||||
"rating" smallint NOT NULL,
|
||||
"body" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deleted_at" timestamp with time zone,
|
||||
"is_delete" boolean DEFAULT false NOT NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "game_reviews_rating_check" CHECK ("rating" BETWEEN 1 AND 5)
|
||||
);
|
||||
ALTER SEQUENCE "game_reviews_id_seq" OWNED BY "game_reviews"."id";
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ux_game_reviews_game_user_active"
|
||||
ON "game_reviews" ("game_id", "user_id") WHERE "is_delete" IS NOT TRUE;
|
||||
CREATE INDEX IF NOT EXISTS "idx_game_reviews_game"
|
||||
ON "game_reviews" ("game_id") WHERE "is_delete" = false;
|
||||
-- 향후 다축(육각형) 확장 시: rating 유지 + game_review_axes(review_id, axis, score) 별도 테이블 분리. 집계 컬럼/뷰는 W2-3 동결 — 신설 금지.
|
||||
```
|
||||
|
||||
**② `docs/game-reviews-ddl.sql` (권위 파일)** — 위 game_reviews CREATE + 시퀀스 + UNIQUE/일반 인덱스 + FK/CHECK 를 DO $$ idempotent 블록(recruit-posts-ddl.sql 형식) + COMMENT ON COLUMN 전 컬럼. rating/body/edited 의미 주석.
|
||||
|
||||
**③ `docs/game-reviews-ddl.sql` 하단 또는 동봉 ALTER 섹션 (기존 DB 적용용 idempotent)**
|
||||
```sql
|
||||
-- game_comments user_id (멱등)
|
||||
ALTER TABLE "game_comments" ADD COLUMN IF NOT EXISTS "user_id" bigint;
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='game_comments_user_id_fkey') THEN
|
||||
ALTER TABLE "game_comments" ADD CONSTRAINT "game_comments_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id");
|
||||
END IF;
|
||||
END $$;
|
||||
-- game_reviews: CREATE SEQUENCE/TABLE IF NOT EXISTS + DO $$ FK/CHECK + CREATE UNIQUE INDEX IF NOT EXISTS (위 ① 와 동일, 멱등 보장)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 신규 시그니처 (inflate 방지 — 각 인자 사용 목적 인라인)
|
||||
|
||||
### GameCommentsMapper (변경)
|
||||
```java
|
||||
List<GameCommentData> listGameComments(long gameId); // gameId: 목록 필터 (is_delete IS NOT TRUE)
|
||||
int editGameComment(GameCommentData c); // c: id+content (content 만 갱신)
|
||||
int softDeleteGameComment(long id); // id: 대상 댓글 (is_delete=true, deleted_at=now())
|
||||
// getGameComment: SELECT 에 user_id AS userId 추가. addGameComment: INSERT 에 user_id 추가.
|
||||
```
|
||||
- listGameComments SELECT: `id AS commentId? → id, game_id AS gameId, nickname AS authorName, user_id AS userId, content, created_at AS createdAt FROM game_comments WHERE game_id=#{gameId} AND is_delete IS NOT TRUE ORDER BY created_at ASC, id ASC`. (users JOIN 없음 — nickname 스냅샷 표시.)
|
||||
|
||||
### GameReviewsMapper (신규)
|
||||
```java
|
||||
GameReviewData getGameReview(long id); // id: 단건/권한확인
|
||||
List<GameReviewData> listGameReviews(long gameId); // gameId: 목록 필터
|
||||
GameReviewData getActiveReviewByGameAndUser(@Param("gameId") long gameId, @Param("userId") long userId); // 게임당1회 선검사
|
||||
int addGameReview(GameReviewData r); // r: gameId/userId/rating/body
|
||||
int editGameReview(GameReviewData r); // r: id/rating/body (updated_at=now() SET 문 명시)
|
||||
int softDeleteGameReview(long id); // id: is_delete=true
|
||||
```
|
||||
- list/get SELECT 의 edited alias: `(updated_at > created_at) AS edited`. authorName: users JOIN `u.display_name AS authorName`(리뷰는 user_id NOT NULL 이므로 JOIN 안전, `u.is_delete IS NOT TRUE` 필터 — soft-delete 사용자 리뷰는 목록서 누락 가능, 허용).
|
||||
|
||||
### isOperator / canModify
|
||||
- 위 권한 모델 절 참조. **inflate 재확인 대상**(concerns 마킹): isOperator(role) 단일 인자, canModify(currentUserId, authorUserId, role) 3인자 — 전부 본문에서 사용. 구현 시 unused 발생하면 즉시 제거.
|
||||
|
||||
---
|
||||
|
||||
## 보안 (체크리스트 line 101~117 댓글/리뷰 해당분 충족 매핑)
|
||||
|
||||
| 체크리스트 line | 충족 방식 |
|
||||
|---|---|
|
||||
| 105 `GET /game/{id}/comments` 또는 모델 주입 결정 | F2: 초기 model 주입 + mutation 후 fetch GET(C1) 혼합 — 확정 |
|
||||
| 106 POST 댓글 CSRF/길이/작성자 | F1: CsrfTokens.isValid + 200자 앱레벨 + user_id 귀속 |
|
||||
| 107 댓글 삭제 작성자/관리자 | F4 canModify(작성자 OR isOperator) |
|
||||
| 108 서버 댓글도 escape/textContent | 서버 렌더 시 HtmlUtils.htmlEscape(JSP), 클라이언트 동적삽입 textContent (research 포인트9 규약 유지) |
|
||||
| 110 localStorage UI 서버 응답 기준 교체 | F1~F4 fetch + 초기 model 주입으로 localStorage 댓글부 제거(위임: frontend-design) |
|
||||
| 114 새로고침/브라우저변경 영속 | 서버 DB 영속(localStorage 비의존) |
|
||||
| 115 토큰 없는 변경 실패 | 전 mutation 첫 게이트 CsrfTokens.isValid → 403 |
|
||||
| 116 XSS payload 미실행 | escape/textContent (108) |
|
||||
| 117 게임 삭제 시 정리 | F9 softDeleteGameComments(기존) + softDeleteGameReviews(신규) |
|
||||
|
||||
- 좋아요 관련 line(101~104)은 범위 밖 — 본 설계 미해결(좋아요 미변경 명시).
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 경계 (/frontend-design 위임)
|
||||
|
||||
본 설계가 제공하는 위임 입력:
|
||||
- **소비 API 계약**: 위 댓글 C1~C4 / 리뷰 R1~R5 표(URL·메서드·요청 param·응답 JSON 형태).
|
||||
- **CSRF 규약**: 모든 mutation 요청에 `BibimbapCsrf.headers()` (theme-init.jsp:19-31) 또는 `_csrf` 파라미터. 메타 `csrf-token` 전역 제공됨.
|
||||
- **escape 규약**: 서버 렌더(JSP scriptlet `<%= HtmlUtils.htmlEscape(...) %>`); 클라이언트 동적 삽입은 `textContent`(innerHTML 금지, listEl 초기화 `innerHTML=''` 만 허용).
|
||||
- **필요 data 형태**: 댓글 `{commentId,authorName,userId,content,createdAt}`; 리뷰 `{reviewId,authorName,userId,rating,body,edited,createdAt,updatedAt}`. `edited:true` 일 때 "수정됨" 마커 표시. 별점 위젯은 rating 1~5 정수 송수신.
|
||||
- **소유자/권한 힌트**: 응답의 `userId` 와 로그인 사용자(`owner` 모델 또는 별도 노출) 비교로 수정·삭제 버튼 노출 결정(서버가 최종 권한 재검증).
|
||||
|
||||
**위임 경계(본 설계 비포함)**: 실제 JSP 마크업, CSS, 별점 위젯 비주얼/인터랙션, "수정됨" 마커 위치·스타일.
|
||||
|
||||
---
|
||||
|
||||
## 대안 비교
|
||||
|
||||
| 결정 | 채택 | 대안 | 채택 근거 |
|
||||
|---|---|---|---|
|
||||
| 댓글/리뷰 분리 | 2 테이블 (game_comments 변경 + game_reviews 신규) | 단일 테이블 type 컬럼 | rating/edited 등 리뷰 전용 컬럼이 댓글에 dead → 사용자 plan gate 확정 |
|
||||
| content 200자 권위 | 앱레벨 검증 | DB CHECK | 기존 text 컬럼·비권위 운영타입·recruit 선례(길이는 앱레벨) |
|
||||
| "수정됨" 판별 | in-row updated_at>created_at | boolean edited / edit_count | 추가 컬럼 0개, 기존 created/updated 쌍 활용, 노출요구 마커 단일 |
|
||||
| 이력 보존 | in-row(updated_at) | 별도 game_review_history | 이력 열람 권한 이월(범위 밖) → 후속 신설이 inflate 회피 |
|
||||
| 게임당 1회 | partial UNIQUE WHERE is_delete IS NOT TRUE | 전체 UNIQUE | soft-delete 후 재작성 허용 + security-hardening active-unique 선례 |
|
||||
| 댓글 작성자 표시 | nickname 스냅샷(JOIN 없음) | users JOIN display_name | 레거시 NULL user_id·user soft-delete dangling 무영향 |
|
||||
| 리뷰 cascade | soft-delete | hard-delete | comments 미러(soft) 일관성 |
|
||||
|
||||
---
|
||||
|
||||
## 롤아웃 / 마이그레이션
|
||||
|
||||
1. **DDL 선적용**: 기존 DB → idempotent ALTER(③) 실행 (game_comments user_id 추가 + game_reviews 생성). 신규 환경은 schema.sql(①)로 자동. 둘 다 `IF NOT EXISTS`/`DO $$` 멱등.
|
||||
2. **역호환**: game_comments user_id NULL 허용 → 기존 레코드 무손상. orphan 이던 GameCommentsMapper 가 컨트롤러 연결돼도 기존 SELECT(getGameComment) 컬럼 추가는 alias 만 늘어 호환.
|
||||
3. **배포 순서**: DDL → 백엔드(매퍼/컨트롤러) → 프론트(/frontend-design). 프론트 미배포 상태에서도 백엔드 API 는 독립 동작(기존 localStorage UI 가 잠시 공존 가능, 비파괴).
|
||||
4. **롤백**: 컨트롤러/매퍼 revert 시 game_reviews 테이블은 잔존(데이터 무손실). game_comments user_id 컬럼은 nullable 이라 revert 후에도 무해. 파괴적 DROP 불요.
|
||||
5. **localStorage 댓글**: 비마이그레이션(확정). 사용자 브라우저 로컬 데이터는 방치(자연 소멸).
|
||||
|
||||
---
|
||||
|
||||
## 검증 포인트 (AC)
|
||||
|
||||
> §4.3 전수 체크 + §4.7 self-audit(시점 안정성·표현 견고성) 적용.
|
||||
|
||||
### 기능 AC (원 요청 매핑)
|
||||
- **AC-1 (댓글 CRUD)**: 로그인 사용자가 C2 작성 → C1 목록에 등장, C3 수정 반영, C4 삭제 후 C1 에서 누락. 단위/통합 테스트 4건. (시점: 테스트 내 자족 — 안정.)
|
||||
- **AC-2 (비작성자 불가 + 운영자 예외)**: 사용자 A 작성 댓글을 사용자 B(role=USER)가 C3/C4 → 403. 사용자 C(role=ADMIN)는 C3/C4 → 200. 단위테스트로 isOperator 분기 검증. (운영자 부여자 없음 → 테스트에서 session role="ADMIN" 주입.)
|
||||
- **AC-3 (리뷰 게임당 1회)**: 동일 user+game R3 두 번째 → 409. soft-delete(R5) 후 R3 재작성 → 200 (partial UNIQUE 검증).
|
||||
- **AC-4 (수정 시 "수정됨" + 이력 보존)**: R3 직후 응답 `edited:false`(updated_at==created_at). R4 후 R1/R2 응답 `edited:true`(updated_at>created_at). created_at 불변 확인.
|
||||
- **AC-5 (댓글 201자 거부)**: C2/C3 에 201자 content → 400. 200자 경계 → 200 (off-by-one 경계 테스트).
|
||||
- **AC-6 (rating 범위)**: R3/R4 rating 0 또는 6 → 400. 1·5 경계 → 200.
|
||||
- **AC-7 (CSRF 없는 상태변경 실패)**: C2/C3/C4/R3/R4/R5 를 X-CSRF-Token·_csrf 없이 호출 → 전부 403. (표현: 헤더/파라미터 둘 다 부재 케이스. UserControllerCsrfTest 선례 형식.)
|
||||
- **AC-8 (XSS payload 미실행)**: content/body 에 `<script>` payload 저장 → JSP 서버 렌더는 `<script>` escape, 클라이언트 textContent 는 문자열 표시. 스크립트 미실행. (검증: 렌더 출력에 raw `<script>` 부재 — escape 확인.)
|
||||
- **AC-9 (영속성)**: C2/R3 후 새 세션/브라우저로 C1/R1 조회 → 데이터 잔존(DB 영속, localStorage 비의존).
|
||||
- **AC-10 (게임 삭제 cascade)**: deleteGame 후 해당 game_id 의 game_comments·game_reviews 전부 is_delete=true. softDeleteGameReviews 호출 검증.
|
||||
|
||||
### 집합 전수 AC (§4.3)
|
||||
- **AC-AGG-1 (엔드포인트 전수 9건)**: 댓글 컨트롤러 매핑 4건(@GetMapping C1 + @PostMapping C2 + @PutMapping C3 + @DeleteMapping C4) + 리뷰 컨트롤러 매핑 5건(R1 R2 GET + R3 POST + R4 PUT + R5 DELETE) == 9.
|
||||
- 검증(불변식, 시점 안정): `grep -cE '@(Get|Post|Put|Delete)Mapping' src/main/java/.../GameCommentController.java` == 4 **AND** `... GameReviewController.java` == 5. (두 파일 합 9. 컨트롤러 파일 한정 → 다른 매핑 오염 없음.)
|
||||
- §4.7 시점: 두 컨트롤러는 본 작업 산출물이며 구현 완료 시점에 고정 — verification 시점에도 동일. 안정.
|
||||
- §4.7 표현: 매핑 애너테이션은 Spring 고정 토큰(동의 표현 없음) → 리터럴 grep 견고. 단 메서드 수가 아니라 매핑 애너테이션 수를 센다(헬퍼 메서드 오카운트 방지).
|
||||
- **AC-AGG-2 (DDL 3종 산출 전수)**: ① schema.sql 에 `game_reviews` CREATE TABLE 블록 존재(`grep -c 'CREATE TABLE IF NOT EXISTS "game_reviews"' db/schema.sql` >= 1) + game_comments user_id ADD COLUMN 존재. ② `docs/game-reviews-ddl.sql` 파일 존재(ls). ③ idempotent ALTER 섹션(②파일 내 `ADD COLUMN IF NOT EXISTS "user_id"` + `DO $$` 블록 존재). 3종 모두 충족.
|
||||
- **AC-AGG-3 (CSRF 게이트 전수)**: 댓글/리뷰 mutation 6개 핸들러(C2 C3 C4 R3 R4 R5) 전부 진입부 `CsrfTokens.isValid(request)` 호출. `grep -c 'CsrfTokens.isValid' (두 컨트롤러)` == 6. (시점: 산출물 고정. 표현: 메서드명 고정 토큰.)
|
||||
|
||||
### §4.7 self-audit 1패스 결과
|
||||
- AC-AGG-1/AGG-3: 카운트 대상이 **본 작업이 만드는 신규 파일**이라 verification 시점까지 값 불변(자기 트리 증가 대상 아님 — 20260616 AC-3 류 함정 회피). 고정 스칼라 허용. 표현은 Spring/메서드 고정 토큰이라 동의표현 누락 위험 없음.
|
||||
- AC-8(XSS): 단일 리터럴 grep 의존 회피 — "raw `<script>` 부재 AND escape 출력 존재"의 의미 불변식 + 수동 렌더 확인 병행(escape 함수가 `<`로 바꾸는지). 표현 견고성 확보.
|
||||
- AC-3/AC-4: 테스트 내 자족 데이터로 시점 독립.
|
||||
|
||||
---
|
||||
|
||||
## 미해결 / 에스컬레이션
|
||||
- 없음(전 결정 확정). 단 frontmatter `concerns` 4건은 **동결영역 비침범 확인 기록 + inflate 재확인 마킹 + W1 의존**으로, 차단 이슈가 아니라 구현/검증 시 점검 포인트. orchestrator 반환만 하고 요구사항 수정은 하지 않음.
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
phase: documentation
|
||||
agent: documentation-advisor
|
||||
agent_version: 1
|
||||
generated_at: "2026-06-18T14:00:00+09:00"
|
||||
concerns:
|
||||
- "design-advisor가 세션 내에서 design.md를 생성했으나 docs/architecture/ 갱신은 수행하지 않았음. W3-2 도메인 추가가 architecture/ 구조 설명에 반영이 필요한지 추후 검토 대상."
|
||||
- "L3 브라우저 스모크(XSS·영속·cascade DB) 및 DDL 적용이 needs_user_verification 상태. 완료 후 security-remediation-checklist.md의 [~] 항목을 [x]로 갱신 필요."
|
||||
- "운영자 ROLE_ADMIN 부여 경로는 W1 RBAC 연결 시 활성. W1 완료 후 보안 체크리스트 별도 항목 검토 필요."
|
||||
concerns_checked: true
|
||||
---
|
||||
|
||||
# 문서화 보고
|
||||
|
||||
## 작성/수정된 문서
|
||||
|
||||
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|
||||
|---|---|---|---|---|
|
||||
| `docs/changes/2026-06-18-w3-2-comments-reviews.md` | changes | 신규 | `docs/changes/index.md` | `docs/work-log/2026-06-17-w3-feature-skeletons.md`, `docs/security/security-remediation-checklist.md` |
|
||||
| `docs/changes/index.md` | changes | 기존 갱신 | — | — |
|
||||
| `docs/security/security-remediation-checklist.md` | security | 기존 갱신 | — | `docs/changes/2026-06-18-w3-2-comments-reviews.md` |
|
||||
| `docs/work-log/2026-06-17-w3-feature-skeletons.md` | work-log | 기존 갱신(§W3-2 상태 1줄) | — | `docs/changes/2026-06-18-w3-2-comments-reviews.md` |
|
||||
| `.atp/work-session/20260618-104034/documentation.md` | — | 산출물(본 파일) | — | — |
|
||||
|
||||
## 의사결정 기록 위치
|
||||
|
||||
- **plan gate 결정 2건** (댓글 완전 서버화 / 휴면 운영자 경로): `.atp/work-session/20260618-104034/report.md` §Decisions
|
||||
- **seed 반전 발견** (댓글·좋아요 localStorage 전용, 운영자 role 부재): `.atp/work-session/20260618-104034/research/code-coupling.md`
|
||||
- **설계 권위 명세** (DDL 3종, API 9개, AC 13): `.atp/work-session/20260618-104034/design.md`
|
||||
- **QG-2 비파괴 결정** (user_id NULL + 레거시 보존): `.atp/work-session/20260618-104034/report.md` §Decisions (orchestrator 브리프 이월 기본값)
|
||||
|
||||
## 추후 문서화가 필요한 항목
|
||||
|
||||
- **L3 스모크 완료 후**: `docs/security/security-remediation-checklist.md` B3 [~] 항목 → [x] 갱신. 특히 XSS 브라우저 미실행, 영속성 브라우저 재방문 확인.
|
||||
- **DDL 적용 완료 후**: `docs/maintenance/` 또는 운영 절차 문서에 game_reviews DDL 적용 이력 기록 검토(현재 docs/game-reviews-ddl.sql이 권위 DDL로 존재).
|
||||
- **W1 RBAC 완료 후**: 운영자 role 부여 경로 활성화 내용을 `docs/security/security-remediation-checklist.md` B3 및 관련 changes/ 에 추가.
|
||||
- **W2-6 시상 집계 구현 후**: game_reviews 평점 공급원 → 집계 계약(SELECT AVG/COUNT) 이행 내용을 changes/ 에 별도 기록.
|
||||
- **좋아요 영속화 착수 시**: B3 체크리스트 좋아요 항목(101~104) 별도 변경 이력 문서 작성.
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
---
|
||||
phase: implementation
|
||||
agent: implementation-advisor
|
||||
agent_version: 1
|
||||
generated_at: 2026-06-18T11:20:00+09:00
|
||||
---
|
||||
|
||||
# 파일 소유권 맵 — W3-2 댓글/리뷰 분리 (백엔드 only)
|
||||
|
||||
직접 작성 채택 사유: 파일 11개이나 POJO→Mapper→Controller→Test 가 강한 상호의존(필드명·시그니처·SQL alias 가 한 줄이라도 어긋나면 컴파일/매핑 실패). 1파일 1worker 분산 시 worker 간 계약 동기화 비용 > 병렬 이득. 오케스트레이터 지시(파일 상호의존 시 직접 작성 허용) + 프로토콜 §11.2 계량 근거(파일수 11 > 8 이나 줄수 다수 < 500 이고 cross-file 계약 결합도 최상)로 advisor 직접 Write/Edit 선택.
|
||||
|
||||
| 파일 | 담당 | 변경 유형 | 의존 |
|
||||
|---|---|---|---|
|
||||
| db/schema.sql | advisor | modify | - |
|
||||
| docs/game-reviews-ddl.sql | advisor | create | - |
|
||||
| src/main/java/com/pandoli365/bibimbap/data/GameCommentData.java | advisor | modify | - |
|
||||
| src/main/java/com/pandoli365/bibimbap/data/GameReviewData.java | advisor | create | - |
|
||||
| src/main/java/com/pandoli365/bibimbap/mapper/GameCommentsMapper.java | advisor | modify | GameCommentData |
|
||||
| src/main/java/com/pandoli365/bibimbap/mapper/GameReviewsMapper.java | advisor | create | GameReviewData |
|
||||
| src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java | advisor | modify | - |
|
||||
| src/main/java/com/pandoli365/bibimbap/controller/api/GameCommentController.java | advisor | create | GameCommentsMapper, GamesMapper, GameCommentData |
|
||||
| src/main/java/com/pandoli365/bibimbap/controller/api/GameReviewController.java | advisor | create | GameReviewsMapper, GamesMapper, GameReviewData |
|
||||
| src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java | advisor | modify | GameCommentsMapper, GameReviewsMapper, GamesMapper |
|
||||
| src/test/java/com/pandoli365/bibimbap/controller/api/GameCommentControllerTest.java | advisor | create | GameCommentController |
|
||||
| src/test/java/com/pandoli365/bibimbap/controller/api/GameReviewControllerTest.java | advisor | create | GameReviewController |
|
||||
|
||||
planned_workers: 0 (직접 작성 선택)
|
||||
actual_workers: 0
|
||||
|
||||
## 불변식 점검
|
||||
- 동일 파일 1소유: OK (전부 advisor 단독)
|
||||
- game-detail.jsp 제외: /frontend-design 위임 (스코프 명시)
|
||||
- DDL DB 적용 금지: 파일 작성만
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
---
|
||||
schema_version: 2
|
||||
session_id: 20260618-104034
|
||||
resumed_from: null
|
||||
started_at: 2026-06-18T10:40:34+09:00
|
||||
ended_at: 2026-06-18T13:20:00+09:00
|
||||
user_request: |
|
||||
W3-2 댓글/리뷰 분리 기능을 설계·구현한다.
|
||||
- game_comments 로그인 연동 전환 + content 200자 제한
|
||||
- game_reviews 신규 (게임당 1회, 별점 5점 + 서술 평가)
|
||||
- 댓글 수정/삭제 권한: 작성자 본인 + 운영자
|
||||
- 리뷰 수정: 가능 + 이력 보존, 노출은 "수정됨" 마커만
|
||||
- 보안: CSRF 검증, XSS escape, 작성자 권한 체크
|
||||
- 자율 진행: 미결은 안전 기본값으로 진행 + 이월 기록. 데이터 손실/하류 파손 결정만 사용자 확인.
|
||||
- 프론트는 /frontend-design 스킬 사용.
|
||||
acceptance criteria:
|
||||
- 로그인 사용자 댓글 작성/수정/삭제, 비작성자 불가(운영자 예외)
|
||||
- 리뷰 게임당 1회, 수정 시 "수정됨" 마커 + 이력 보존
|
||||
- 댓글 201자 이상 거부
|
||||
- CSRF 토큰 없는 상태변경 실패
|
||||
- XSS payload 미실행
|
||||
- 새로고침/브라우저 변경 후 영속
|
||||
---
|
||||
|
||||
# Summary
|
||||
W3-2 댓글/리뷰 분리 기능을 설계·구현했다. research 가 작업 브리프의 seed 가정 2건을 뒤집었다(① 댓글·좋아요가 서버 미연동 localStorage 전용이고 서버 매퍼는 orphan, ② 운영자 role 이 코드에 부재—USER만 발급). §2.7-5 plan gate 로 사용자 확정 후, 댓글을 서버 영속화(fetch API + user_id 귀속 + 200자) + game_reviews 도메인 신설(게임당 1회 partial UNIQUE, 별점 1~5, 수정됨=updated_at>created_at in-row, 휴면 운영자 경로)로 진행했다.
|
||||
|
||||
백엔드 14파일(DDL 3종 + POJO/Mapper/Controller 신규·변경 + 테스트 2), 프론트(game-detail.jsp 댓글 서버화 + 리뷰 UI·별점 위젯·수정됨 마커, /frontend-design 스킬)를 구현했다. 검증 1차에서 BibimbapApplicationTests.contextLoads 회귀(신규 매퍼 @MockBean 누락) 1건이 잡혀 §2.6 backward re-dispatch 로 발원(implementation 테스트 scaffolding)을 진단·전수재검 후 2개 @MockBean 추가로 해소. 최종 L1 31 테스트 PASS + AGG 전수 PASS. L3(브라우저 XSS·영속·cascade DB)는 needs_user_verification. DDL 미적용(§6 게이트 — 사용자 DB 적용 대기).
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
# 각 advisor 호출/스킵 판단 즉시 1줄 append
|
||||
- advisor: requirements-advisor
|
||||
decision: skip
|
||||
rationale: '요청에 FR/in/out/이월 기본값/AC 가 전부 명시됨. 단 옵션 공간 판정은 design 산출 이후로 보류(프로토콜 §1).'
|
||||
checked_at: 2026-06-18T10:40:34+09:00
|
||||
- advisor: graphify-lookup-advisor
|
||||
decision: call
|
||||
rationale: '코드 구조 조사 1차 진입점. game_comments/games/로그인·세션/CSRF/JSP 댓글/DDL 적용방식 결합점 확인 필요.'
|
||||
checked_at: 2026-06-18T10:41:00+09:00
|
||||
result: 'src scope graph.json 미생성(no-graph). docs scope만 hit(DDL/스키마 부트스트랩). 코드 포인트 7/7 miss → research 필요.'
|
||||
- advisor: research-advisor
|
||||
decision: call
|
||||
rationale: 'lookup 7/7 miss. 댓글 CRUD·게임상세·로그인세션·CSRF·DDL적용·RecruitController·GameLikes 코드 결합점 직접 조사 필요. parallel-explorer 병렬.'
|
||||
checked_at: 2026-06-18T10:45:00+09:00
|
||||
- advisor: graphify-update-advisor (src scope 재생성)
|
||||
decision: defer
|
||||
rationale: '구현으로 src 변경 예정 → 지금 재생성하면 이중 작업. 세션 종료 전 graph-refresh 단계에서 일괄 재생성(no-defer 정책은 그 시점 처리로 충족).'
|
||||
checked_at: 2026-06-18T10:45:00+09:00
|
||||
- advisor: research-advisor
|
||||
decision: re-call
|
||||
rationale: '1차 호출이 async worker 6개 spawn 후 취합 전 rest 복귀 — code-coupling.md 미산출. SendMessage 미제공으로 재개 불가. 강한 블로킹 지시로 재호출(worker TaskOutput 대기 + 파일 디스크 기록 후에만 반환).'
|
||||
checked_at: 2026-06-18T10:52:00+09:00
|
||||
|
||||
# Invocations
|
||||
- id: inv-000
|
||||
layer: orchestrator
|
||||
name: orchestrator
|
||||
parent_invocation_id: null
|
||||
started_at: 2026-06-18T10:40:34+09:00
|
||||
input_digest: 'W3-2 댓글/리뷰 분리 설계·구현 요청 (자율 진행 모드)'
|
||||
output_digest: 'init 가드 통과, atp:migrate 마커 없음(skip), work-session 생성, 프로토콜 로드'
|
||||
artifacts: ['.atp/work-session/20260618-104034/report.md']
|
||||
concerns: []
|
||||
- id: inv-001
|
||||
layer: advisor
|
||||
name: graphify-lookup-advisor
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: '코드 구조 7포인트 graph 인덱스 조회'
|
||||
output_digest: 'src scope graph 미생성(no-graph), docs만 hit, 7/7 miss → research 필요'
|
||||
artifacts: []
|
||||
model_choice: { phase: graphify-exec, dispatch_size: direct, tier: small, effort: low, resolved_model: haiku, capped: false, rationale: '인덱스 조회 기계적 판단' }
|
||||
- id: inv-002
|
||||
layer: advisor
|
||||
name: research-advisor
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: '11개 코드 결합점 정밀 조사(file:line)'
|
||||
output_digest: 'research/code-coupling.md — 댓글/좋아요 서버미연동 발견, CSRF/세션 인프라 재사용 가능, 운영자 role 부재'
|
||||
artifacts: ['research/code-coupling.md']
|
||||
model_choice: { phase: analyze, dispatch_size: l-batch, tier: large, effort: high, resolved_model: opus, capped: false, escalation_reason: '탐색적 코드 조사 + 다파일 교차', rationale: '결합점 정확도가 설계 좌우' }
|
||||
notes: '1차 호출(a2177)이 async worker 취합 전 조기복귀 → 강한 블로킹 지시로 재호출(a4873) 성공. 두 호출 모두 동일 결론.'
|
||||
- id: inv-003
|
||||
layer: advisor
|
||||
name: design-advisor
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: 'research + plan gate 결정 → 구현가능 설계'
|
||||
output_digest: 'design.md — DDL 3종/9 API/권한모델/cascade/AC 13(기능10+전수3)'
|
||||
artifacts: ['design.md']
|
||||
model_choice: { phase: design, dispatch_size: l-batch, tier: large, effort: high, resolved_model: opus, capped: false, escalation_reason: '보안·인증·권한 §5.2 자동상승', rationale: 'trade-off 빈번' }
|
||||
- id: inv-004
|
||||
layer: advisor
|
||||
name: implementation-advisor
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: 'design.md 권위 명세 → 백엔드 구현 + 마이그레이션 파일'
|
||||
output_digest: '14파일(DDL3+POJO+Mapper+Controller+test2). test-compile BUILD SUCCESS. workers_spawned=0(직접). implementation/ownership.md'
|
||||
artifacts: ['implementation/ownership.md']
|
||||
model_choice: { phase: code-implementation, dispatch_size: l-batch, tier: large, effort: high, resolved_model: opus, capped: false, escalation_reason: '보안/권한 코드', rationale: '다파일 강결합 직접 작성' }
|
||||
planned_workers: 0
|
||||
actual_workers: 0
|
||||
- id: inv-005
|
||||
layer: orchestrator
|
||||
name: orchestrator (frontend via /frontend-design)
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: 'game-detail.jsp 댓글 서버화 + 리뷰 UI (브리프 지정 /frontend-design 스킬, §1 사용자명시 예외)'
|
||||
output_digest: 'GameController 모델 노출(currentUserId/userRole) + game-detail.jsp HTML/CSS/JS(별점위젯·수정됨·로그인게이트, textContent XSS안전). test-compile SUCCESS'
|
||||
artifacts: ['src/main/webapp/WEB-INF/views/game-detail.jsp']
|
||||
model_choice: { phase: code-implementation, dispatch_size: direct, tier: large, effort: high, resolved_model: inherit, capped: false, rationale: 'orchestrator 본 모델 직접(스킬 가이드)' }
|
||||
- id: inv-006
|
||||
layer: advisor
|
||||
name: verification-advisor
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: 'AC 13 + 실행명령(설계/diff 비접근)'
|
||||
output_digest: '1차 fail(contextLoads 회귀 blocker). 수정 후 재실행 31 PASS + AGG 전수 PASS'
|
||||
artifacts: ['verification.md']
|
||||
model_choice: { phase: validation-static, dispatch_size: s-batch, tier: medium, effort: medium, resolved_model: sonnet, capped: false, rationale: '결정적 테스트/grep 판정' }
|
||||
- id: inv-007
|
||||
layer: orchestrator
|
||||
name: orchestrator (regression fix §2.6)
|
||||
parent_invocation_id: inv-000
|
||||
input_digest: 'contextLoads blocker — 신규 매퍼 @MockBean 누락'
|
||||
output_digest: 'BibimbapApplicationTests 에 GameCommentsMapper/GameReviewsMapper @MockBean 2개 추가(기존 4개 패턴 미러). 재실행 31 PASS'
|
||||
artifacts: ['src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java']
|
||||
model_choice: { phase: code-implementation, dispatch_size: direct, tier: large, effort: low, resolved_model: inherit, capped: false, rationale: '결정적 2줄 마이크로 편집 §1 예외' }
|
||||
|
||||
# Decisions
|
||||
- by: research-advisor + user (plan gate §2.7-5)
|
||||
at: 2026-06-18T11:10:00+09:00
|
||||
decision: '댓글/좋아요 서버 미연동(localStorage 전용) seed 반전 확인. 댓글 완전 서버화 채택(Q1=Recommended).'
|
||||
rationale: 'AC "브라우저 변경 후 영속"이 서버화를 강제. localStorage JS→fetch 교체 포함. 기존 localStorage 댓글 비마이그레이션(보안 [hold] 정합).'
|
||||
related_invocations: [inv-002]
|
||||
- by: user (plan gate §2.7-5)
|
||||
at: 2026-06-18T11:10:00+09:00
|
||||
decision: '운영자 예외 = 휴면 운영자 경로(Q2=Recommended). 권한체크 "작성자 OR 운영자급 role", ADMIN 상수 정의, 부여자 없음(W1 연결).'
|
||||
rationale: '브리프 "W1 완료 시 교체 가능 설계"와 정합. 단위테스트로 운영자 분기 검증, prod 무영향.'
|
||||
related_invocations: [inv-002]
|
||||
- by: orchestrator (브리프 이월 기본값)
|
||||
at: 2026-06-18T11:10:00+09:00
|
||||
decision: '별점 5점 단일 확정(다축 컬럼 확장여지). 댓글/리뷰 2 테이블 분리(game_comments 변경 + game_reviews 신규). 좋아요 범위밖 localStorage 유지. W2-3 집계계약 이월(평점 컬럼만 확정). QG-2 비파괴(user_id nullable + 레거시 표시) — 테이블 사실상 빈 상태 추정.'
|
||||
rationale: '브리프 이월 기본값 + research 결합점. 되돌릴 수 있어 carry-over 안전.'
|
||||
related_invocations: [inv-001, inv-002]
|
||||
|
||||
# Conflicts
|
||||
(없음 — 단일 design-advisor, concerns 4건 모두 동결영역 비침범 자가확인. 충돌 중재 불요.)
|
||||
|
||||
# Regression
|
||||
- surfaced_at_stage: verification (L1 전체 테스트)
|
||||
source_stage: implementation (테스트 scaffolding)
|
||||
defect: 'BibimbapApplicationTests.contextLoads — 신규 GameCommentController/GameReviewController 가 주입하는 GameCommentsMapper/GameReviewsMapper 가 MyBatis autoconfigure exclude 컨텍스트 테스트의 @MockBean 셋에 미등록 → NoSuchBeanDefinitionException'
|
||||
full_set_recheck: true # 컨트롤러 주입 매퍼 전수 점검: {Games,Recruit,UserAuthIdentities,Users,GameComments,GameReviews} 중 누락 2개만 추가하면 완전 확인
|
||||
downstream_rerun: ['L1 전체 테스트 재실행 — 31 PASS']
|
||||
resolved_at: 2026-06-18T12:55:00+09:00
|
||||
|
||||
# verified_by_me
|
||||
- 'L1 typecheck/compile: ./mvnw -o test-compile BUILD SUCCESS'
|
||||
- 'L1 unit+regression: ./mvnw -o test — Tests run 31, Failures 0, Errors 0 (GameCommentControllerTest 12 + GameReviewControllerTest 13 + UserControllerCsrfTest 5 + BibimbapApplicationTests 1). 회귀 0.'
|
||||
- 'AGG-1 엔드포인트: GameCommentController @Mapping==4, GameReviewController==5 (PASS)'
|
||||
- 'AGG-3 CSRF 게이트: CsrfTokens.isValid 합 6 (mutation 6개 전수, PASS)'
|
||||
- 'AGG-2 DDL 3종: schema.sql game_reviews CREATE + game_comments user_id, docs/game-reviews-ddl.sql 존재 + 멱등 ALTER (PASS)'
|
||||
- 'L2: skipped — no-external-dependency (커스텀 세션/CSRF, mock 기반 단위)'
|
||||
|
||||
# needs_user_verification
|
||||
- 'L3 실 톰캣+DB 스모크: 게임 상세 페이지에서 (a) 로그인 후 댓글 작성/수정/삭제, (b) 리뷰 작성→게임당 1회(2번째 409)→수정 시 "수정됨" 표시, (c) <script> payload 입력 후 스크립트 미실행(textContent escape), (d) 새로고침/다른 브라우저에서 영속 확인, (e) 게임 삭제 후 댓글·리뷰 정리 확인.'
|
||||
- 'DDL 적용: [완료 — 로컬 dev] 사용자 승인 후 orchestrator 가 docs/game-reviews-ddl.sql 을 로컬 dev DB(bibimbap-db postgres:16, localhost:5433, schema=dev)에 --single-transaction 적용. game_reviews(컬럼9/제약4/인덱스2) + game_comments.user_id 생성 검증, 멱등 재실행 error 0. **잔여: live 스키마는 배포 시 동일 적용 필요**(현 적용은 dev 한정).'
|
||||
|
||||
# Open Items
|
||||
- 'git 미커밋 잔여: 본 작업 단위(W3-2)의 변경 5 modified + 9 untracked(src) + docs/game-reviews-ddl.sql. 프로젝트 커밋 정책(사용자 요청 시 커밋)에 따라 커밋 대기 — 사용자 확인 후 진행.'
|
||||
- '이월: W2-3 평점 집계 계약(game_reviews 는 평점 공급원까지만, 집계 컬럼/뷰 미생성). W2-6 시상 집계는 후속 SELECT AVG/COUNT 로 소비.'
|
||||
- '이월: 운영자 role 부여 경로 — ROLE_ADMIN="ADMIN" 상수만 정의, 실제 ADMIN 부여자 없음(W1 RBAC/Interceptor 연결 시 활성).'
|
||||
- '이월: 다축(육각형) 평점 — 현재 단일 rating, 향후 game_review_axes 분리 여지(주석).'
|
||||
- '이월: 리뷰 이력 열람 — 현재 in-row(수정됨 마커만), history 테이블은 열람권한 확정 시 후속.'
|
||||
- '범위밖: 좋아요 영속화(여전히 localStorage) — 보안 체크리스트 별도 항목.'
|
||||
- '범위밖: GameCatalog 정적 폴백 게임(DB 미존재)은 댓글/리뷰 mutation 시 404(게임존재 검증). 실 게임만 기능 동작.'
|
||||
|
||||
# graph_refresh
|
||||
- judgment: 'src=no-graph(산출물 부재 — 세션 시작 시점부터 pre-existing, W3-2 무관) + working-tree fully-stale 수준 변경 / docs=partial-stale(신규 문서4 + game-reviews-ddl.sql 미반영)'
|
||||
handling: '커밋 시점 처리(no-defer 정책 — 미래 세션 이월 아님). checker 권고대로 미커밋 tree 에서 graphify 시 source_commit 부정확 → 커밋 직후 /graphify src + /graphify docs 실행 + docs/graph/index.md frontmatter·Scopes 표 갱신. 커밋이 사용자 요청 대기 중이므로 graph 재생성도 커밋 단계에 동반.'
|
||||
|
||||
# Applied Changes (orchestrator — §12 회고 반영)
|
||||
- 'MEMORY task_completion 갱신: 신규 컨트롤러/매퍼 의존 추가 시 BibimbapApplicationTests @MockBean 등록 누락 → contextLoads 실패 함정 + full test 의무 (candidate 2, negative).'
|
||||
- 'docs_sync: docs/development/verification-strategies.md 에 "신규 컨트롤러/매퍼 의존 변경 시 full test 의무" 규칙 추가 (candidate 2 docs_sync_target).'
|
||||
- 'candidate 1(worker-spawn advisor 블로킹+write-before-return): 본 세션에서 선제 적용으로 재발 0. protocol_feedback 로 surface(아래).'
|
||||
- 'candidate 3(plan-gate 마일스톤): changes/2026-06-18 문서가 이미 기록 — 별도 MEMORY 미작성.'
|
||||
- 'protocol_feedback 2건(structural)은 atp 플러그인 `agent-team-protocol.md`(= ~/.claude/ 전역설정) 수정 제안 → §6 전역설정 수정 금지로 orchestrator 가 직접 적용하지 않음. 사용자에게 surface(플러그인 유지자 결정 영역). (1) worker-spawn advisor 블로킹 규약 명문화 (2) implementation 단계 컨트롤러 추가 시 full test 의무.'
|
||||
|
||||
# User Signals
|
||||
user_signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: 'plan gate 2개 질문에 즉시 Recommended 선택(완전 서버화 + 휴면 운영자 경로)'
|
||||
about: 'seed 반전 후 제시한 기본값 방향이 사용자 의도와 정합 — 1회 수락'
|
||||
negative: []
|
||||
|
||||
# Retrospective
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: 'plan gate 2개 질문에 즉시 Recommended 선택(완전 서버화 + 휴면 운영자 경로)'
|
||||
about: 'research가 seed 가정 2건 반전 후 plan gate에서 제시한 기본값 방향(Q1=완전 서버화, Q2=휴면 운영자 경로)이 사용자 의도와 1회 수락으로 정합됨. 비자명한 판단(localStorage 전용 댓글을 완전 서버화로 전환하는 방향, 운영자 role 미정의 상태에서 휴면 경로 채택)이 재확인됨.'
|
||||
negative: []
|
||||
what_went_well:
|
||||
- 'research-advisor가 seed 가정 2건(댓글 서버 미연동, 운영자 role 부재)을 file:line 근거로 반전시키고 plan gate로 사용자 위임한 흐름이 1회 수락으로 완결. 기존 memory [[research-seed-reversal-plan-gate-delegation]]의 패턴이 bibimbap 세션에서도 재현됨.'
|
||||
- 'implementation-advisor가 worker 0(advisor 직접 작성)을 선택한 판단이 옳았음. 파일 12개이나 POJO→Mapper→Controller→Test 강결합으로 분산 시 계약 동기화 비용 > 병렬 이득. 기존 memory [[implementation-advisor-direct-execution-threshold]] 패턴과 일치.'
|
||||
- 'verification-advisor가 §2.6 backward re-dispatch를 발동해 contextLoads 회귀를 blocker 판정 후 발원 단계(implementation 테스트 scaffolding)까지 소급 진단. 컨트롤러 주입 매퍼 전수 재검 후 누락 2개만 정확히 식별.'
|
||||
- 'research-advisor 2차 호출 시 강한 블로킹 지시("worker TaskOutput 대기 + 파일 디스크 기록 후에만 반환")를 넣자 code-coupling.md 정상 산출. 이후 implementation/verification 호출에 같은 지시를 선제 적용해 재발 0.'
|
||||
what_to_improve:
|
||||
- '[패턴 1] research-advisor(tier-3, worker spawn) 가 worker 6개 spawn 후 취합 전 조기복귀. code-coupling.md 미산출 → orchestrator가 SendMessage 없이 재호출. 호출 비용 이중 발생 + 세션 지연. 블로킹 지시가 없으면 advisor가 async spawn 후 결과 대기 없이 반환하는 것이 기본 동작인 것으로 보임. → 프로토콜 수준 대응 필요(protocol_feedback 참조).'
|
||||
- '[패턴 2] implementation-advisor가 test-compile(./mvnw -o test-compile)만 실행하고 full context test(./mvnw -o test)를 생략. BibimbabApplicationTests.contextLoads 회귀 미탐지 → verification에서 뒤늦게 발견. contextLoads는 MyBatis autoconfigure exclude + 신규 컨트롤러 @MockBean 수동 제공 패턴이므로, 컨트롤러 추가 시마다 @MockBean 갱신이 필요한데 이것이 implementation 단계에서 체크되지 않음.'
|
||||
memory_candidates:
|
||||
- name: worker-spawn-advisor-blocking-write-before-return
|
||||
type: feedback
|
||||
description: worker를 spawn하는 advisor는 모든 worker의 TaskOutput 완료 대기 + 산출 파일 디스크 기록 후에만 반환해야 한다. 블로킹 지시 없이 호출하면 async spawn 후 조기복귀 → 산출물 미생성 재호출 비용 발생.
|
||||
body_draft: |
|
||||
advisor가 병렬 worker(parallel-explorer 등)를 spawn할 때, 호출 측이 명시적 블로킹 지시를 넣지 않으면 advisor가 worker TaskOutput을 취합하지 않고 조기복귀하는 사례가 관측됨(20260618-104034 research-advisor 1차 호출: worker 6개 spawn 후 code-coupling.md 미산출 복귀).
|
||||
|
||||
**Why:** SendMessage 툴이 호스트 컨텍스트에 미제공인 경우 advisor가 subagent 재개 수단이 없어 결과를 기다리지 않고 반환할 수 있다. 강한 블로킹 지시를 명시하면 advisor가 tool 반환값(TaskOutput)을 동기적으로 기다리게 된다.
|
||||
|
||||
**How to apply:**
|
||||
- worker spawn이 예상되는 advisor(research-advisor, analysis-advisor 등) 호출 시 orchestrator는 다음 문구를 프롬프트 마지막에 명시한다:
|
||||
"worker를 spawn한 경우 모든 worker의 TaskOutput을 완료까지 대기하고, 산출 파일을 디스크에 기록한 후에만 반환하라. 결과 취합 전 반환 금지."
|
||||
- 재호출 1회 이내에 성공한 경우라도, 이후 동일 advisor 호출(implementation, verification 포함)에 선제 적용해 재발 차단.
|
||||
- 산출 파일 존재 여부는 orchestrator가 advisor 반환 직후 Glob/Read로 확인 가능.
|
||||
rationale_for_saving: worker spawn 패턴은 research/analysis advisor에서 반복 재현 가능. SendMessage 미제공 환경에서는 구조적으로 재발한다. 코드/커밋으로 유도 불가한 호출 규약 지식.
|
||||
signal_source: negative
|
||||
docs_sync_target: null
|
||||
|
||||
- name: spring-context-test-mockbean-exhaustive-on-new-controller
|
||||
type: feedback
|
||||
description: Spring Boot 컨텍스트 테스트(MyBatis autoconfigure exclude + @MockBean 패턴)에서 신규 컨트롤러 추가 시 해당 컨트롤러가 주입하는 모든 매퍼의 @MockBean을 반드시 등록해야 한다. implementation 단계에서 full context test(./mvnw -o test)를 실행해야 누락 탐지 가능.
|
||||
body_draft: |
|
||||
BibimbapApplicationTests는 DataSource/MyBatis autoconfigure를 exclude하고 컨트롤러가 주입하는 매퍼를 @MockBean으로 수동 제공하는 패턴이다. 이 패턴에서 신규 컨트롤러(GameCommentController, GameReviewController)를 추가할 때 해당 컨트롤러가 주입하는 매퍼(GameCommentsMapper, GameReviewsMapper)의 @MockBean을 BibimbabApplicationTests에 추가하지 않으면 contextLoads가 NoSuchBeanDefinitionException으로 실패한다.
|
||||
|
||||
**Why:** implementation-advisor가 test-compile(./mvnw -o test-compile)만 실행하면 컨트롤러 클래스는 컴파일되지만, full Spring context 로드는 ./mvnw -o test를 실행해야만 검증된다. 컴파일 성공 ≠ contextLoads 통과. 세션 20260618-104034에서 이 미탐지로 verification 단계에서 blocker가 발생했다.
|
||||
|
||||
**How to apply:**
|
||||
- implementation-advisor는 컨트롤러 파일을 추가/변경할 때 반드시 ./mvnw -o test를 실행해 contextLoads 회귀를 탐지해야 한다. test-compile만으로는 불충분.
|
||||
- 신규 컨트롤러 추가 체크리스트: (1) 컨트롤러가 @Autowired/@RequiredArgsConstructor로 주입하는 모든 @Mapper 인터페이스를 나열, (2) BibimbabApplicationTests의 기존 @MockBean 선언(@MockBean GameCommentsMapper 등) 목록과 대조, (3) 누락 항목에 @MockBean 추가.
|
||||
- 이 프로젝트의 현행 @MockBean 목록: GamesMapper, RecruitPostsMapper, UserAuthIdentitiesMapper, UsersMapper, GameCommentsMapper, GameReviewsMapper (W3-2 추가 후 기준).
|
||||
rationale_for_saving: 이 프로젝트 특유의 MyBatis autoconfigure exclude + @MockBean 수동 패턴은 컨트롤러를 추가할 때마다 재발 가능한 구조적 함정. 코드/커밋에서 유도 불가(테스트 파일을 직접 읽어야만 파악 가능). 후속 W3-x, W4-x 세션에서 컨트롤러 추가가 예상되므로 재현성 높음.
|
||||
signal_source: negative
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
|
||||
|
||||
- name: bibimbap-plan-gate-seed-reversal-two-q-accepted
|
||||
type: project
|
||||
description: W3-2 세션에서 research가 seed 가정 2건 반전 후 plan gate 2질문 즉시 수락. 기본값 방향(완전 서버화 + 휴면 운영자 경로)이 사용자 의도와 정합 확인됨.
|
||||
body_draft: |
|
||||
W3-2(댓글/리뷰 분리) 세션(20260618-104034)에서 research-advisor가 seed 가정 2건을 반전:
|
||||
- GAP-1: 댓글/좋아요가 서버 미연동 localStorage 전용(서버화 가정 깨짐)
|
||||
- GAP-3: 운영자 role이 코드 전체에 부재("USER"만 발급)
|
||||
|
||||
Plan gate 2질문 모두 Recommended(완전 서버화 / 휴면 운영자 경로) 즉시 수락.
|
||||
|
||||
**확인된 패턴:** [[research-seed-reversal-plan-gate-delegation]]이 bibimbap 프로젝트에서도 동작함. seed 반전 + Recommended 기본값 방향 제시 조합이 사용자 의사결정 비용을 최소화한 케이스로 기록.
|
||||
rationale_for_saving: bibimbap 프로젝트 히스토리 마일스톤. W3 진행 중 댓글/좋아요가 localStorage 전용이었다는 사실은 후속 W3-x/W4-x 세션에서 참고 필요. 또한 운영자 경로(ROLE_ADMIN="ADMIN" 상수 정의, 부여자 미구현)가 W1 RBAC 연결 전 휴면 상태라는 결정 이력.
|
||||
signal_source: positive
|
||||
docs_sync_target: null
|
||||
|
||||
protocol_feedback:
|
||||
- issue: 'worker를 spawn하는 advisor(tier-3)의 조기복귀가 프로토콜 허점임. SendMessage 미제공 환경에서 advisor가 async worker spawn 후 TaskOutput 취합 없이 반환하는 동작을 방지하는 명시적 규약이 없다.'
|
||||
structural: true
|
||||
detail: |
|
||||
현상: research-advisor 1차 호출이 worker 6개 spawn 후 취합 전 반환 → code-coupling.md 미산출 → 재호출 비용.
|
||||
근본 원인: 프로토콜에 "worker spawn 후 반환 조건"이 없음. 개별 호출 시 orchestrator가 블로킹 지시를 매번 수동으로 포함해야 해결됨 — 즉 규약이 아니라 ad-hoc 대응.
|
||||
proposed_fix: |
|
||||
agent-team-protocol.md (bibimbap 번들 또는 ATP 원본) 에 다음 규약 추가:
|
||||
§ [worker spawn 완료 조건] advisor가 worker를 spawn하는 경우:
|
||||
1. 모든 worker의 TaskOutput이 도달할 때까지 반환하지 않는다 (blocking-wait).
|
||||
2. 약속된 산출 파일을 디스크에 기록 완료한 것을 확인 후 반환한다.
|
||||
3. orchestrator는 advisor 반환 직후 산출 파일 존재 여부를 Glob/Read로 확인한다. 미존재 시 즉시 블로킹 지시 추가 후 재호출한다.
|
||||
[선택] orchestrator dispatch 시 worker spawn이 예상되는 advisor에 대해 표준 블로킹 문구를 항상 append하는 것을 orchestrator 디스패치 규약으로 명문화.
|
||||
docs_target: docs/development/agent-team-protocol.md (bibimbap 번들) 또는 ATP 원본 리포
|
||||
|
||||
- issue: 'implementation-advisor의 테스트 실행 범위가 test-compile에 한정되어 full context 회귀(contextLoads)를 미탐지. verification 단계에서 blocker로 발견되어 §2.6 backward re-dispatch 비용 발생.'
|
||||
structural: true
|
||||
detail: |
|
||||
현상: implementation-advisor가 test-compile만 실행 → contextLoads 회귀 미탐지 → verification에서 blocker 판정 → §2.6 backward re-dispatch.
|
||||
근본 원인: 프로토콜/verification-strategies에 "implementation 단계에서 컨트롤러 추가 시 full test 실행 의무"가 없음. 컴파일 성공을 단위 검증 완료로 간주하는 암묵적 관행.
|
||||
proposed_fix: |
|
||||
verification-strategies.md 또는 implementation-advisor 호출 규약에 다음 추가:
|
||||
"컨트롤러 파일을 신규 추가하거나 기존 컨트롤러의 매퍼 의존을 변경하는 경우, implementation 단계에서도 ./mvnw -o test(full context 포함)를 실행한다. test-compile만으로는 Spring ApplicationContext 로드 실패를 탐지할 수 없다."
|
||||
단, 프로젝트별 test 실행 시간이 긴 경우 -Dtest=BibimbabApplicationTests만 별도 실행하는 것도 허용.
|
||||
docs_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md (신규 섹션 "컨트롤러 추가 시 검증 의무")
|
||||
|
||||
applied_changes: []
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
---
|
||||
phase: research
|
||||
agent: research-advisor
|
||||
agent_version: 2
|
||||
generated_at: 2026-06-18T11:05:00+09:00
|
||||
concerns:
|
||||
- "동결/권위 영역: db/schema.sql 의 game_comments/game_likes/games/users 는 '비권위 복원본'(타입 추론). game_reviews 신규 DDL 작성 시 인접 타입을 권위로 신뢰 금지 — recruit_posts(권위) 스타일을 따를 것."
|
||||
- "현존 게임 상세의 댓글/좋아요는 100% 클라이언트 localStorage 다(서버 미연동). W3-2 가 '분리'가 아니라 사실상 '서버 영속화 신설'을 포함함 — 요구사항 advisor 전제(이미 서버 댓글이 있다는 가정)가 있다면 깨짐."
|
||||
- "JSP 두 종 header 혼재: 활성 header 는 views/header.jsp(커스텀 세션·BibimbapCsrf). jsp/fragments/header.jspf 는 Spring Security taglib(sec:authorize, _csrf.parameterName) 기반 미사용 잔재 — 신규 작업에서 절대 참조 금지."
|
||||
concerns_checked: true
|
||||
source_confidence: high
|
||||
workers_spawned: 0
|
||||
---
|
||||
|
||||
# 조사 결과 — W3-2 댓글/리뷰 분리 코드 결합점
|
||||
|
||||
## 주제
|
||||
W3-2 (댓글/리뷰 분리) 설계·구현을 위한 11개 결합점 정밀 조사. 모든 발견은 file:line 근거. 본 산출물은 전부 프로젝트 내부 코드 1차 확인(확인됨). 외부 자료 미사용 → source_confidence: high.
|
||||
|
||||
> 조사 방식: parallel-explorer worker 미사용. 11개 포인트가 동일 소규모 코드베이스(~25 Java + 17 JSP)에 밀결합되어 있어 advisor 가 직접 Read/Grep 으로 전수 확인. 모든 항목 1차 출처 직접 확인됨.
|
||||
|
||||
---
|
||||
|
||||
## 포인트별 발견
|
||||
|
||||
### 포인트 1: 댓글 도메인 CRUD 전체 [확인됨]
|
||||
- **Mapper**: `mapper/GameCommentsMapper.java`
|
||||
- `GameCommentData getGameComment(long id)` — `:13-25` (SELECT, `WHERE id=#{id} AND is_delete IS NOT TRUE`)
|
||||
- `int addGameComment(GameCommentData)` — `:27-39` (INSERT game_id/nickname/content, `@Options(useGeneratedKeys=true, keyProperty="id")`)
|
||||
- `int updateGameComment(GameCommentData)` — `:41-51` (nickname/content/deleted_at 갱신 + `is_delete = CASE WHEN deletedAt IS NULL THEN false ELSE true END`)
|
||||
- **DELETE/list 메서드 없음**. game_id 별 목록 조회 메서드도 없음(soft-delete 일괄은 GamesMapper 에 있음 — 포인트 8).
|
||||
- **POJO**: `data/GameCommentData.java:5-61` — 필드: `Long id, Long gameId, String nickname, String content, OffsetDateTime createdAt, OffsetDateTime deletedAt`. (userId 없음 — 작성자 식별 컬럼 부재, nickname 만 있음)
|
||||
- **컬럼**(db/schema.sql:99-108, 비권위): `id bigint`, `game_id bigint NOT NULL REFERENCES games(id)`, `nickname varchar(100)`, `content text`, `created_at timestamptz DEFAULT now()`, `deleted_at timestamptz`, `is_delete boolean DEFAULT false`. (요청서가 언급한 6컬럼 + is_delete 7개 전부 일치)
|
||||
- **컨트롤러 엔드포인트**: **존재하지 않음**. `GameCommentsMapper` 를 주입/호출하는 컨트롤러 없음(rg 확인). 즉 서버측 댓글 작성/조회/삭제 HTTP API 가 전무.
|
||||
- **서비스 계층**: 없음(프로젝트 전체가 controller→mapper 직결 구조).
|
||||
- **작성/조회/삭제 흐름**: 현재 댓글은 서버 미연동. game-detail.jsp 가 localStorage 로만 처리(포인트 2). GameCommentsMapper 는 사실상 orphan(유일 사용처: 게임 삭제 cascade 의 `GamesMapper.softDeleteGameComments`, 포인트 8).
|
||||
|
||||
### 포인트 2: 게임 상세 페이지 [확인됨]
|
||||
- **핸들러**: `controller/api/GameController.java:103-129` `@GetMapping("/game/{id}") String gameDetail(long id, Model, HttpSession)`.
|
||||
- DB 게임 존재 시 `addGameModel(model, game, sessionUserId(session))` → view `"game-detail"` (`:104-109`).
|
||||
- DB miss 시 `GameCatalog` 정적 폴백(`:111-128`).
|
||||
- **model attribute**(`addGameModel` `:254-268`): `gameId, gameName, creator, likeCount, likeCountFormatted, creatorNote, gitUrl, webglUrl, webglFrameSrc, webglDeployPath, owner`. `owner = currentUserId != null && currentUserId.equals(game.getUserId())` (`:267`).
|
||||
- **game-detail.jsp 댓글/좋아요 실제 코드**(localStorage):
|
||||
- 좋아요 버튼 `#game-like-btn` HTML `:730-735`; 좋아요 JS 전부 localStorage(`LIKE_KEY='bibimbap-game-liked'`) — `getLikedMap :815-823`, `setLiked :825-832`, `localStorage.setItem :830`, 클릭 핸들러 `:904-908` (서버 POST/DELETE 없음).
|
||||
- 댓글 폼 `#game-comment-form` HTML `:790-801` (textarea name="comment" maxlength=1000, 작성자 입력란 없음).
|
||||
- 댓글 JS 전부 localStorage(`COMMENT_KEY='bibimbap-game-comments'`) — `getComments :911-920`, `saveComments :922-930`(`localStorage.setItem :928`), 렌더 `:938-992`, submit 핸들러 `:994-1005`(닉네임 하드코딩 `DEFAULT_NICK='익명' :936`, id=crypto.randomUUID).
|
||||
- 라인 근거: 요청서가 지목한 812(LIKE_KEY)/830(setItem)/913(getComments raw)/928(saveComments setItem) 전부 위치 확정.
|
||||
|
||||
### 포인트 3: 로그인/세션 인증 패턴 [확인됨]
|
||||
- **인증 방식**: Spring Security 아님. **커스텀 HttpSession attribute** 기반.
|
||||
- **로그인 컨트롤러**: `controller/api/UserController.java` — `@PostMapping("/login") :122-168`, `/signup :65-120`, `/logout :170-179`. 로그인 페이지(GET)는 `WebMvcController.mainView` switch `case "login" :75-80`.
|
||||
- **세션 저장**(`UserController.saveLoginSession :502-525`): `session.setAttribute("userId", user.getId())`(Long), 그 외 `id, displayName, email, avatarUrl, role, status, authProvider, authIdentityId, lastLoginAt`, 그리고 `account`(LinkedHashMap 복제본). 로그인 시 `request.changeSessionId() :160`(세션 고정 방어).
|
||||
- **현재 로그인 사용자 얻는 코드**(컨트롤러 공통 헬퍼, 3곳에 동일 복붙): `sessionUserId(HttpSession)` — `GameController:291-307`, `RecruitController:155-171`, `UserController:333-349`. `session.getAttribute("userId")` 를 Number/String→Long 변환, 없으면 null. (WebMvcController 는 `:118-127` 변형 — null 대신 `IllegalStateException` throw.)
|
||||
- **UserData 필드 전체**(`data/UserData.java:5-15`): `Long id, String displayName, String canonicalEmail, String avatarUrl, String role, String status, OffsetDateTime lastLoginAt, createdAt, updatedAt`. **id 타입 Long, role 타입 String**(기본값 "USER", UserController:43 `ROLE_USER="USER"`). 운영자 role 명칭은 코드상 미정의(USER 만 발급됨) — 포인트 6 참조.
|
||||
- **비로그인 처리**: 페이지는 `redirect:/login`(RecruitController:43-46, GameController:133-135, WebMvcController:82-83). AJAX/상태변경 API 는 `401 UNAUTHORIZED + {status,message}` JSON(GameController:57-59 등).
|
||||
|
||||
### 포인트 4: CSRF 현황 (설계 핵심 제약) [확인됨]
|
||||
- **spring-security 의존**: **없음**. pom.xml 의존성 = web, mybatis-spring-boot-starter, postgresql, tomcat-embed-jasper, lombok, starter-tomcat(provided), starter-test(test) (`pom.xml:53-87`). starter-security 부재.
|
||||
- **SecurityConfig / SecurityFilterChain / @EnableWebSecurity**: **클래스 없음**(rg 확인 0건).
|
||||
- **CSRF 인프라**: **커스텀 자체 구현 존재** — `security/CsrfTokens.java`:
|
||||
- `SESSION_ATTRIBUTE="csrfToken" :12`, `HEADER_NAME="X-CSRF-Token" :13`.
|
||||
- `getOrCreate(HttpSession) :20-33`(세션에 토큰 발급/재사용, Base64 32바이트).
|
||||
- `isValid(HttpServletRequest) :35-49` — 헤더 `X-CSRF-Token` 우선, 없으면 파라미터 `_csrf`(`:46`). 세션 토큰과 `.equals` 비교.
|
||||
- `errorBody() :51-56` → `{status:403, message:"요청 보안 토큰이 유효하지 않습니다."}`.
|
||||
- **기존 POST/AJAX 의 토큰 전달 방식**:
|
||||
- **뷰 노출**: `theme-init.jsp:5-7` 가 `<meta name="csrf-token" content="<%= HtmlUtils.htmlEscape(CsrfTokens.getOrCreate(session)) %>">` 출력. 모든 페이지가 theme-init.jsp 를 include 하므로 메타 토큰이 전역 제공됨.
|
||||
- **JS 헬퍼**: theme-init.jsp:19-31 `window.BibimbapCsrf` = `{token():meta 읽기, headers(extra):extra+{'X-CSRF-Token':token}}`.
|
||||
- **form hidden**: login.jsp:221 `<input type="hidden" name="_csrf" value="<%= csrfToken %>">`(signup.jsp 동일).
|
||||
- **AJAX 사용례**: login.jsp:283 `BibimbapCsrf.headers(...)`, game-detail.jsp:850(삭제) 동일 패턴.
|
||||
- **서버 검증례**: 모든 상태변경 핸들러 진입부 `if(!CsrfTokens.isValid(request)) return 403`(GameController:53/171/227, RecruitController:65, UserController:76/131/172/187/223). 테스트: `test/.../UserControllerCsrfTest.java`.
|
||||
- **설계 함의**: 신규 댓글/리뷰 POST/DELETE 는 **반드시 `CsrfTokens.isValid(request)` 게이트 + 클라이언트 `BibimbapCsrf.headers()` 사용**. 새 CSRF 인프라 신설 불필요(재사용).
|
||||
|
||||
### 포인트 5: DDL/스키마 적용 방식 [확인됨]
|
||||
- **부트스트랩**: `db/schema.sql` 가 전체 스키마. flyway/liquibase **없음**(docs/usage/local-setup.md:139 "flyway/liquibase 가 없다").
|
||||
- **적용법**:
|
||||
- Docker: db 컨테이너 최초 기동 시 `db/schema.sql` 이 `docker-entrypoint-initdb.d` 로 자동 1회 실행(dev 스키마 채움, live 는 빈 스키마). 재적용은 `docker compose down -v` 후 재기동(local-setup.md:149).
|
||||
- 호스트 로컬 PG: `psql -f db/schema.sql` 수동(local-setup.md:150).
|
||||
- **권위 패턴**: `docs/recruit-posts-ddl.sql`(권위) 가 신규 테이블 표준 스타일. `docs/security-hardening-ddl.sql` 는 기존 테이블에 인덱스/제약 추가용(중복 점검 SELECT → CREATE UNIQUE INDEX → DO $$ idempotent ALTER 패턴).
|
||||
- **DbUpdateQueryGenerator**: 테스트 `test/.../DbUpdateQueryGeneratorTest.java` + `test/db/dev-to-live-update.sql` 존재(surefire 에서 제외됨, pom.xml:130-133). dev→live 스키마 동기화 SQL 생성 용도로 보임 — **마이그레이션 자동화 도구 아님**(테스트성).
|
||||
- **신규 테이블(game_reviews) 추가 절차**: ① `db/schema.sql` 에 `SET search_path TO dev` 블록 내 CREATE TABLE 추가, ② 권위 DDL 파일을 docs/ 에 별도 작성(recruit-posts-ddl.sql 선례), ③ 기존 DB 적용용 idempotent ALTER 스크립트(security-hardening-ddl.sql 선례). 파일 위치: `db/schema.sql` + `docs/*-ddl.sql`.
|
||||
|
||||
### 포인트 6: 유사 게시판 패턴 — RecruitController (핵심 레퍼런스) [확인됨]
|
||||
- `controller/RecruitController.java` + `mapper/RecruitPostsMapper.java`.
|
||||
- **구현된 것**: list(`/recruit` GET → JSP recruit-list `:35-39`), form(`/recruit/new` GET, 비로그인 redirect `:41-47`), create(`/recruit/new` POST `:49-143`), detail(`/recruit/{id}` GET → JSP recruit-detail `:145-153`).
|
||||
- **create 패턴(댓글/리뷰가 그대로 따를 표준)**:
|
||||
1. `@Transactional` (`:50`)
|
||||
2. CSRF 우선 검증 `if(!CsrfTokens.isValid(request)) 403` (`:65-67`)
|
||||
3. 로그인 검증 `userId = sessionUserId(session); if null → 401` (`:68-71`)
|
||||
4. trimToNull/trimToEmpty 정규화 + 길이/허용값(Set.contains) 검증, 위반 시 `400 BAD_REQUEST` (`:73-114`)
|
||||
5. data POJO 세팅 후 mapper.add, 생성 id null 체크 → `500` (`:132-135`)
|
||||
6. 성공 응답 = **JSON** `{status:200, message, recruitPostId, location:"/recruit/{id}"}` (`:137-142`)
|
||||
- **응답 형식**: 상태변경(POST/DELETE)은 `ResponseEntity<Map<String,Object>>` JSON. 조회(GET)는 view name String(JSP). redirect 는 비로그인 폼 접근시만.
|
||||
- **작성자 권한 체크 / 운영자 예외**: **RecruitController 에는 update/delete 가 아예 없음** → 작성자 권한 체크·운영자 예외 선례는 RecruitController 에 없다. **권한 체크 표준 선례는 GameController** 다: `if(!userId.equals(existing.getUserId())) return 403 "작성자만 수정/삭제할 수 있습니다"` (GameController:183-185 수정, :239-241 삭제). **운영자(admin) 예외 분기는 코드 전체에 없음**(role 비교 분기 부재). → W3-2 가 운영자 삭제를 요구하면 신설 영역(role="ADMIN" 등 명칭도 미정의, 포인트 3).
|
||||
- **RecruitPostsMapper 메서드**: `getRecruitPost :15-42`(users JOIN, is_delete/visible 필터), `getVisibleRecruitPosts :44-72`, `addRecruitPost :74-108`(generatedKeys), `nextSortOrder :110-115`. update/delete 매퍼 없음.
|
||||
|
||||
### 포인트 7: 좋아요 패턴 (참고) [확인됨]
|
||||
- `mapper/GameLikesMapper.java`: `getGameLike :13-22`, `addGameLike :24-34`(generatedKeys), `updateGameLike :36-43`. **삭제/중복방지/카운트 매퍼 없음**.
|
||||
- `data/GameLikeData.java:5-43`: `Long id, Long gameId, String userKey, OffsetDateTime createdAt`. (좋아요 주체 식별이 `userKey` String — userId FK 아님)
|
||||
- **컬럼**(db/schema.sql:114-122, 비권위): `id, game_id NOT NULL FK, user_key varchar(200) NOT NULL, created_at`. **is_delete 없음**(hard delete 설계, schema.sql:112 주석 명시).
|
||||
- **영속화/중복방지 현황**: GameLikesMapper 를 호출하는 컨트롤러 **없음**(rg 0건). 좋아요도 댓글과 동일하게 서버 미연동 → game-detail.jsp localStorage(`LIKE_KEY`)만. 중복방지는 DB UNIQUE 제약 없음(schema 상). 즉 likes 도 orphan mapper.
|
||||
- **카운트 출처**: `games.like_count`(GamesMapper.getGame:27 `like_count AS likeCount`) 정적 컬럼 — 실시간 game_likes COUNT 아님.
|
||||
|
||||
### 포인트 8: 게임 삭제 cascade [확인됨]
|
||||
- `GameController.deleteGame :222-252` (`@DeleteMapping("/game/{id}")`, `@Transactional`).
|
||||
- 순서(`:243-245`): `gamesMapper.softDeleteGameComments(id)` → `gamesMapper.deleteGameLikes(id)` → `gamesMapper.softDeleteGame(id)`.
|
||||
- **cascade 매퍼**(GamesMapper):
|
||||
- `softDeleteGameComments(long gameId) :165-173` — `UPDATE game_comments SET is_delete=true, deleted_at=COALESCE(deleted_at,now()) WHERE game_id=#{gameId} AND is_delete IS NOT TRUE` (**soft delete**)
|
||||
- `deleteGameLikes(long gameId) :175-179` — `DELETE FROM game_likes WHERE game_id=#{gameId}` (**hard delete**)
|
||||
- `softDeleteGame(long id) :181-189` — `UPDATE games SET is_delete=true, updated_at=now()`
|
||||
- **game_reviews 동일 정리 패턴**: review 가 soft-delete(is_delete 컬럼) 채택 시 → `softDeleteGameReviews(gameId)` (comments 미러). hard-delete 채택 시 → `deleteGameReviews(gameId)` (likes 미러). 그리고 `deleteGame :243-245` 에 호출 한 줄 추가가 정확한 결합점.
|
||||
|
||||
### 포인트 9: JSP escape 패턴 [확인됨]
|
||||
- **서버 렌더 출력**: `org.springframework.web.util.HtmlUtils.htmlEscape(...)` 를 scriptlet `<%= %>` 안에서 사용이 지배적 패턴.
|
||||
- recruit-detail.jsp: 전 사용자 데이터 `<%= HtmlUtils.htmlEscape(role/projectName/summary/contact/description...) %>` (`:219-288`).
|
||||
- game-detail.jsp: `creatorNoteValue/gameNameValue/creatorValue` 등은 핸들러 model 값을 `HtmlUtils.htmlEscape` 로 미리 감싼 변수로 출력(`:9` likeCountFormattedValue 등, 본문 `:703/721/771`).
|
||||
- header.jsp:11 avatarUrl `HtmlUtils.htmlEscape`.
|
||||
- **JSTL escape**: `<c:out value='${q}'/>`(header.jspf:7 — 단 header.jspf 는 미사용 잔재). 활성 뷰는 scriptlet 방식.
|
||||
- **클라이언트 렌더**: game-detail.jsp 댓글 렌더는 **`textContent` 사용**(`:951 av.textContent`, `:958 nickEl.textContent`, `:969 p.textContent`) — innerHTML 은 초기화용 `listEl.innerHTML=''`(`:942`)만. → 신규 서버연동 댓글/리뷰 클라이언트 렌더도 textContent 규약 유지 필수(CLAUDE.md 보안원칙 일치).
|
||||
- **신규 review JSP 출력 규약**: 사용자 입력(평점 코멘트 등) 서버 렌더 시 `HtmlUtils.htmlEscape`, 클라이언트 동적 삽입 시 `textContent`.
|
||||
|
||||
### 포인트 10: MyBatis annotation 규약 [확인됨]
|
||||
- annotation-only(XML 매퍼 없음). `@Mapper` 인터페이스 + `@Select/@Insert/@Update/@Delete` 인라인 SQL(Java text block `"""`).
|
||||
- **generatedKey**: `@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")` (GameCommentsMapper:38, RecruitPostsMapper:107, GamesMapper:138, GameLikesMapper:33).
|
||||
- **created_at**: INSERT 문에 미포함 → **DB DEFAULT now() 의존**(GameCommentsMapper:27-37 은 game_id/nickname/content 만 INSERT). updated_at 갱신은 UPDATE 문에서 `updated_at=now()` 명시(GamesMapper:159).
|
||||
- **바인딩**: 전부 `#{}` (예: `#{id}`, `#{gameId}`). `${}` 동적 치환 사용처 없음(검색 0건). 다중 인자는 `@Param("name")`(GamesMapper:91/115, RecruitPostsMapper:42). 컬럼 alias 는 `snake_case AS camelCase` 로 POJO 매핑.
|
||||
- **soft-delete 조회 규약**: 모든 SELECT 가 `is_delete IS NOT TRUE` 필터 + users JOIN 시 `u.is_delete IS NOT TRUE`.
|
||||
|
||||
### 포인트 11: DB 종류·문법 [확인됨]
|
||||
- **DB**: PostgreSQL. driver `org.postgresql.Driver`, url `jdbc:postgresql://localhost:5433/bibimbap?currentSchema=dev` (src/main/resources/dev/db.properties). Docker: `jdbc:postgresql://db:5432/...?currentSchema=${APP_SCHEMA:-dev}` (docker-compose.yml:40). pom.xml:64-67 postgresql runtime.
|
||||
- **스키마 분리**: dev / live 두 PostgreSQL schema(search_path). schema.sql:21-24 `CREATE SCHEMA dev/live; SET search_path TO dev`.
|
||||
- **DDL 문법 관례**(db/schema.sql, recruit-posts-ddl.sql): SERIAL/BIGSERIAL **미사용** → 명시적 `CREATE SEQUENCE` + `bigint DEFAULT nextval('..._id_seq'::regclass)` + `ALTER SEQUENCE ... OWNED BY`. 타임스탬프 `timestamp with time zone`(=timestamptz, OffsetDateTime 매핑). boolean `is_delete DEFAULT false NOT NULL`. FK `bigint REFERENCES "table"("id")`. **ON DELETE CASCADE 미사용**(앱레벨 cascade, 포인트 8). CHECK 제약 사용례 recruit_posts(schema.sql:156-161). 인덱스 `CREATE INDEX IF NOT EXISTS ... WHERE`(partial index).
|
||||
- **id 타입**: 전 테이블 `bigint`(POJO Long). like_count/sort_order 만 `integer`.
|
||||
|
||||
---
|
||||
|
||||
## 종합 판단
|
||||
|
||||
### 핵심 상위 패턴
|
||||
- 프로젝트는 **controller→mapper 직결**(서비스 계층 없음), **커스텀 세션 인증 + 커스텀 CsrfTokens**(Spring Security 전무), **MyBatis annotation + 명시 시퀀스 PostgreSQL**, **soft-delete(is_delete) 규약**.
|
||||
- 상태변경 API 표준 시퀀스: `@Transactional` → CSRF 검증 → 로그인 검증 → 정규화/검증 → mapper → JSON 응답.
|
||||
|
||||
### 충돌·갭 (요구사항 전제 점검 — 프로토콜 충돌 시 조항)
|
||||
- **GAP-1 (중대)**: "댓글/리뷰 **분리**"라는 표현은 기존에 서버 댓글이 존재함을 함의하나, **실제 댓글·좋아요 모두 서버 미연동 localStorage** 다. GameCommentsMapper/GameLikesMapper 는 orphan(삭제 cascade 외 미사용). 따라서 W3-2 는 분리 이전에 **댓글 서버 영속화 + 리뷰 신설** 두 가지를 포함. 요구사항 advisor 가 '댓글은 이미 서버에 있고 리뷰만 떼낸다'를 전제했다면 깨짐.
|
||||
- **GAP-2**: game_comments 에 작성자 식별 컬럼 없음(nickname 만, userId FK 부재). 로그인 사용자 귀속·작성자 권한 삭제를 원하면 user_id 컬럼 신설 필요.
|
||||
- **GAP-3**: 운영자(admin) 예외 로직이 코드 전체에 부재. role 은 "USER"만 발급되며 ADMIN 명칭·분기 미정의. 운영자 댓글/리뷰 삭제 요구 시 전부 신설.
|
||||
- **GAP-4**: game_reviews 테이블·POJO·매퍼·DDL **전무**(docs work-log 도 "review 테이블 없음" 명시). 평점 축(단일/다축) 미정 — work-log:C6 에서 W3-2 설계 종속으로 남겨둠.
|
||||
|
||||
### 권위 격상 전 검증 필요 항목
|
||||
- 없음(전 항목 1차 코드 확인). 단, **db/schema.sql 의 game_comments/game_likes/games/users 타입·길이·기본값은 schema.sql 자체가 '비권위 추론값'으로 선언**(주석 :4-11). 신규 game_reviews DDL 의 인접 타입을 이들에서 복사할 때는 recruit_posts(권위) 스타일을 기준으로 삼을 것. (source_confidence 는 '코드가 이렇게 되어있다'는 사실에 대해 high. '운영 DB 실제 타입'은 schema.sql 스스로 미확인 선언.)
|
||||
|
||||
---
|
||||
|
||||
## 설계 입력 요약 (a 재사용 / b 신설)
|
||||
|
||||
### (a) 재사용할 현존 패턴
|
||||
- **CSRF**: `CsrfTokens.isValid(request)` 게이트 + 뷰 `BibimbapCsrf.headers()`/메타 `csrf-token` + hidden `_csrf`. (신규 인프라 불필요)
|
||||
- **인증**: `sessionUserId(HttpSession)` 헬퍼(session attr "userId"→Long), 비로그인 페이지 redirect:/login, API 401 JSON.
|
||||
- **컨트롤러 골격**: RecruitController.createRecruitPost(:49-143) 시퀀스 + GameController 의 작성자 권한 체크(:183-185/239-241)를 합성.
|
||||
- **응답 형식**: `ResponseEntity<Map<String,Object>>` `{status,message,...,location}` (조회는 JSP view name).
|
||||
- **MyBatis**: `@Mapper` + 인라인 SQL + `@Options(useGeneratedKeys)` + `#{}` + `is_delete IS NOT TRUE` 필터 + `snake AS camel`.
|
||||
- **삭제 cascade**: GameController.deleteGame(:243-245) 에 review 정리 한 줄 추가.
|
||||
- **DDL 스타일**: recruit_posts(권위) — CREATE SEQUENCE + bigint nextval + timestamptz + partial index, ON DELETE CASCADE 미사용.
|
||||
- **JSP escape**: 서버 `HtmlUtils.htmlEscape`, 클라이언트 `textContent`.
|
||||
- **스키마 적용**: db/schema.sql + docs/*-ddl.sql(권위) + idempotent ALTER(security-hardening 선례).
|
||||
|
||||
### (b) 신설해야 할 것
|
||||
- **게임 댓글 서버 API**: GameComment(s) 컨트롤러 신설(list GET + create POST + delete) — 현재 전무. localStorage JS(game-detail.jsp:807-1009) → fetch 기반으로 교체.
|
||||
- **GameCommentsMapper 확장**: game_id 별 목록 SELECT, (soft)delete 메서드 — 현재 없음.
|
||||
- **game_reviews 도메인 전체**: 테이블 DDL(db/schema.sql + docs/game-reviews-ddl.sql), GameReviewData POJO, GameReviewsMapper, 리뷰 컨트롤러, JSP 영역, 삭제 cascade 정리 메서드.
|
||||
- **작성자 식별**: 댓글/리뷰에 user_id 컬럼(로그인 귀속·작성자 삭제 원할 경우) — game_comments 현재 nickname 만.
|
||||
- **(요구 시) 운영자 예외**: role 명칭 정의 + 권한 분기 — 현재 전무.
|
||||
- **(설계 종속) 평점 모델**: 단일 평점 vs 다축 — W3-2 설계에서 확정 필요(work-log C6).
|
||||
|
||||
---
|
||||
|
||||
## 미해결
|
||||
- **운영 DB 실제 스키마**: game_comments/game_likes/games 타입·제약은 schema.sql 가 비권위 선언. pg_dump 대조 전까지 game_reviews 인접 타입을 운영값으로 확신 불가(설계는 recruit_posts 권위 스타일로 진행 권장).
|
||||
- **평점 데이터 모델**: 단일 점수/다축/리뷰수 정렬 의미 — W3-2 요구·설계 결정 사항(조사 범위 밖).
|
||||
- **운영자 role 명칭**: ADMIN 등 명칭과 부여 경로 미정(코드상 USER 만 존재).
|
||||
- **댓글↔리뷰 '분리'의 정확한 의미**: 별도 테이블 2개인지, 단일 테이블 type 컬럼인지 — 요구사항 확정 필요(본 조사는 결합점만 제공, 설계 판단은 design-advisor 몫).
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
---
|
||||
phase: verification
|
||||
agent: verification-advisor
|
||||
agent_version: 1
|
||||
generated_at: "2026-06-18T11:51:35+09:00"
|
||||
concerns:
|
||||
- "BibimbapApplicationTests.contextLoads ERROR: GameCommentsMapper bean not found in full Spring context (DataSource/MyBatis autoconfigured but MyBatis mapper scan 미적용 상태). 이 테스트는 신규 도입된 mapper 가 MyBatis scan 대상에서 누락된 경우 발생하는 회귀. blocker 판정."
|
||||
- "L3 skipped: no-external-dependency (외부 서비스 불필요, 런타임 톰캣+DB 미기동 단위 범위 밖)"
|
||||
concerns_checked: true
|
||||
---
|
||||
|
||||
# 검증 결과
|
||||
|
||||
## Acceptance Criteria (입력 받은 그대로 인용)
|
||||
|
||||
1. **댓글 CRUD**: 로그인 사용자 작성/수정/삭제 가능 — GameCommentControllerTest 의 작성·수정·삭제 케이스 통과 여부.
|
||||
2. **비작성자 불가 + 운영자 예외**: 타 사용자(USER) 수정/삭제 403, ADMIN role 200 — 테스트 케이스 통과 여부.
|
||||
3. **리뷰 게임당 1회**: 동일 user+game 두 번째 작성 409 — 테스트.
|
||||
4. **리뷰 수정 "수정됨" + 이력 보존**: 수정 후 edited=true(updated_at>created_at), created_at 불변 — 테스트.
|
||||
5. **댓글 201자 거부 / 200자 경계 허용**: 201자 400, 200자 통과 — 테스트.
|
||||
6. **rating 범위**: 0/6 거부 400, 1/5 허용 — 테스트.
|
||||
7. **CSRF 없는 상태변경 실패**: mutation 6개 CSRF 토큰 부재 시 403 — 테스트 + AGG-3 게이트 수.
|
||||
8. **XSS payload 미실행**: 컨트롤러는 content/body raw 반환(이중escape 안 함), 클라이언트는 textContent 렌더 → 실제 미실행은 L3 런타임. 단위 가능 범위(컨트롤러 raw 저장/반환)만 판정하고 브라우저 미실행은 L3-defer.
|
||||
9. **영속성(새로고침/브라우저변경)**: DB 영속 — 서버 저장 자체는 단위(addGameComment/addGameReview) 통과로 부분 확인, 실제 브라우저 재방문 영속은 L3-defer.
|
||||
10. **게임 삭제 cascade**: deleteGame 후 game_reviews soft-delete — 테스트(GamesMapper softDeleteGameReviews mock verify) 또는 grep 으로 deleteGame 에 호출 존재 확인.
|
||||
|
||||
---
|
||||
|
||||
## 실행된 전략
|
||||
|
||||
| id | cmd | exit | severity | 결과 |
|
||||
|---|---|---|---|---|
|
||||
| L1-a (mvn test) | `./mvnw -o test` | 1 (BUILD FAILURE) | blocker | FAIL |
|
||||
| L1-b AGG-1 GameCommentController | `grep -cE '@(Get|Post|Put|Delete)Mapping' ...GameCommentController.java` | 0 | blocker | PASS (실제 4, 기대 4) |
|
||||
| L1-b AGG-1 GameReviewController | `grep -cE '@(Get|Post|Put|Delete)Mapping' ...GameReviewController.java` | 0 | blocker | PASS (실제 5, 기대 5) |
|
||||
| L1-b AGG-3 CSRF GameComment | `grep -c 'CsrfTokens.isValid' ...GameCommentController.java` | 0 | blocker | PASS (실제 3, 기대 3) |
|
||||
| L1-b AGG-3 CSRF GameReview | `grep -c 'CsrfTokens.isValid' ...GameReviewController.java` | 0 | blocker | PASS (실제 3, 기대 3) |
|
||||
| L1-b AGG-3 합산 | CSRF 게이트 총합 | — | blocker | PASS (합계 6, 기대 6) |
|
||||
| L1-b AGG-2 game_reviews DDL | `grep -c 'CREATE TABLE IF NOT EXISTS "game_reviews"' db/schema.sql` | 0 | blocker | PASS (실제 1, 기대 >=1) |
|
||||
| L1-b AGG-2 user_id 존재 | `grep -c 'user_id' db/schema.sql` | 0 | blocker | PASS (실제 12, 기대 >=1) |
|
||||
| L1-b AGG-2 docs/game-reviews-ddl.sql 존재 | `ls docs/game-reviews-ddl.sql` | 0 | blocker | PASS (파일 존재) |
|
||||
| L1-b AGG-2 멱등 ALTER | `grep -c 'IF NOT EXISTS' docs/game-reviews-ddl.sql` | 0 | blocker | PASS (실제 10, 기대 >=1) |
|
||||
| L2 | 외부 서비스 의존 없음 | — | — | skipped: no-external-dependency |
|
||||
| L3 | 런타임 톰캣+DB 미기동 | — | — | needs_user_verification |
|
||||
|
||||
### L1-a 분해 결과
|
||||
|
||||
| 단계 | 결과 |
|
||||
|---|---|
|
||||
| L1 컴파일 | pass (컴파일 성공, 30개 신규+기존 테스트 실행 도달) |
|
||||
| GameCommentControllerTest (12건) | pass (Failures: 0, Errors: 0) |
|
||||
| GameReviewControllerTest (13건) | pass (Failures: 0, Errors: 0) |
|
||||
| UserControllerCsrfTest (5건) | pass (Failures: 0, Errors: 0) |
|
||||
| BibimbapApplicationTests (1건) | **FAIL** (Errors: 1 — contextLoads) |
|
||||
| L2 contract | skipped: no-external-dependency |
|
||||
| L3 런타임 | needs_user_verification |
|
||||
| 로그 스캔 | warn: BibimbapApplicationTests ERROR 1건 (APPLICATION FAILED TO START) |
|
||||
|
||||
---
|
||||
|
||||
## 실패 상세
|
||||
|
||||
### BibimbapApplicationTests.contextLoads — blocker
|
||||
|
||||
- **테스트 클래스**: `com.pandoli365.bibimbap.BibimbapApplicationTests`
|
||||
- **실패 지점**: `BibimbapApplicationTests.java` contextLoads() (ApplicationContext 로드 실패)
|
||||
- **원인 체인**:
|
||||
1. `UnsatisfiedDependencyException`: bean `gameCommentController` 생성 실패
|
||||
2. `NoSuchBeanDefinitionException`: `com.pandoli365.bibimbap.mapper.GameCommentsMapper` — 등록 bean 없음
|
||||
3. `BibimbapApplicationTests`는 DataSource/MyBatis autoconfigure 를 exclude 하지만 MyBatis mapper scan 이 동작하지 않아 `GameCommentsMapper` bean 이 ApplicationContext 에 등록되지 않음
|
||||
- **재현 명령**:
|
||||
```
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
||||
cd /Users/wemadeplay/workspace/stz/bibimbap
|
||||
./mvnw -o test -Dtest=BibimbapApplicationTests 2>&1
|
||||
```
|
||||
- **분리 실행 (신규 테스트만 통과 확인용)**:
|
||||
```
|
||||
./mvnw -o test -Dtest="GameCommentControllerTest,GameReviewControllerTest,UserControllerCsrfTest" 2>&1 | grep -E '(Tests run|BUILD)'
|
||||
```
|
||||
- **회귀 여부**: 이 테스트는 기존부터 존재. `GameCommentsMapper` 신규 추가 후 이 테스트가 전체 컨텍스트를 로드하면서 MyBatis scan 범위에서 누락된 mapper 를 찾지 못하는 것. 기존 테스트가 신규 구현으로 인해 실패하므로 **회귀 blocker**.
|
||||
|
||||
---
|
||||
|
||||
## AGG 집합 전수 결과 상세
|
||||
|
||||
### AGG-1 엔드포인트 매핑 수
|
||||
| 컨트롤러 | 실제 count | 기대 | 판정 |
|
||||
|---|---|---|---|
|
||||
| GameCommentController | 4 | 4 | PASS |
|
||||
| GameReviewController | 5 | 5 | PASS |
|
||||
|
||||
### AGG-3 CSRF 게이트 수
|
||||
| 컨트롤러 | 실제 count | 기대 | 판정 |
|
||||
|---|---|---|---|
|
||||
| GameCommentController | 3 | 3 | PASS |
|
||||
| GameReviewController | 3 | 3 | PASS |
|
||||
| **합산** | **6** | **6** | **PASS** |
|
||||
|
||||
### AGG-2 DDL 3종
|
||||
| 항목 | 실제 | 기대 | 판정 |
|
||||
|---|---|---|---|
|
||||
| `CREATE TABLE IF NOT EXISTS "game_reviews"` in db/schema.sql | 1 | >=1 | PASS |
|
||||
| `user_id` in db/schema.sql | 12 | >=1 | PASS |
|
||||
| docs/game-reviews-ddl.sql 파일 존재 | 존재 | 존재 | PASS |
|
||||
| `IF NOT EXISTS` in docs/game-reviews-ddl.sql | 10 | >=1 | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance 매칭
|
||||
|
||||
| criterion | 매칭 전략 | 판정 |
|
||||
|---|---|---|
|
||||
| 1. 댓글 CRUD | GameCommentControllerTest 12건 all pass | PASS |
|
||||
| 2. 비작성자 403 / ADMIN 200 | GameCommentControllerTest 포함 (12건 all pass) | PASS |
|
||||
| 3. 리뷰 게임당 1회 409 | GameReviewControllerTest 13건 all pass | PASS |
|
||||
| 4. 리뷰 수정 edited=true + created_at 불변 | GameReviewControllerTest 13건 all pass | PASS |
|
||||
| 5. 댓글 201자 400 / 200자 허용 | GameCommentControllerTest 12건 all pass | PASS |
|
||||
| 6. rating 0/6→400 / 1/5→200 | GameReviewControllerTest 13건 all pass | PASS |
|
||||
| 7. CSRF 없는 상태변경 403 (mutation 6개) | AGG-3 게이트 6개 PASS + UserControllerCsrfTest 5건 all pass | PASS |
|
||||
| 8. XSS payload — 컨트롤러 raw 반환(단위 범위) | 단위 테스트 통과로 부분 확인 / 브라우저 미실행 | PASS(단위) + L3-defer(브라우저 렌더) |
|
||||
| 9. 영속성 — 서버 저장 단위 범위 | 단위 테스트 addGameComment/addGameReview 통과 | PASS(단위) + L3-defer(브라우저 재방문) |
|
||||
| 10. 게임 삭제 cascade | GameReviewControllerTest 포함 확인 (13건 all pass) | PASS |
|
||||
| **회귀 (BibimbapApplicationTests.contextLoads)** | L1-a 전체 테스트 실행 | **FAIL — blocker** |
|
||||
|
||||
---
|
||||
|
||||
## 종합 판정
|
||||
|
||||
```
|
||||
overall: fail
|
||||
rollback_signal: partial
|
||||
```
|
||||
|
||||
**rollback_signal: partial** 근거: `GameCommentsMapper` bean 이 ApplicationContext scan 에서 누락된 것. 컨트롤러 구현·mapper 인터페이스 자체는 존재하지만 MyBatis mapper 등록 설정(annotation 또는 `@MapperScan` 범위)이 누락 또는 불완전. 신규 파일 추가 범위의 설정 보완으로 수정 가능하며 전체 revert 보다는 설정 파일 부분 수정이 적합하다.
|
||||
|
||||
---
|
||||
|
||||
## verified_by_me (L1 + AGG 통과 항목)
|
||||
|
||||
- GameCommentControllerTest 12건 all pass (Failures: 0, Errors: 0)
|
||||
- GameReviewControllerTest 13건 all pass (Failures: 0, Errors: 0)
|
||||
- UserControllerCsrfTest 5건 all pass (Failures: 0, Errors: 0)
|
||||
- AGG-1: 엔드포인트 매핑 수 정확 (Comment 4, Review 5)
|
||||
- AGG-3: CSRF 게이트 합산 6개 정확 (Comment 3 + Review 3)
|
||||
- AGG-2: DDL 3종 (schema.sql game_reviews 테이블 존재, user_id 존재, docs/game-reviews-ddl.sql 존재, 멱등 ALTER 존재)
|
||||
- AC 1~10 단위 범위 모두 PASS (회귀 1건 제외)
|
||||
|
||||
## needs_user_verification (L3 스모크)
|
||||
|
||||
수동으로 확인이 필요한 항목:
|
||||
|
||||
1. **XSS 브라우저 렌더**: 톰캣+DB 기동 후 `<script>alert(1)</script>` 를 댓글/리뷰 body 에 입력 → 화면에 alert 팝업이 뜨지 않고 텍스트로 표시되는지 확인. 확인 경로: `textContent` 바인딩 JSP/JS 코드 동작.
|
||||
2. **영속성 브라우저 재방문**: 댓글/리뷰 작성 후 브라우저 탭 닫고 재방문 → 데이터가 유지되는지 확인.
|
||||
3. **실제 게임 삭제 cascade**: 게임 삭제 API 호출 후 DB에서 `game_reviews.deleted_at` 설정 여부 확인 (`SELECT * FROM game_reviews WHERE game_id = ?`).
|
||||
|
||||
## blocker (수정 필요)
|
||||
|
||||
**BibimbapApplicationTests.contextLoads — 회귀 FAIL**
|
||||
|
||||
- 실패: `NoSuchBeanDefinitionException` for `com.pandoli365.bibimbap.mapper.GameCommentsMapper`
|
||||
- `BibimbapApplicationTests` 가 전체 Spring ApplicationContext 로드 시 `GameCommentsMapper` bean 을 찾지 못함.
|
||||
- 재현: `./mvnw -o test -Dtest=BibimbapApplicationTests 2>&1`
|
||||
- 이 테스트는 신규 mapper 추가 이전부터 존재하던 기존 테스트. 신규 구현 이후 실패 → 회귀.
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
---
|
||||
schema_version: 2
|
||||
sid: 20260618-121419
|
||||
started_at: 2026-06-18T12:14:19+09:00
|
||||
ended_at: 2026-06-18T13:05:00+09:00
|
||||
applied_changes:
|
||||
- "Serena memory 기록: bibimbap-auth-pbkdf2-not-bcrypt (수용, signal=observation)"
|
||||
- "Serena memory 기록: bibimbap-dev-seed-blueprint (수용, FK순서+active-unique 통합)"
|
||||
- "Serena memory 기록: java-crypto-verify-via-jdk-container-when-no-host-jdk (수용, signal=positive)"
|
||||
- "docs_sync 보류: CLAUDE.md / verification-strategies.md 반영은 tracked 문서 편집+미커밋이라 사용자 승인 대기"
|
||||
user_request: |
|
||||
새로 추가한 기능(게임 리뷰/댓글, W3-2) 테스트하기 위한 더미 상황이 필요하다:
|
||||
1. 게임 (dummy game)
|
||||
2. 계정 (dummy account, 로그인 가능)
|
||||
3. 이미 등록되어 있는 더미 리뷰
|
||||
status: done
|
||||
---
|
||||
|
||||
# Summary
|
||||
W3-2 게임 리뷰/댓글 기능 테스트용 더미 상황을 dev 스키마에 구축. 멱등 seed SQL
|
||||
(`db/seed-dev.sql`) + 정리 SQL(`db/seed-dev-teardown.sql`) 작성 후 가동중 컨테이너
|
||||
(bibimbap-db)에 적용. 구성: 로그인 가능 테스트 계정 1개(tester@bibimbap.local /
|
||||
test1234!), 표시되는 더미 게임 1개(테스트 계정 소유), 별점 다양한 더미 리뷰 5개
|
||||
(각자 다른 더미 유저 — game당 user 1리뷰 unique 제약 충족), 보너스 댓글 3개.
|
||||
로그인 계정은 해당 게임에 리뷰가 없어 "직접 작성" 테스트도 가능.
|
||||
|
||||
핵심 제약 발견: 로그인 password_hash 는 BCrypt 가 아닌 자체 PBKDF2
|
||||
(`pbkdf2_sha256$210000$<saltB64>$<hashB64>`, PBKDF2WithHmacSHA256/256bit/16B salt).
|
||||
저장 해시가 실제 Java `UserController.verifyPassword` 경로로 검증됨을 JDK 컨테이너에서
|
||||
교차 실행 확인(verify('test1234!')=true, 오답=false) → 로그인 동작 보장.
|
||||
|
||||
# Invocations
|
||||
- research-advisor: 인증 seed 경로 + 리뷰/댓글 유저귀속 + seed 적용경로 조사 (완료)
|
||||
- artifact: research/index.md (file:line 근거 포함). 결론 = orchestrator 독립 조사와 일치.
|
||||
- graph-refresh-checker: seed SQL 추가 후 graph staleness 판정 (running)
|
||||
|
||||
# Decisions
|
||||
- 적용 방식: 재사용 SQL 파일(db/seed-dev.sql) + 즉시 적용. (사용자 선택, 추천값)
|
||||
- 로그인 자격: 기본값 tester@bibimbap.local / test1234! / 닉네임 '테스터'. (사용자 선택)
|
||||
- 규모: 게임 1 + 리뷰 5(별점 5/4/3/5/2). (사용자 선택)
|
||||
- 설계: 리뷰어는 별도 더미 유저 5명(unique 제약 game당 user 1리뷰 때문). 로그인 계정은
|
||||
그 게임에 리뷰 없음(작성 테스트 가능). 게임 소유자 = 로그인 계정(getGamesByUserId 도 테스트).
|
||||
- design/implementation-advisor 스킵: 스키마·계약·컬럼값 완비된 단일 seed SQL → orchestrator 직접 구현.
|
||||
- 댓글 3개 보너스 포함: 신규 기능이 리뷰/댓글 동시 배치라 같은 더미 상황에 포함이 합리적.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
- advisor: requirements-advisor
|
||||
decision: skip
|
||||
rationale: '요청 3항목 명확, seed 위치/자격/규모는 orchestrator 가 plan-gate(AskUserQuestion)로 직접 수렴'
|
||||
checked_at: 2026-06-18T12:14:30+09:00
|
||||
- advisor: research-advisor
|
||||
decision: call
|
||||
rationale: '로그인 가능 계정 seed 위한 password_hash 알고리즘·provider·세션귀속이 미문서화 미지수'
|
||||
checked_at: 2026-06-18T12:15:00+09:00
|
||||
- advisor: design-advisor
|
||||
decision: skip
|
||||
rationale: 'research 로 컬럼·계약·제약 전부 확정 → 설계 오픈질문 0, orchestrator 직접 설계'
|
||||
checked_at: 2026-06-18T12:30:00+09:00
|
||||
- advisor: implementation-advisor
|
||||
decision: skip
|
||||
rationale: '단일 SQL 파일 산출, 파일 충돌 없음 → worker 분산 불요'
|
||||
checked_at: 2026-06-18T12:31:00+09:00
|
||||
- advisor: verification-advisor
|
||||
decision: skip-direct
|
||||
rationale: 'orchestrator 가 매퍼 동등쿼리 + JDK 교차해시검증 + 멱등 재실행을 직접 실행해 AC 충족 확인'
|
||||
checked_at: 2026-06-18T12:45:00+09:00
|
||||
|
||||
# user_signals
|
||||
positive: []
|
||||
negative: []
|
||||
|
||||
# verified_by_me
|
||||
- L1 (데이터 무결성 / 매퍼 동등 쿼리, 실 DB 실행):
|
||||
- login identity 조회(provider=email): tester 1건 + password_hash 존재 → pass
|
||||
- tester users.status=ACTIVE (로그인 필수 조건) → pass
|
||||
- getVisibleGames 동등: '테스트 게임 (더미)' is_visible=true 포함 → pass
|
||||
- listGameReviews(game): 5건, 최신순 정렬 + authorName JOIN 정상 → pass
|
||||
- getActiveReviewByGameAndUser(game, tester): 0건 → 본인 리뷰 작성 테스트 가능 → pass
|
||||
- listGameComments(game): 3건 → pass
|
||||
- L2 (교차구현 해시 검증, 실제 Java verifyPassword 코드 경로, eclipse-temurin:21-jdk):
|
||||
- verify('test1234!', 저장해시) = true → 로그인 성공 보장
|
||||
- verify('wrongpass', 저장해시) = false → 음성 케이스 정상
|
||||
- 멱등성: seed-dev.sql 재실행 후 카운트 불변(users6/games1/reviews5/comments3) → pass
|
||||
- 로그 스캔: psql ON_ERROR_STOP=1 무에러, NOTICE 정상 → clean
|
||||
|
||||
# needs_user_verification
|
||||
- 런타임 UI 스모크 1회 (호스트에 JDK 없음 + bibimbap-app 미가동이라 자동 불가):
|
||||
1. `docker compose up --build` (또는 호스트 실행) 으로 앱 기동
|
||||
2. tester@bibimbap.local / test1234! 로 로그인 → 성공 확인
|
||||
3. '테스트 게임 (더미)' 상세 진입 → 기존 리뷰 5건 + 댓글 3건 표시 확인
|
||||
4. 로그인 상태로 리뷰 1건 + 댓글 1건 신규 작성 → 정상 등록 확인
|
||||
|
||||
# graph_refresh
|
||||
- 판정: partial-stale (but 본 세션 변경은 fresh).
|
||||
- 본 세션 변경(db/seed-dev.sql, seed-dev-teardown.sql)은 src/docs graph scope 대상경로
|
||||
밖 데이터 픽스처 → graph 영향 0, 재생성 불요(no-defer: 본 세션 무액션).
|
||||
- partial-stale 트리거는 **별도 작업단위**(W3-2 댓글/리뷰 기능 구현 — GameCommentController/
|
||||
GameReviewController/GameReviewsMapper/GameReviewData 등, 대부분 untracked). 해당 코드가
|
||||
미커밋 진행중이라 지금 /graphify src/ 재생성은 시기상조 → open_items 로 권고만 이월하지
|
||||
않고 "기능 커밋 시점 처리" 로 명시.
|
||||
|
||||
# post-session-debug (로그인 server error 후속)
|
||||
- 사용자 보고 "로그인 시 server error(500)". 진단 결과:
|
||||
- 원인 = **stale 앱 인스턴스**(pid 59109, 6/17 14:36 기동, 최근 auth 수정 이전 빌드).
|
||||
내 seed 가 tester 계정을 만들어 처음으로 "계정 존재" 상태 로그인 경로를 타며 표면화.
|
||||
- 조치: 사용자 동의(직접 캡처 요청) 하에 pid 59109 종료 → `JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||||
./mvnw -P dev spring-boot:run` 재기동(현재 코드 = target/classes 재컴파일, W3-2 컨트롤러 포함).
|
||||
로그: /tmp/bibimbap-app.log. 신규 pid 23318 (백그라운드).
|
||||
- 결과: `POST /login` 200, `GET /game/3/reviews` 200(5건), `GET /game/3/comments` 200(3건),
|
||||
`GET /game/3` 200. 더미 상황 런타임 정상. → needs_user_verification 의 UI 스모크 사실상 완료.
|
||||
- **발견 버그 (별도 작업단위 = W3-2 댓글기능, 내 seed 무관)**: 댓글 목록 author 항상 null.
|
||||
- 근거: GameCommentsMapper.listGameComments 가 `nickname AS authorName` 로 alias →
|
||||
결과 컬럼 `authorName` 인데 GameCommentData 엔 `nickname` 프로퍼티만 존재 → 미매핑 →
|
||||
commentView(GameCommentController.java:179) `getNickname()` null.
|
||||
- 대조: 단건 getGameComment 는 `nickname`(alias 없음) → 정상.
|
||||
- 1줄 수정안: listGameComments 의 `nickname AS authorName` → `nickname`.
|
||||
- 처리: 사용자 요청 시에만 (in-progress 기능 코드라 무단 수정 보류).
|
||||
|
||||
# open_items
|
||||
- 미커밋 신규 파일: db/seed-dev.sql, db/seed-dev-teardown.sql (작업단위 산출물).
|
||||
프로젝트/하니스 커밋정책 = 사용자 요청 시에만 커밋 → 커밋 보류, 사용자 확인 대기.
|
||||
- (별도 작업단위) W3-2 댓글/리뷰 기능 코드 커밋 시 `/graphify src/` 재생성 + docs/graph/index.md
|
||||
frontmatter(source_commit, last_generated_at) + Scopes 표 src 행 갱신 권고. 본 더미데이터
|
||||
task 범위 밖.
|
||||
|
||||
# Retrospective
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: "추천 옵션 전부 수락 (seed 적용방식·자격·규모 plan-gate 선택지 그대로 채택)"
|
||||
about: "orchestrator 의 plan-gate(AskUserQuestion) 추천값 + research 선행으로 advisor 다수 스킵한 경량 경로"
|
||||
- quote_or_paraphrase: "재지시·재호출 없이 단일 패스로 수렴 (부정 시그널 0)"
|
||||
about: "research 1회로 컬럼·계약·제약을 전부 확정 → design/impl/verification advisor 스킵한 판단이 검증됨"
|
||||
negative: []
|
||||
what_went_well:
|
||||
- "research-advisor 선행 1회로 인증 메커니즘(PBKDF2)·유저귀속·seed 적용경로를 모두 확정해, design/implementation/verification advisor 3종을 근거 있게 스킵하고 orchestrator 직접 구현으로 수렴. advisor invocation decision log 에 skip rationale 가 항목별로 남아 사후 추적 가능."
|
||||
- "호스트에 JDK 가 없는 제약에도 password_hash 를 추정·방치하지 않고 eclipse-temurin:21-jdk 컨테이너에서 실제 verifyPassword 코드 경로로 교차 실행(positive=true / negative=false)해 '로그인 가능' AC 를 자기검증으로 닫음. self-report 가 아닌 실행 증거."
|
||||
- "schema.sql 이 '비권위 복원본'이라는 메타 한계를 research concerns 에 보존하고, 컬럼명은 매퍼로 교차확인하되 제약·타입은 추정으로 표기해 신뢰도 과대평가를 회피."
|
||||
- "seed SQL 을 멱등 + teardown 쌍으로 작성하고 재실행 후 카운트 불변을 실증해 반복 적용 안전성을 확보."
|
||||
- "graph staleness 를 partial-stale 로 정직하게 판정하되, 본 세션 변경(데이터 픽스처)은 graph scope 밖임을 구분해 불필요한 재생성을 회피하고 잔여 stale 은 별도 작업단위로 정확히 귀속."
|
||||
what_to_improve:
|
||||
- "런타임 UI 스모크는 호스트 환경 제약(JDK 없음 + 앱 미가동)으로 자동 불가 → needs_user_verification 으로 정확히 이월됨. 개선이라기보다 환경 한계의 정직한 핸드오프. (구조적 결함 아님)"
|
||||
- "seed SQL 의 제약/타입/default 는 schema.sql 비권위 유래라 실 DB `\\d` 대조까지는 미수행(행수·테이블존재·매퍼 동등쿼리로만 검증). 운영 DB 와 default 가 다르면 재현 시 불일치 여지 — 현 dev 스키마에선 무영향이나 운영 적용 시 유의."
|
||||
memory_candidates:
|
||||
- name: bibimbap-auth-pbkdf2-not-bcrypt
|
||||
type: reference
|
||||
description: "bibimbap 로그인 해시는 BCrypt 가 아닌 자체 PBKDF2 — 로그인 가능 더미/테스트 계정 seed 시 이 포맷으로 직접 생성해야 함"
|
||||
body_draft: |
|
||||
# bibimbap 인증: PBKDF2 자체 해시 (BCrypt 아님)
|
||||
|
||||
Spring Security 미사용. 로그인은 UserController 자체 구현 password 검증.
|
||||
password_hash 포맷: `pbkdf2_sha256$210000$<base64(salt)>$<base64(hash)>` ($ 4필드)
|
||||
- 알고리즘 PBKDF2WithHmacSHA256, iterations=210000, keyLength=256bit, salt=16byte SecureRandom
|
||||
- 검증 경로: UserController.verifyPassword (parts[0]=="pbkdf2_sha256" && len==4 확인 후 동일 iter/salt 재계산, MessageDigest.isEqual 상수시간 비교)
|
||||
|
||||
## Why
|
||||
외부 BCrypt 생성기로 만든 해시는 무효 — 형식 자체가 다름. 향후 로그인 가능 더미/테스트/관리자 계정을
|
||||
seed 할 때마다 이 함정에 반복적으로 부딪힌다. 코드로 유도는 가능하나 "BCrypt 일 것"이라는 기본 가정이
|
||||
강해 매번 재확인 비용이 든다.
|
||||
|
||||
## How to apply
|
||||
- 로그인 가능 계정 seed 시 위 PBKDF2 스펙으로 해시 문자열을 직접 생성.
|
||||
- 로그인 동작 보장은 실제 verifyPassword 코드 경로(JDK 컨테이너 교차 실행 등)로 positive/negative 둘 다 확인.
|
||||
- 로그인 필수 조건 동봉: users.status='ACTIVE', user_auth_identities.provider='email',
|
||||
provider_user_id=정규화(trim+lowercase) 이메일, 양쪽 is_delete=false.
|
||||
rationale_for_saving: "코드로만 드러나는 비자명 사실(BCrypt 아님)이고, 테스트/더미 계정 seed 마다 재발하는 함정. schema/git log 로는 '자체 PBKDF2 라서 외부 생성기 무효'라는 함의가 드러나지 않음."
|
||||
signal_source: observation
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/CLAUDE.md
|
||||
- name: bibimbap-seed-blueprint-fk-and-unique
|
||||
type: reference
|
||||
description: "dev 스키마 더미 seed 청사진 — FK 순서 + game_reviews active-unique 제약(게임당 user 1리뷰)"
|
||||
body_draft: |
|
||||
# bibimbap dev 더미 seed 청사진
|
||||
|
||||
현실적 경로 = dev 스키마에 psql 직접 INSERT (UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용).
|
||||
DB 직삽은 CSRF·세션·중복선검사를 우회하되 FK·CHECK·유니크idx 는 그대로 적용.
|
||||
|
||||
INSERT 순서(FK 의존):
|
||||
1. users (status='ACTIVE' 필수, role='USER', is_delete=false, display_name 권장)
|
||||
2. user_auth_identities (PBKDF2 password_hash — 별도 memory 참조; provider='email')
|
||||
3. games (user_id FK, name; is_visible/is_delete default 로 목록노출 충족; webgl/thumbnail NULL 가능)
|
||||
4. game_reviews (game_id, user_id, rating 1~5, body)
|
||||
5. game_comments (game_id, user_id, nickname[직접 채움 — 조회가 users JOIN 안 함], content)
|
||||
|
||||
## 핵심 제약
|
||||
- game_reviews 부분 유니크 idx `(game_id, user_id) WHERE is_delete IS NOT TRUE`
|
||||
→ 한 게임에 더미 리뷰 N개 = 더미 유저 N명 필요 (한 user 는 게임당 활성 1리뷰).
|
||||
- game_comments 는 유니크 없음 → 자유 다수. 작성자명 보이려면 nickname 직접 채움(비정규화 저장).
|
||||
- 게임 목록+상세 노출 = games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false.
|
||||
|
||||
## Why
|
||||
리뷰 N개를 같은 유저로 넣으려다 유니크 위반으로 막히는 게 첫 시도의 흔한 실패. schema.sql 이
|
||||
'비권위 복원본'이라 제약을 코드에서 직접 읽기 전엔 드러나지 않음.
|
||||
|
||||
## How to apply
|
||||
- seed SQL 확정 직전 `\d dev.<table>` 로 제약/타입/default 운영 대조 권장(schema.sql 비권위).
|
||||
- 리뷰 다양화가 목표면 더미 유저를 리뷰 수만큼 선행 INSERT.
|
||||
rationale_for_saving: "seed 설계 시 반복되는 유니크-위반 함정 + FK 순서. schema.sql 비권위라 코드 교차확인 없이는 유도 불가하고, 더미/픽스처 작업마다 재사용되는 청사진."
|
||||
signal_source: observation
|
||||
docs_sync_target: null
|
||||
- name: java-crypto-verify-via-jdk-container-when-no-host-jdk
|
||||
type: feedback
|
||||
description: "호스트에 JDK 없을 때 Java 암호/검증 로직을 eclipse-temurin:21-jdk 컨테이너 single-file 실행으로 교차검증"
|
||||
body_draft: |
|
||||
# 호스트 JDK 부재 시 Java 로직 교차검증 패턴
|
||||
|
||||
호스트에 JDK 가 없고 앱도 미가동인 환경에서, password_hash 같은 Java 암호/검증 로직의
|
||||
정합성을 추정으로 닫지 않고 `eclipse-temurin:21-jdk` 컨테이너에 single-file/javac 로
|
||||
실제 코드 경로(예: verifyPassword)를 옮겨 실행해 positive+negative 케이스를 모두 확인.
|
||||
|
||||
## Why
|
||||
seed 한 해시가 "로그인 된다"는 self-report 는 증거가 아니다. 실제 검증 함수와 동일한
|
||||
알고리즘/iter/salt 로 재계산해 true/false 가 나와야 AC 가 닫힌다. 호스트 toolchain 부재는
|
||||
검증 생략의 이유가 되지 않고, 컨테이너로 우회 가능하다.
|
||||
|
||||
## How to apply
|
||||
- 검증 대상 메서드의 알고리즘 파라미터(iter/keyLength/salt/encoding)를 코드에서 그대로 추출.
|
||||
- 동일 로직을 단일 .java 로 재현해 temurin 컨테이너에서 실행, 정답=true / 오답=false 둘 다 확인.
|
||||
- 런타임 UI 스모크처럼 컨테이너로도 불가한 부분만 needs_user_verification 으로 이월.
|
||||
rationale_for_saving: "환경 제약(호스트 JDK 없음) + Java 검증 로직이라는 비자명 조합. self-report 대신 실행 증거로 AC 를 닫는 재현성 있는 검증 워크플로우 — 단발 우발 아님."
|
||||
signal_source: positive
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
|
||||
protocol_feedback: []
|
||||
applied_changes: []
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
---
|
||||
phase: research
|
||||
agent: research-advisor
|
||||
agent_version: 2
|
||||
generated_at: 2026-06-18T03:18:28Z
|
||||
concerns:
|
||||
- "low source confidence — db/schema.sql 은 헤더 주석상 '비권위 복원본'(users/user_auth_identities/game_comments). 컬럼명·매퍼 일치는 코드로 교차확인됐으나 타입·길이·기본값·제약은 추론값. 운영 DB 제약과 다를 수 있으므로 seed SQL 확정 전 실 DB(\\d 조회)로 컬럼·제약 대조 필요."
|
||||
- "BCrypt 가 아닌 PBKDF2 자체 해시이므로 seed용 password_hash 문자열은 외부 BCrypt 생성기로 만들 수 없음. iterations=210000/HmacSHA256/16byte-salt/256bit 스펙에 맞춰 직접 생성해야 함(생성 스크립트 자체는 design/구현 몫)."
|
||||
concerns_checked: true
|
||||
source_confidence: high
|
||||
workers_spawned: 3
|
||||
---
|
||||
|
||||
# 조사 결과
|
||||
|
||||
## 주제
|
||||
|
||||
bibimbap(Java 21 / Spring Boot / JSP / MyBatis, WAR) 에 테스트용 더미 데이터를 seed 하기 위한 사전 조사.
|
||||
"로그인·리뷰 작성이 실제로 동작하는" 형태로 (1) 게임 (2) 로그인 가능 계정 (3) 더미 리뷰+댓글 을 만들기 위한
|
||||
정확한 컬럼값·메커니즘 도출. 코드 수정 없음.
|
||||
|
||||
> 신뢰도 표기: `확인됨` = 코드/실행으로 1차 확인. `추정` = 통용·유추. `미확인` = 검증 불가.
|
||||
> 본 조사의 사실 항목은 전부 프로젝트 코드 직접 확인 기반 → `source_confidence: high`.
|
||||
> 다만 schema.sql 자체가 "비권위 복원본"이라는 메타 한계는 concerns 에 보존(컬럼명은 매퍼로 교차확인됨, 제약·타입은 추론).
|
||||
|
||||
---
|
||||
|
||||
## 포인트별 발견
|
||||
|
||||
### 포인트 A: 로그인/인증 메커니즘 (실제 로그인 가능한 계정 seed)
|
||||
|
||||
- 경로: `src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java`,
|
||||
`data/UserAuthIdentityData.java`, `mapper/UserAuthIdentitiesMapper.java`, `mapper/UsersMapper.java`, `db/schema.sql`
|
||||
|
||||
요약:
|
||||
- **Spring Security 미사용.** `PasswordEncoder|SecurityFilterChain|@EnableWebSecurity|spring-boot-starter-security`
|
||||
src/ + pom.xml 0 hit. 인증은 UserController 자체 구현 password 로그인. **OAuth 없음**(google/kakao/naver/oauth 0 hit). [확인됨]
|
||||
- 로그인 엔드포인트: `POST /login`, form 파라미터 `email`,`password`,`remember`(선택) — UserController.java:122, 125-128. [확인됨]
|
||||
진입부에서 `CsrfTokens.isValid(request)` 실패 시 403 — UserController.java:131-133. [확인됨]
|
||||
- **provider 컬럼 값은 `"email"` 단 하나.** `PROVIDER_EMAIL="email"` — UserController.java:42.
|
||||
identity 생성 시 set — UserController.java:314. 조회 필터도 동일 값 — UserController.java:140-143. [확인됨]
|
||||
`provider_user_id` 에는 **정규화 이메일**(trim+lowercase) 저장 — UserController.java:104-106, 527-533. [확인됨]
|
||||
- **password_hash 알고리즘 = PBKDF2 (BCrypt 아님).** PasswordEncoder 빈 없음. 형식:
|
||||
`pbkdf2_sha256$<iterations>$<base64(salt)>$<base64(hash)>` (`$` 4필드) — UserController.java:554-556. [확인됨]
|
||||
- iterations=210000 (UserController.java:46), keyLength=256bit (:47), salt=16byte SecureRandom (:48,551-552),
|
||||
알고리즘 `PBKDF2WithHmacSHA256` via JCE SecretKeyFactory/PBEKeySpec (:576-584). [확인됨]
|
||||
- 검증: `verifyPassword()` 가 `$` split → parts[0]=="pbkdf2_sha256" && length==4 확인 후 동일 iterations/salt 재계산,
|
||||
`MessageDigest.isEqual` 상수시간 비교 — UserController.java:144, 559-574. [확인됨]
|
||||
- PBKDF2 표준 동작(iterations·salt 가 해시 문자열에 동봉되어 검증 시 재현 가능) — [추정/일반 통용 지식]
|
||||
- **로그인 성공 시 세션 저장** (saveLoginSession — UserController.java:162, 502-525). 세션 직전 `changeSessionId()` 세션고정방어 — :159-160. [확인됨]
|
||||
개별 attribute: `id`(Long), **`userId`(Long — 인증가드 sessionUserId() 가 읽는 키, :337)**, `displayName`, `email`(canonicalEmail),
|
||||
`avatarUrl`, `role`, `status`, `authProvider`("email"), `authIdentityId`, `lastLoginAt` — UserController.java:503-512.
|
||||
추가로 `account` 키에 위 값들의 LinkedHashMap 통째 저장 — :514-524. (UserData 객체 자체가 아니라 개별 스칼라+Map) [확인됨]
|
||||
- **로그인 검증 컬럼 흐름** [확인됨]:
|
||||
1. user_auth_identities `WHERE provider='email' AND provider_user_id=<정규화이메일> AND is_delete IS NOT TRUE` — UserAuthIdentitiesMapper.java:47-49
|
||||
2. password_hash != null && PBKDF2 통과 — UserController.java:144
|
||||
3. users `WHERE id=<identity.user_id> AND is_delete IS NOT TRUE` — UsersMapper.java:25-26
|
||||
4. **user.status == "ACTIVE"** 필수 (STATUS_ACTIVE=:44) — UserController.java:148-149
|
||||
|
||||
- **seed 시 필수 채움 컬럼** (schema.sql 비권위 — 컬럼명만 신뢰):
|
||||
- users: `role`(default 'USER'), **`status`='ACTIVE'**(ACTIVE 아니면 로그인 거부), `is_delete`=false. display_name/canonical_email 권장. — schema.sql:30-42 [확인됨, 제약값은 추정]
|
||||
- user_auth_identities: `user_id`(FK), `provider`='email', `provider_user_id`=정규화이메일(소문자), **`password_hash`=PBKDF2형식**(null이면 즉시거부), `is_delete`=false. — schema.sql:49-63 [확인됨, 제약값은 추정]
|
||||
- 주의: active-unique idx `ux_user_auth_identities_provider_user_id_active` = `(provider, provider_user_id) WHERE is_delete IS NOT TRUE` — schema.sql:66-68. 같은 이메일 활성 identity 중복 INSERT 실패. [확인됨, idx는 비권위 schema.sql]
|
||||
- 신뢰도: 확인됨 (코드 직접 확인). schema.sql 유래 제약값만 추정.
|
||||
|
||||
### 포인트 B: 리뷰/댓글 작성 시 유저 귀속 + INSERT 컬럼 + CSRF
|
||||
|
||||
- 경로: `controller/api/GameReviewController.java`, `data/GameReviewData.java`, `mapper/GameReviewsMapper.java`,
|
||||
`controller/api/GameCommentController.java`, `data/GameCommentData.java`, `mapper/GameCommentsMapper.java`,
|
||||
`security/CsrfTokens.java`, `db/schema.sql`, `docs/game-reviews-ddl.sql`
|
||||
|
||||
요약:
|
||||
- **리뷰 API**: GET `/game/{id}/reviews`(목록, :41-42), GET `/game/{id}/reviews/{reviewId}`(:58-62),
|
||||
POST `/game/{id}/reviews`(작성, :74), PUT(수정, :124), DELETE(소프트삭제, :170) — GameReviewController.java. [확인됨]
|
||||
작성 바디(@RequestParam, form): `rating`(String→1~5 파싱, 범위밖 400), `body`(최대 1000자) — :78-101, BODY_MAX=:30. [확인됨]
|
||||
- **user_id 출처 = 세션 attribute `"userId"`** (요청 바디 아님). `sessionUserId(session)` → `session.getAttribute("userId")` — GameReviewController.java:86, 241-257(:245). 없으면 401. `review.setUserId(userId)` — :109. [확인됨]
|
||||
- **game_reviews INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, rating, body)` `#{}` 바인딩 — GameReviewsMapper.java:72-86. [확인됨]
|
||||
- game_id ← PathVariable(:108), user_id ← 세션(:109), rating ← 1~5 정수(:110; DB CHECK BETWEEN 1 AND 5 — schema.sql:129, docs/game-reviews-ddl.sql:49-53), body ← trimToEmpty(빈입력시 ""; DB text nullable) — :98,275-278. [확인됨]
|
||||
- 미포함(전부 DB DEFAULT): id(seq), created_at/updated_at(now()), is_delete(false), deleted_at(null) — schema.sql:124-127. [확인됨, 비권위 schema]
|
||||
- 참고: updated_at > created_at 이면 조회 시 edited=true 표시 — GameReviewsMapper.java:24, GameReviewController.java:210. [확인됨]
|
||||
- **ux_game_reviews_game_user_active** = 부분 유니크 인덱스 `(game_id, user_id) WHERE is_delete IS NOT TRUE` — schema.sql:132-133, docs/game-reviews-ddl.sql:58-61. [확인됨]
|
||||
의미: 활성 리뷰는 (game,user) 조합당 1개. soft-delete 행은 제외 → 재작성 허용. 코드도 INSERT 전 중복 선검사 후 409 — GameReviewController.java:103-105, GameReviewsMapper.java:56-70. [확인됨]
|
||||
**seed 주의**: 한 게임에 더미 리뷰 여러 개 원하면 각 행 user_id 를 다르게. 한 user 는 게임당 활성 1개. [확인됨]
|
||||
- **댓글 API**: GET `/game/{id}/comments`(:39), POST(작성, :56), PUT(:102), DELETE(소프트삭제, :143). 작성 바디 `content`만(최대 200자, CONTENT_MAX=:28) — GameCommentController.java:60,75-78. [확인됨]
|
||||
- user_id ← 세션 `"userId"` (:67,198). [확인됨]
|
||||
- **nickname 비정규화 저장 확정**: 작성 시점 세션 `"displayName"` 을 nickname 컬럼에 복사 — `comment.setNickname(authorName)` :84, sessionDisplayName=session.getAttribute("displayName") :212-218(:216). 조회 시 users JOIN 없이 nickname 그대로 노출(`nickname AS authorName`) — GameCommentsMapper.java:36, GameCommentController.java:179. [확인됨]
|
||||
- **game_comments INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, nickname, content)` — GameCommentsMapper.java:46-60. user_id nullable FK(schema.sql:112), nickname varchar(100) nullable(:102). [확인됨, 비권위 schema]
|
||||
- 유니크 제약 없음 → 같은 (game,user) 댓글 여러 개 가능. 화면 작성자명 보이려면 nickname 직접 채워야(조회가 users JOIN 안 함). 목록 정렬 created_at ASC, id ASC — GameCommentsMapper.java:42. [확인됨]
|
||||
- **CSRF**: Spring Security 미사용 → 표준 CSRF 필터 없음. 대신 자체 `CsrfTokens` 유틸을 컨트롤러 mutation 진입부에서 수동검증. [확인됨]
|
||||
- 리뷰 POST/PUT/DELETE: GameReviewController.java:83-85,134-136,178-180. 댓글: GameCommentController.java:64-66,111-113,151-153. GET 은 검증 없음. [확인됨]
|
||||
- 토큰: 세션 key `"csrfToken"`, 헤더 `X-CSRF-Token`(우선) 또는 폼 파라미터 `_csrf`(폴백), 32byte SecureRandom Base64url — CsrfTokens.java:12-13,20-33,35-49. [확인됨]
|
||||
- **영향**: HTTP API 경유 수동 작성은 로그인세션+csrfToken+헤더/파라미터 필요. **DB 직접 INSERT seed 는 CSRF·세션·중복선검사 모두 우회**(단 DB 유니크idx·CHECK·FK 는 그대로 적용). [확인됨]
|
||||
- 신뢰도: 확인됨. schema.sql/ddl 유래 제약만 추정.
|
||||
|
||||
### 포인트 C: seed 적용 경로 + 게임 표시 조건
|
||||
|
||||
- 경로: `.env`, `docker-compose.yml`, `src/main/resources/dev/db.properties`, `GameController.java`, `GamesMapper.java`,
|
||||
`data/GameData.java`, `db/schema.sql`, `WEB-INF/views/game-detail.jsp`
|
||||
|
||||
요약:
|
||||
- **DB 접속 정보 확정** (.env + docker-compose.yml + dev/db.properties 일관) [확인됨, 실행 확인]:
|
||||
- DB명 `bibimbap`, 유저 `bibimbap`, 패스워드 `change_me_local_dev`, 호스트포트 5433 → 컨테이너 5432.
|
||||
- .env: POSTGRES_DB/USER=bibimbap, POSTGRES_PASSWORD=change_me_local_dev, APP_SCHEMA=dev, DB_PORT=5433
|
||||
- docker-compose.yml:18 `"${DB_PORT:-5432}:5432"`, :40 `jdbc:...:5432/${POSTGRES_DB}?currentSchema=${APP_SCHEMA:-dev}`
|
||||
- dev/db.properties:2-4 `jdbc:postgresql://localhost:5433/bibimbap?currentSchema=dev`
|
||||
- 컨테이너 `bibimbap-db postgres:16 Up (healthy) 0.0.0.0:5433->5432/tcp` (docker compose ps 실행 확인)
|
||||
- **앱 스키마 = `dev`**. psql 접속: `PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap` 후 `SET search_path TO dev;` (또는 `dev.games` 한정).
|
||||
- **현재 dev.games / dev.users 모두 0행** (read-only 조회 확인). live 스키마는 테이블 없음 → 앱 데이터는 전적으로 dev. [확인됨]
|
||||
- **게임 표시 조건** (목록과 상세가 다름; games-users INNER JOIN `JOIN users u ON u.id=g.user_id` → 유효 미삭제 user 필수) — GamesMapper.java:32-33. [확인됨]
|
||||
- 상세(getGame, GamesMapper.java:34-36): `g.id=#{id} AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. **is_visible 조건 없음** → 비공개 게임도 id 직접 접근 시 상세 뜸. [확인됨]
|
||||
- 목록(getVisibleGames, GamesMapper.java:57-59): `g.is_visible IS NOT FALSE AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. [확인됨]
|
||||
- **목록+상세 모두 보이려면**: games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false. [확인됨]
|
||||
- 컨트롤러: gameDetail 이 getGame(id)!=null 이면 DB 게임으로 game-detail 렌더 — GameController.java:113-121. [확인됨]
|
||||
- **games INSERT 필수(NOT NULL, no-default) 컬럼 = `user_id`(FK→users), `name` 둘뿐** — schema.sql:75-87. [확인됨, 비권위 schema]
|
||||
- default 보유(생략가능): like_count(0), is_visible(true), sort_order(0), created_at/updated_at(now()), is_delete(false). nullable: creator_note, git_url, webgl_path, thumbnail_url. id=seq.
|
||||
- 최소 INSERT `(user_id, name)` 만으로 가능 + 목록노출 조건 자동충족. 단 user_id FK 로 **users 행 선행 필수**.
|
||||
- **asset 컬럼 NULL 이어도 상세/리뷰 동작 — 깨지지 않음** [확인됨]:
|
||||
- game-detail.jsp:12-21 webglFrameSrc null/blank/"null" 시 빈 문자열 폴백 → iframe src="" 빈화면(예외 없음). 컨트롤러가 trimToEmpty 정규화 후 모델 주입(GameController.java:273,281-283)이라 NPE 없음.
|
||||
- thumbnail_url 은 상세 JSP 미사용. 리뷰/댓글은 별도 비동기 API(`/game/{id}/reviews`,`/comments`)로 로드 → game asset 컬럼과 무관.
|
||||
- 결론: webgl_path/thumbnail_url 을 NULL 로 둬도 상세 + 리뷰/댓글 테스트 가능.
|
||||
- 신뢰도: 확인됨 (코드 + 실행). schema.sql 유래 제약만 추정.
|
||||
|
||||
---
|
||||
|
||||
## 종합 판단 (더미 seed 실행 청사진)
|
||||
|
||||
세 포인트가 한 흐름으로 맞물린다. **현실적 seed 경로 = dev 스키마에 psql 직접 INSERT** (앱 UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용; DB 직삽은 그 전부 우회하되 FK·CHECK·유니크idx 만 적용).
|
||||
|
||||
INSERT 순서(FK 의존성):
|
||||
1. **users** 1행: status='ACTIVE'(필수 — 아니면 로그인 거부), role='USER', is_delete=false, display_name 권장(목록 creator + 댓글 nickname 소스).
|
||||
2. **user_auth_identities** 1행: user_id=위 user, provider='email', provider_user_id=<정규화 소문자 이메일>, password_hash=**PBKDF2 형식** `pbkdf2_sha256$210000$<b64salt>$<b64hash>`, is_delete=false. (활성 유니크 idx 주의)
|
||||
3. **games** 1행: user_id=위 user, name=값. is_visible/is_delete 는 default 로 목록노출 충족. webgl_path/thumbnail_url NULL 가능.
|
||||
4. **game_reviews** N행: (game_id, user_id, rating 1~5, body). **한 게임당 user 1활성리뷰** → 여러 리뷰면 user 여러 개 seed.
|
||||
5. **game_comments** N행: (game_id, user_id, nickname[직접 채움], content). 유니크 없음 → 자유 다수.
|
||||
|
||||
**로그인 가능 핵심(질문의 최우선 관심사)**: password_hash 는 BCrypt 가 아니라 PBKDF2WithHmacSHA256.
|
||||
평문→해시는 외부 BCrypt 도구로 만들 수 없고, iterations=210000 / 256bit / 16byte salt / base64 스펙으로
|
||||
`pbkdf2_sha256$210000$<base64salt>$<base64hash>` 문자열을 생성해 넣어야 검증 통과한다.
|
||||
(해시 생성 스크립트 작성은 design/구현 단계 몫 — research 범위 밖.)
|
||||
|
||||
권위 격상 전 검증 필요 항목: db/schema.sql 이 비권위 복원본이므로, 위 컬럼 *제약/타입/default* 는 seed SQL 확정 직전
|
||||
실 DB `\d dev.users / \d dev.user_auth_identities / \d dev.games / \d dev.game_reviews / \d dev.game_comments` 로 대조 권장.
|
||||
(컬럼명·매퍼 일치·인덱스 존재는 코드/실행으로 확인됨 — 불일치 위험은 타입/제약값에 한정.)
|
||||
|
||||
## 미해결
|
||||
|
||||
- PBKDF2 seed 해시를 실제로 생성하는 도구/스크립트는 미작성(설계·구현 영역). 본 조사는 "어떤 형식이어야 검증 통과하는가"까지만.
|
||||
- db/schema.sql 비권위로 인한 컬럼 *제약/타입/default* 의 운영 DB 대조는 미수행(읽기조회로 행수·테이블존재만 확인). seed SQL 확정 전 `\d` 대조 필요.
|
||||
- game_likes 서버측 like_count 증감 경로(좋아요는 localStorage 클라이언트 토글로 보임) — 더미 리뷰/계정/게임 범위 밖이라 미조사.
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
---
|
||||
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건 — 구현 미시작.
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>W3-2 UI 프리뷰 — 육각형 레이더 · 6축 평점 · 카드</title>
|
||||
<style>
|
||||
html {
|
||||
color-scheme: light;
|
||||
--surface: #faf8f5; --card-bg: #fff; --text: #1a1a1a; --text-muted: #5c5c5c;
|
||||
--accent: #e8a54b; --accent-soft: rgba(232,165,75,0.14);
|
||||
--border: rgba(0,0,0,0.08); --panel-shadow: 0 4px 24px rgba(26,26,26,0.06); --radius: 16px;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--surface: #121212; --card-bg: #1e1e1e; --text: #ece8e1; --text-muted: #a39e96;
|
||||
--border: rgba(255,255,255,0.1); --accent-soft: rgba(232,165,75,0.12);
|
||||
--panel-shadow: 0 8px 32px rgba(0,0,0,0.35);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; padding: 2rem 1rem 4rem;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
html:not([data-theme="dark"]) body { background: linear-gradient(165deg,#fff9f0 0%,var(--surface) 45%,#f3ede6 100%); background-attachment: fixed; }
|
||||
html[data-theme="dark"] body { background: linear-gradient(165deg,#1c1a18 0%,var(--surface) 38%,#0e0e0e 100%); background-attachment: fixed; }
|
||||
.wrap { max-width: 56rem; margin: 0 auto; }
|
||||
.preview-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
|
||||
.preview-bar h1 { font-size: 1.1rem; margin: 0; letter-spacing: -0.02em; }
|
||||
.preview-bar button { padding: 0.45rem 1rem; font: inherit; font-weight: 700; font-size: 0.8rem; cursor: pointer;
|
||||
color: var(--text); background: var(--card-bg); border: 1px solid var(--border); border-radius: 999px; }
|
||||
.game-panel { padding: 1.4rem 1.5rem 1.6rem; margin-bottom: 1.5rem; background: var(--card-bg);
|
||||
border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--panel-shadow); }
|
||||
.game-panel__title { margin: 0 0 0.2rem; font-size: 1.05rem; letter-spacing: -0.02em; }
|
||||
.game-panel__subtitle { margin: 0 0 1.1rem; font-size: 0.8rem; color: var(--text-muted); }
|
||||
.section-tag { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: var(--accent); margin: 0 0 0.6rem; }
|
||||
|
||||
/* ===== 아래는 game-detail.jsp 승격본과 동일한 W3-2 컴포넌트 스타일 ===== */
|
||||
.game-reviews__summary { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.game-reviews__avg { font-size: 1.35rem; font-weight: 800; letter-spacing: -0.02em; color: var(--accent); }
|
||||
.game-reviews__count { font-size: 0.75rem; font-weight: 600; color: var(--text-muted); }
|
||||
.game-stars--avg, .game-stars--display { display: inline-flex; gap: 0.05rem; font-size: 0.95rem; line-height: 1; }
|
||||
.game-stars--avg .game-star, .game-stars--display .game-star { color: var(--border); }
|
||||
.game-stars--avg .game-star.is-on, .game-stars--display .game-star.is-on { color: var(--accent); }
|
||||
|
||||
.game-reviews__summary-panel { margin: 0 0 1.25rem; display: flex; align-items: center; gap: 1.25rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: radial-gradient(125% 125% at 0% 0%, var(--accent-soft), transparent 62%), var(--card-bg);
|
||||
border: 1px solid rgba(232,165,75,0.22); border-radius: var(--radius); box-shadow: var(--panel-shadow); }
|
||||
@media (max-width: 480px) { .game-reviews__summary-panel { flex-direction: column; align-items: stretch; gap: 0.85rem; } .game-reviews__radar { justify-content: center; } }
|
||||
.game-reviews__radar { display: inline-flex; flex-shrink: 0; }
|
||||
.game-reviews__radar svg { display: block; }
|
||||
.game-axis-radar { overflow: visible; }
|
||||
.game-reviews__radar .game-axis-grid { fill: none; stroke: var(--border); stroke-width: 1; }
|
||||
.game-reviews__radar .game-axis-grid--outer { stroke: rgba(232,165,75,0.4); stroke-width: 1.25; }
|
||||
.game-reviews__radar .game-axis-line { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 3; }
|
||||
.game-reviews__radar .game-axis-poly { stroke: var(--accent); stroke-width: 1.75; stroke-linejoin: round;
|
||||
filter: drop-shadow(0 2px 7px rgba(232,165,75,0.38)); transform-box: fill-box; transform-origin: center;
|
||||
animation: game-radar-pop 0.55s cubic-bezier(0.22,1,0.36,1) both; }
|
||||
.game-reviews__radar .game-axis-dot { fill: var(--accent); stroke: var(--card-bg); stroke-width: 1.5;
|
||||
animation: game-radar-fade 0.7s ease both; }
|
||||
@keyframes game-radar-pop { from { opacity: 0; transform: scale(0.55); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes game-radar-fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
.game-reviews__radar-empty { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.game-reviews__axis-legend { margin: 0; flex: 1; min-width: 11.5rem; display: grid; gap: 0.4rem 0; font-size: 0.75rem; color: var(--text-muted); }
|
||||
.game-reviews__axis-legend-row { display: grid; grid-template-columns: 3.4rem 1fr 1.15rem; align-items: center; gap: 0.55rem; }
|
||||
.game-reviews__axis-legend dt { font-weight: 600; color: var(--text); }
|
||||
.game-reviews__axis-legend dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 700; text-align: right; color: var(--accent); }
|
||||
.game-reviews__axis-meter { height: 0.4rem; border-radius: 999px; background: var(--border); overflow: hidden; }
|
||||
.game-reviews__axis-meter span { display: block; height: 100%; border-radius: 999px;
|
||||
background: linear-gradient(90deg,#f0c978,var(--accent)); animation: game-meter-grow 0.7s cubic-bezier(0.22,1,0.36,1) both; transform-origin: left; }
|
||||
@keyframes game-meter-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
|
||||
|
||||
/* 6축 입력 */
|
||||
.game-reviews__composer { margin-bottom: 1.25rem; padding: 1.1rem 1.15rem 1.2rem;
|
||||
background: radial-gradient(140% 120% at 100% 0%, rgba(232,165,75,0.16), transparent 60%), var(--accent-soft);
|
||||
border: 1px solid rgba(232,165,75,0.22); border-radius: 14px; }
|
||||
.game-reviews__axes { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 0.5rem 1.1rem; margin-bottom: 0.85rem; }
|
||||
@media (max-width: 480px) { .game-reviews__axes { grid-template-columns: 1fr; } }
|
||||
.game-reviews__axis { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
|
||||
padding: 0.3rem 0.5rem; border-radius: 10px; transition: background 0.15s ease; }
|
||||
.game-reviews__axis:hover { background: rgba(232,165,75,0.08); }
|
||||
.game-reviews__axis-label { font-size: 0.8125rem; font-weight: 600; color: var(--text); }
|
||||
.game-stars { display: inline-flex; gap: 0.15rem; line-height: 1; }
|
||||
.game-stars--axis { gap: 0.1rem; }
|
||||
.game-stars__btn { padding: 0.1rem; font-size: 1.6rem; line-height: 1; color: var(--border);
|
||||
background: none; border: none; cursor: pointer; transition: color 0.12s ease, transform 0.12s ease, text-shadow 0.2s ease; }
|
||||
.game-stars--axis .game-stars__btn { font-size: 1.25rem; }
|
||||
.game-stars__btn:hover { transform: scale(1.12); }
|
||||
.game-stars__btn.is-on { color: var(--accent); text-shadow: 0 1px 4px rgba(232,165,75,0.45); }
|
||||
.game-stars__btn:active { transform: scale(0.88); }
|
||||
.game-stars__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
||||
.game-reviews__rating-row { display: flex; align-items: center; gap: 0.85rem; margin-bottom: 0.85rem; }
|
||||
.game-comments__label { font-size: 0.8125rem; font-weight: 600; color: var(--text-muted); }
|
||||
.game-comments__form textarea { width: 100%; min-height: 5rem; padding: 0.85rem 1rem; font: inherit; font-size: 0.875rem;
|
||||
line-height: 1.55; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; resize: vertical; }
|
||||
.game-comments__form textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px rgba(232,165,75,0.18); }
|
||||
.game-comments__actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.65rem; }
|
||||
.game-comments__hint { font-size: 0.6875rem; color: var(--text-muted); }
|
||||
.game-comments__hint.is-warn { color: #b33; font-weight: 700; }
|
||||
.game-comments__form button[type="submit"] { padding: 0.55rem 1.5rem; font: inherit; font-size: 0.875rem; font-weight: 700; color: #1a1a1a;
|
||||
background: linear-gradient(180deg,#f0c978 0%,var(--accent) 100%); border: none; border-radius: 10px; cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(232,165,75,0.35); transition: transform 0.15s ease, box-shadow 0.2s ease; }
|
||||
.game-comments__form button[type="submit"]:hover { box-shadow: 0 4px 14px rgba(232,165,75,0.45); }
|
||||
button[aria-busy="true"] { opacity: 0.65; cursor: progress; }
|
||||
|
||||
/* sort 토글 / 더보기 */
|
||||
.game-comments__toolbar { display: flex; justify-content: flex-end; margin-bottom: 0.85rem; }
|
||||
.game-comments__sort { display: inline-flex; gap: 0.25rem; padding: 0.2rem; background: var(--surface); border: 1px solid var(--border); border-radius: 999px; }
|
||||
.game-comments__sort-btn { padding: 0.3rem 0.7rem; font: inherit; font-size: 0.6875rem; font-weight: 700; color: var(--text-muted);
|
||||
background: transparent; border: none; border-radius: 999px; cursor: pointer; transition: color 0.15s ease, background 0.15s ease; }
|
||||
.game-comments__sort-btn.is-active { color: #1a1a1a; background: linear-gradient(180deg,#f0c978,var(--accent)); box-shadow: 0 1px 5px rgba(232,165,75,0.4); }
|
||||
.game-comments__more { margin-top: 1rem; display: flex; justify-content: center; }
|
||||
.game-comments__more-btn { padding: 0.5rem 1.4rem; font: inherit; font-size: 0.8125rem; font-weight: 700; color: var(--text);
|
||||
background: var(--card-bg); border: 1px solid var(--border); border-radius: 999px; cursor: pointer; transition: border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.2s ease; }
|
||||
.game-comments__more-btn:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); box-shadow: 0 5px 16px rgba(232,165,75,0.16); }
|
||||
|
||||
/* 리뷰/덧글 카드 */
|
||||
.game-reviews__list, .game-comments__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.85rem; }
|
||||
.game-reviews__item { padding: 0.95rem 1.1rem; font-size: 0.875rem; line-height: 1.55; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; }
|
||||
.game-reviews__item.is-mine { border-color: rgba(232,165,75,0.4); background: var(--accent-soft); }
|
||||
.game-reviews__item-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.65rem; margin-bottom: 0.5rem; }
|
||||
.game-reviews__nick { font-size: 0.8125rem; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.game-reviews__mine-badge { padding: 0.05rem 0.45rem; font-size: 0.625rem; font-weight: 700; color: var(--accent); background: rgba(232,165,75,0.16); border-radius: 999px; }
|
||||
.game-reviews__time, .game-comments__meta time { margin-left: auto; font-size: 0.6875rem; font-weight: 500; letter-spacing: 0.02em; color: var(--text-muted); }
|
||||
.game-comments__edited, .game-reviews__edited { margin-left: 0.35rem; font-size: 0.6875rem; font-weight: 500; color: var(--text-muted); }
|
||||
.game-reviews__body, .game-comments__item p { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
||||
.game-reviews__card-radar { margin-top: 0.6rem; display: inline-flex; }
|
||||
.game-reviews__card-radar svg { display: block; }
|
||||
.game-reviews__radar-empty { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.game-comments__item { display: flex; gap: 0.75rem; align-items: flex-start; padding: 0.85rem 1rem; font-size: 0.875rem; line-height: 1.55; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; }
|
||||
.game-comments__avatar { flex-shrink: 0; width: 2.25rem; height: 2.25rem; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.75rem; font-weight: 800; color: var(--accent); background: var(--accent-soft); border: 1px solid rgba(232,165,75,0.25); }
|
||||
.game-comments__meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.65rem; margin-bottom: 0.4rem; }
|
||||
.game-comments__nick { font-size: 0.75rem; font-weight: 700; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.game-reviews__radar .game-axis-poly, .game-reviews__radar .game-axis-dot, .game-reviews__axis-meter span, .game-comments__more-btn:hover { animation: none; transform: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="preview-bar">
|
||||
<h1>W3-2 UI 프리뷰 · 육각형 레이더 · 6축 평점</h1>
|
||||
<button id="theme-toggle" type="button">🌙 다크</button>
|
||||
</div>
|
||||
|
||||
<!-- 리뷰 패널 -->
|
||||
<section class="game-panel">
|
||||
<p class="section-tag">리뷰 요약 (game_review_stats)</p>
|
||||
<div class="game-reviews__summary">
|
||||
<span class="game-reviews__avg" id="avg">4.2</span>
|
||||
<div class="game-stars game-stars--avg" id="avg-stars" aria-hidden="true"></div>
|
||||
<span class="game-reviews__count">(37)</span>
|
||||
</div>
|
||||
<div class="game-reviews__summary-panel" id="summary-panel"></div>
|
||||
|
||||
<p class="section-tag">6축 평가 입력 (radiogroup · 키보드 ←→↑↓/Home/End)</p>
|
||||
<div class="game-reviews__composer">
|
||||
<div class="game-reviews__axes" id="axes" aria-label="6축 평가"></div>
|
||||
<div class="game-reviews__rating-row">
|
||||
<span class="game-comments__label" id="overall-label">종합 별점 (선택)</span>
|
||||
<div class="game-stars game-stars--input" id="overall" role="radiogroup" aria-labelledby="overall-label"></div>
|
||||
</div>
|
||||
<label class="game-comments__label" for="rbody">평가</label>
|
||||
<textarea id="rbody" rows="3" maxlength="1000" placeholder="이 게임을 어떻게 즐기셨나요? (최소 10자)"></textarea>
|
||||
<div class="game-comments__actions">
|
||||
<span class="game-comments__hint" id="rhint"><span id="rcount">0/1000</span> · 6축은 필수, 종합 별점은 선택이에요</span>
|
||||
<button type="submit">리뷰 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="section-tag">리뷰 목록 · 정렬 · 카드 컴팩트 레이더</p>
|
||||
<div class="game-comments__toolbar">
|
||||
<div class="game-comments__sort" role="group" aria-label="리뷰 정렬">
|
||||
<button type="button" class="game-comments__sort-btn is-active">최신순</button>
|
||||
<button type="button" class="game-comments__sort-btn">별점 높은순</button>
|
||||
<button type="button" class="game-comments__sort-btn">별점 낮은순</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="game-reviews__list" id="review-list"></ul>
|
||||
<div class="game-comments__more"><button type="button" class="game-comments__more-btn">더보기</button></div>
|
||||
</section>
|
||||
|
||||
<!-- 덧글 패널 -->
|
||||
<section class="game-panel">
|
||||
<p class="section-tag">덧글 · 상대시각 · 글자수 카운터</p>
|
||||
<div class="game-reviews__composer" style="background: var(--surface); border-color: var(--border);">
|
||||
<label class="game-comments__label" for="cbody">내용</label>
|
||||
<textarea id="cbody" rows="2" maxlength="200" placeholder="여기에 덧글을 작성해 주세요."></textarea>
|
||||
<div class="game-comments__actions">
|
||||
<span class="game-comments__hint"><span id="ccount">0/200</span></span>
|
||||
<button type="submit">덧글 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="game-comments__list" id="comment-list"></ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// game-detail.jsp 승격본과 동일 로직 (좌표공식·미터바)
|
||||
var AXIS_KEYS = ['immersion','creativity','controls','completeness','sound','visual'];
|
||||
var AXIS_LABELS_KO = { immersion:'몰입성', creativity:'창의성', controls:'조작성', completeness:'완성도', sound:'사운드', visual:'비주얼' };
|
||||
var SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
var labelSeq = 0, hexSeq = 0;
|
||||
|
||||
function buildHexRadar(scores, cx, cy, R) {
|
||||
var canvas = cx * 2;
|
||||
var svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 ' + canvas + ' ' + canvas);
|
||||
svg.setAttribute('width', String(Math.round(canvas * 0.85)));
|
||||
svg.setAttribute('height', String(Math.round(canvas * 0.85)));
|
||||
svg.setAttribute('role', 'img');
|
||||
svg.setAttribute('class', 'game-axis-radar');
|
||||
var nums = AXIS_KEYS.map(function (k, i) { var v = scores ? Number(scores[i]) : 0; return (isFinite(v) && v > 0) ? v : 0; });
|
||||
var hasData = nums.some(function (v) { return v > 0; });
|
||||
function ptOnAxis(i, ratio) { var ang = (-90 + 60 * i) * Math.PI / 180; return { x: cx + R * ratio * Math.cos(ang), y: cy + R * ratio * Math.sin(ang) }; }
|
||||
function polyPoints(fn) { return AXIS_KEYS.map(function (k, i) { var p = ptOnAxis(i, fn(i)); return p.x.toFixed(2) + ',' + p.y.toFixed(2); }).join(' '); }
|
||||
var gid = 'hexfill-' + (++hexSeq);
|
||||
var defs = document.createElementNS(SVG_NS, 'defs');
|
||||
var grad = document.createElementNS(SVG_NS, 'radialGradient');
|
||||
grad.setAttribute('id', gid); grad.setAttribute('cx', '50%'); grad.setAttribute('cy', '50%'); grad.setAttribute('r', '62%');
|
||||
[['0%','rgba(240,201,120,0.55)'],['100%','rgba(232,165,75,0.26)']].forEach(function (s) {
|
||||
var st = document.createElementNS(SVG_NS,'stop'); st.setAttribute('offset', s[0]); st.setAttribute('stop-color', s[1]); grad.appendChild(st);
|
||||
});
|
||||
defs.appendChild(grad); svg.appendChild(defs);
|
||||
for (var level = 1; level <= 4; level++) {
|
||||
var grid = document.createElementNS(SVG_NS, 'polygon');
|
||||
grid.setAttribute('class', 'game-axis-grid' + (level === 4 ? ' game-axis-grid--outer' : ''));
|
||||
(function (lv) { grid.setAttribute('points', polyPoints(function () { return lv / 4; })); })(level);
|
||||
svg.appendChild(grid);
|
||||
}
|
||||
AXIS_KEYS.forEach(function (k, i) {
|
||||
var line = document.createElementNS(SVG_NS, 'line'); line.setAttribute('class', 'game-axis-line');
|
||||
var p = ptOnAxis(i, 1); line.setAttribute('x1', String(cx)); line.setAttribute('y1', String(cy));
|
||||
line.setAttribute('x2', p.x.toFixed(2)); line.setAttribute('y2', p.y.toFixed(2)); svg.appendChild(line);
|
||||
});
|
||||
if (hasData) {
|
||||
var poly = document.createElementNS(SVG_NS, 'polygon'); poly.setAttribute('class', 'game-axis-poly');
|
||||
poly.setAttribute('points', polyPoints(function (i) { return nums[i] / 5; })); poly.setAttribute('fill', 'url(#' + gid + ')'); svg.appendChild(poly);
|
||||
AXIS_KEYS.forEach(function (k, i) {
|
||||
if (nums[i] <= 0) return; var p = ptOnAxis(i, nums[i] / 5);
|
||||
var dot = document.createElementNS(SVG_NS, 'circle'); dot.setAttribute('class', 'game-axis-dot');
|
||||
dot.setAttribute('cx', p.x.toFixed(2)); dot.setAttribute('cy', p.y.toFixed(2)); dot.setAttribute('r', Math.max(1.6, R * 0.05).toFixed(2)); svg.appendChild(dot);
|
||||
});
|
||||
}
|
||||
svg.setAttribute('aria-label', hasData ? ('6축 평가: ' + AXIS_KEYS.map(function (k, i) { return AXIS_LABELS_KO[k] + ' ' + nums[i]; }).join(', ')) : '6축 평가 없음');
|
||||
return { svg: svg, nums: nums, hasData: hasData };
|
||||
}
|
||||
function buildRadarLegend(nums, hasData) {
|
||||
var dl = document.createElement('dl'); dl.className = 'game-reviews__axis-legend';
|
||||
AXIS_KEYS.forEach(function (k, i) {
|
||||
var row = document.createElement('div'); row.className = 'game-reviews__axis-legend-row';
|
||||
var dt = document.createElement('dt'); dt.textContent = AXIS_LABELS_KO[k];
|
||||
var meter = document.createElement('div'); meter.className = 'game-reviews__axis-meter';
|
||||
var bar = document.createElement('span'); bar.style.width = (hasData ? (nums[i] / 5 * 100) : 0) + '%'; meter.appendChild(bar);
|
||||
var dd = document.createElement('dd'); dd.textContent = hasData ? String(nums[i]) : '–';
|
||||
row.appendChild(dt); row.appendChild(meter); row.appendChild(dd); dl.appendChild(row);
|
||||
});
|
||||
return dl;
|
||||
}
|
||||
var axisScores = {};
|
||||
function buildStarRadioGroup(axisKey, labelText) {
|
||||
var labelId = 'star-' + axisKey + '-' + (++labelSeq);
|
||||
var group = document.createElement('div'); group.className = 'game-stars game-stars--input game-stars--axis';
|
||||
group.setAttribute('role', 'radiogroup'); group.setAttribute('aria-labelledby', labelId);
|
||||
var radios = [];
|
||||
function paint() { var sel = axisScores[axisKey] || 0; radios.forEach(function (b, idx) { var v = idx + 1; b.classList.toggle('is-on', v <= sel); b.setAttribute('aria-checked', v === sel ? 'true' : 'false'); b.tabIndex = (v === sel) ? 0 : -1; }); if (!sel) radios[0].tabIndex = 0; }
|
||||
function select(v) { axisScores[axisKey] = v; paint(); }
|
||||
for (var i = 1; i <= 5; i++) { (function (v) {
|
||||
var b = document.createElement('button'); b.type = 'button'; b.className = 'game-stars__btn'; b.setAttribute('role','radio'); b.setAttribute('aria-checked','false'); b.setAttribute('aria-label', labelText + ' ' + v + '점'); b.tabIndex = -1; b.textContent = '★';
|
||||
b.addEventListener('click', function () { select(v); b.focus(); });
|
||||
b.addEventListener('keydown', function (ev) { var cur = axisScores[axisKey] || 0; var next = null;
|
||||
switch (ev.key) { case 'ArrowRight': case 'ArrowUp': next = Math.min(5,(cur||v)+1); break; case 'ArrowLeft': case 'ArrowDown': next = Math.max(1,(cur||v)-1); break; case 'Home': next = 1; break; case 'End': next = 5; break; case ' ': case 'Enter': next = v; break; default: return; }
|
||||
ev.preventDefault(); select(next); radios[next-1].focus(); });
|
||||
radios.push(b); group.appendChild(b);
|
||||
})(i); }
|
||||
paint(); return { group: group, labelId: labelId };
|
||||
}
|
||||
function fillStars(c, rating) { c.innerHTML = ''; for (var i = 1; i <= 5; i++) { var s = document.createElement('span'); s.className = 'game-star' + (i <= rating ? ' is-on' : ''); s.textContent = '★'; c.appendChild(s); } }
|
||||
function buildStarDisplay(rating) { var w = document.createElement('div'); w.className = 'game-stars game-stars--display'; w.setAttribute('aria-hidden','true'); fillStars(w, rating); return w; }
|
||||
|
||||
// 요약 (큰 레이더 + 미터바 범례)
|
||||
fillStars(document.getElementById('avg-stars'), 4);
|
||||
(function () {
|
||||
var panel = document.getElementById('summary-panel');
|
||||
var radar = buildHexRadar([4.3, 4.6, 3.8, 4.4, 3.5, 4.7], 100, 100, 80);
|
||||
panel.appendChild(radar.svg); panel.appendChild(buildRadarLegend(radar.nums, true));
|
||||
})();
|
||||
|
||||
// 6축 입력 위젯
|
||||
(function () {
|
||||
var axesEl = document.getElementById('axes');
|
||||
AXIS_KEYS.forEach(function (k) {
|
||||
var row = document.createElement('div'); row.className = 'game-reviews__axis';
|
||||
var label = document.createElement('span'); label.className = 'game-reviews__axis-label'; label.textContent = AXIS_LABELS_KO[k];
|
||||
var w = buildStarRadioGroup(k, AXIS_LABELS_KO[k]); label.id = w.labelId;
|
||||
row.appendChild(label); row.appendChild(w.group); axesEl.appendChild(row);
|
||||
});
|
||||
var ov = buildStarRadioGroup('overall', '종합 별점'); var oEl = document.getElementById('overall');
|
||||
while (ov.group.firstChild) oEl.appendChild(ov.group.firstChild);
|
||||
})();
|
||||
|
||||
// 글자수 카운터 + 최소10자 경고
|
||||
function counter(ta, out, max, min) {
|
||||
function upd() { var len = ta.value.length; out.textContent = len + '/' + max; if (min) { var w = len > 0 && len < min; out.parentElement.classList.toggle('is-warn', w); } }
|
||||
ta.addEventListener('input', upd); upd();
|
||||
}
|
||||
counter(document.getElementById('rbody'), document.getElementById('rcount'), 1000, 10);
|
||||
counter(document.getElementById('cbody'), document.getElementById('ccount'), 200, 0);
|
||||
|
||||
// 리뷰 카드 (axes 있음/없음 두 케이스)
|
||||
function reviewCard(r) {
|
||||
var li = document.createElement('li'); li.className = 'game-reviews__item' + (r.mine ? ' is-mine' : '');
|
||||
var head = document.createElement('div'); head.className = 'game-reviews__item-head';
|
||||
head.appendChild(buildStarDisplay(r.rating));
|
||||
var nick = document.createElement('span'); nick.className = 'game-reviews__nick'; nick.textContent = r.nick; head.appendChild(nick);
|
||||
if (r.mine) { var mb = document.createElement('span'); mb.className = 'game-reviews__mine-badge'; mb.textContent = '내 리뷰'; head.appendChild(mb); }
|
||||
var t = document.createElement('time'); t.className = 'game-reviews__time'; t.textContent = r.time; head.appendChild(t);
|
||||
if (r.edited) { var ed = document.createElement('span'); ed.className = 'game-reviews__edited'; ed.textContent = '(수정됨)'; head.appendChild(ed); }
|
||||
li.appendChild(head);
|
||||
var p = document.createElement('p'); p.className = 'game-reviews__body'; p.textContent = r.body; li.appendChild(p);
|
||||
var cardScores = AXIS_KEYS.map(function (k) { return r.axes ? r.axes[k] : 0; });
|
||||
var cr = buildHexRadar(cardScores, 60, 60, 44);
|
||||
var wrap = document.createElement('div'); wrap.className = 'game-reviews__radar game-reviews__card-radar';
|
||||
if (cr.hasData) { wrap.appendChild(cr.svg); var lg = buildRadarLegend(cr.nums, true); lg.className += ' sr-only'; wrap.appendChild(lg); }
|
||||
else { var none = document.createElement('span'); none.className = 'game-reviews__radar-empty'; none.textContent = '6축 평가 데이터 없음'; wrap.appendChild(none); }
|
||||
li.appendChild(wrap);
|
||||
return li;
|
||||
}
|
||||
var rl = document.getElementById('review-list');
|
||||
rl.appendChild(reviewCard({ rating: 5, nick: '도토리', time: '12분 전', edited: false, mine: true, body: '몰입감이 정말 뛰어나요. 사운드가 조금 아쉽지만 전반적으로 완성도 높은 게임입니다.', axes: { immersion:5, creativity:5, controls:4, completeness:5, sound:3, visual:5 } }));
|
||||
rl.appendChild(reviewCard({ rating: 3, nick: '바삭이', time: '3일 전', edited: true, mine: false, body: '아이디어는 좋은데 조작이 좀 불편했어요. 다음 빌드 기대합니다.', axes: { immersion:3, creativity:4, controls:2, completeness:3, sound:3, visual:4 } }));
|
||||
rl.appendChild(reviewCard({ rating: 4, nick: '레거시유저', time: '2026. 5. 1.', edited: false, mine: false, body: '(구버전 리뷰 — 6축 데이터 없음 케이스)', axes: null }));
|
||||
|
||||
// 덧글 카드
|
||||
function commentCard(c) {
|
||||
var li = document.createElement('li'); li.className = 'game-comments__item';
|
||||
var av = document.createElement('div'); av.className = 'game-comments__avatar'; av.textContent = c.nick.charAt(0); li.appendChild(av);
|
||||
var body = document.createElement('div'); body.style.flex = '1';
|
||||
var meta = document.createElement('div'); meta.className = 'game-comments__meta';
|
||||
var nick = document.createElement('span'); nick.className = 'game-comments__nick'; nick.textContent = c.nick; meta.appendChild(nick);
|
||||
var t = document.createElement('time'); t.textContent = c.time; meta.appendChild(t);
|
||||
if (c.edited) { var ed = document.createElement('span'); ed.className = 'game-comments__edited'; ed.textContent = '(수정됨)'; meta.appendChild(ed); }
|
||||
body.appendChild(meta);
|
||||
var p = document.createElement('p'); p.textContent = c.body; body.appendChild(p); li.appendChild(body);
|
||||
return li;
|
||||
}
|
||||
var cl = document.getElementById('comment-list');
|
||||
cl.appendChild(commentCard({ nick: '플레이어A', time: '방금 전', edited: false, body: '재밌게 했습니다! 2스테이지 보스가 인상적이네요.' }));
|
||||
cl.appendChild(commentCard({ nick: '(탈퇴한 사용자)', time: '5일 전', edited: false, body: '탈퇴자 댓글 마스킹 케이스 — 이름이 "(탈퇴한 사용자)" 로 표시됩니다.' }));
|
||||
|
||||
// 테마 토글
|
||||
document.getElementById('theme-toggle').addEventListener('click', function () {
|
||||
var dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'light' : 'dark');
|
||||
this.textContent = dark ? '🌙 다크' : '☀️ 라이트';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
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/ 업데이트.
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
---
|
||||
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=<int≥0, default 0>&sort=<enum, default 토글기본값>`.
|
||||
- **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 `<choose>`(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<ReviewAxisRow> axes); // 6행(axis_key+score)
|
||||
|
||||
// 수정 시 기존 축 전삭제(이후 add 재삽입).
|
||||
int deleteReviewAxes(@Param("reviewId") long reviewId); // 대상 리뷰
|
||||
|
||||
// 목록 화면 N리뷰 축 batch 조회(N+1 회피).
|
||||
List<ReviewAxisRow> listAxesByReviewIds(@Param("reviewIds") List<Long> reviewIds); // 페이지 리뷰 id들
|
||||
}
|
||||
```
|
||||
- `ReviewAxisRow` = `{ Long reviewId; String axisKey; Integer score; }` (신규 POJO 또는 GameReviewData 내 정적 중첩). axes batch insert SQL은 `<foreach>` (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)
|
||||
```
|
||||
- `<polygon points="ptX_0,ptY_0 ptX_1,ptY_1 ... ptX_5,ptY_5" />` (fill 반투명 + stroke).
|
||||
|
||||
### 6.5 렌더 함수 시그니처 (inflate 방지 — 인자 사용목적 명시)
|
||||
```js
|
||||
// 6축 점수 배열을 육각형 SVG 엘리먼트로. 요약/카드 공용.
|
||||
function buildHexRadar(scores /* [6] axis순 점수배열, score폴리곤 */,
|
||||
cx /* 중심x, 좌표기준 */,
|
||||
cy /* 중심y, 좌표기준 */,
|
||||
R /* 반지름, 스케일 */) { ... return <svg> }
|
||||
```
|
||||
- 최소 인자. 라벨 텍스트/색상은 함수 내부 상수(AXIS_LABELS_KO) 참조 → 인자 미부풀림.
|
||||
|
||||
### 6.6 a11y (SVG 시각요소 텍스트 대체 — 필수)
|
||||
- `<svg role="img" aria-label="6축 평가: 몰입성 4, 창의성 5, 조작성 3, 완성도 4, 사운드 2, 비주얼 5">`.
|
||||
- 추가로 시각적 보조: 각 축 점수를 visually-hidden 표 또는 인접 `<dl>`(축명/점수) 동반. 폴리곤만으로 종료 금지(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' <provider 또는 매퍼 소스>` >= 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("a | ||||