13 KiB
| kind | title | session_id | resumed_from | created_at | status | related_prev_change | related_security_checklist | related_work_log | related_design | related_report |
|---|---|---|---|---|---|---|---|---|---|---|
| change | W3-2 댓글/리뷰 고도화 — 다축 평점·일관성·목록규모·UX | 20260622-092800 | 20260618-145152 | 2026-06-22 | implemented | ./2026-06-18-w3-2-comments-reviews.md | ../security/security-remediation-checklist.md | ../work-log/2026-06-17-jam-platform-roadmap.md | ../../.atp/work-session/20260622-092800/implementation/design.md | ../../.atp/work-session/20260622-092800/report.md |
W3-2 댓글/리뷰 고도화 변경 이력
상위 코어 이력: 2026-06-18 W3-2 댓글/리뷰 분리 구현
개요
2026-06-18 코어 구현(댓글/리뷰 서버 영속화·CRUD) 위에 4개 축의 고도화를 얹었다.
| 고도화 축 | 주요 내용 |
|---|---|
| A 일관성 | commentView 단일 통일(A1) + 작성자명 하이브리드 마스킹 QG-2 해결(A2) + edited/updatedAt(A3) |
| B 목록규모 | 페이지네이션 더보기 20건(B1) + 리뷰 본문 최소 10자(B2) + TextNormalizer 제어문자 정규화(B3) |
| 다축 평점 | game_review_axes 6축(리뷰당 6행) + overall 자동평균/수동덮어쓰기(is_rating_manual) + game_review_stats 집계뷰 |
| C UX | submit 잠금(C1) + 상대시각(C2) + 글자수 카운터(C4) + 완전소멸 유지(C5) + radiogroup 완전 패턴 roving tabindex(C6) + 육각형 SVG 레이더 |
프론트엔드 구현(game-detail.jsp 시각 컴포넌트)은 frontend-design 스킬 패스로 처리됐다.
C3 재분류 — 직전 코어 이월 항목 해소 (중요)
배경
직전 코어 이력(2026-06-18-w3-2-comments-reviews.md) 이월 항목 1번:
"W2-3 평점 집계 계약 (SELECT AVG/COUNT) — W2-6 시상 후속."
이 이월 표기는 game_review_stats 집계뷰가 W2-3 동결 묶음에 속한다는 오분류에 근거했다. 본 세션에서 정정 확인 후 해소했다.
정정 근거
docs/work-log/2026-06-17-jam-platform-roadmap.md 3개 지점:
:63— "W2-3 범위 = 잼 평가만. 댓글/리뷰 스키마 자체는 W3에서 설계(동결 묶음 아님).":201— "잼 평가(심사/투표/시상) 스키마만 동결. 댓글/리뷰 스키마는 W3-2에서 별도 설계.":202— "댓글/리뷰 분리 → W3-2 일반기능으로 재분류: 잼 평가 동결묶음에서 분리."
결론
game_review_stats = game_reviews(W3-2 테이블) 위 읽기전용 집계뷰 신설 = W3-2 일반 DDL. W2-3 잼 평가(심사/투표/시상) 스키마 동결과 완전히 무관하다. §6 파괴적 게이트 / 사용자 재확인 경로 불필요 — 본 세션에서 일반 CREATE VIEW 로 처리 완료.
부수 무효화
아래 두 보수 주석이 본 변경으로 무효화됐다(구현 완료 시 갱신 대상):
| 위치 | 기존 보수 주석 | 상태 |
|---|---|---|
docs/game-reviews-ddl.sql:63 |
"집계 컬럼/뷰는 신설하지 않음 (W2-3 동결 보호)" | 본 DDL 블록 추가로 무효화 |
db/schema.sql:136-137 |
"집계 컬럼/뷰는 W2-3 동결 — 신설 금지" | 동기화 DDL 블록으로 무효화 |
ADR 권고: W2-3 동결 경계 재확인(잼 평가 한정, 댓글/리뷰 무관)은 미래 세션에서
adr/ADR-0001로 정식화할 가치가 있다. 현재는 직전 오분류를 정정하는 수준이므로changes/내 섹션으로 기록하되, W2-3 관련 설계 분기가 재발하면 ADR 발행을 권고한다.
DDL 변경
신규 테이블: game_review_axes
리뷰당 정확히 6행. UNIQUE(review_id, axis_key)로 중복 방지.
| 컬럼 | 타입 | 비고 |
|---|---|---|
id |
bigint PK | game_review_axes_id_seq |
review_id |
bigint NOT NULL | FK → game_reviews(id) |
axis_key |
varchar(20) NOT NULL | CHECK 6종 (아래 참조) |
score |
smallint NOT NULL | CHECK(1~5) |
axis_key 6종 (육각형 축 인덱스 0~5 순서 고정):
immersion(몰입성) / creativity(창의성) / controls(조작성) / completeness(완성도) / sound(사운드) / visual(비주얼)
game_comments.updated_at (멱등 ALTER)
timestamptz DEFAULT now() NOT NULL 추가. edited = updated_at > created_at (리뷰 대칭). ALTER 직후 기존 댓글 updated_at = created_at 정렬 UPDATE 1회 적용(오표시 방지, 멱등).
game_reviews.is_rating_manual (멱등 ALTER)
boolean DEFAULT false NOT NULL 추가. true = 유저 직접 선택 overall, false = 6축 자동평균.
game_review_stats (VIEW 신규)
game_reviews(is_delete IS NOT TRUE) LEFT JOIN 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) 폐기 공급원.
적용 대상 파일
docs/game-reviews-ddl.sql— 멱등 블록 append(+ :63 보수주석 갱신)db/schema.sql— 동일 정의 동기화(+ :136-137 주석 갱신)
적용 상태: needs_user_verification (dev: schema.sql 재부트 or ddl 수동 실행)
런타임 동작 변화
A1 — commentView 단일 통일
| 항목 | 이전 | 이후 |
|---|---|---|
| POST /comments 응답 | flat(commentId/gameId/authorName/userId/content) | commentView 전체(+createdAt/edited/updatedAt) |
| PUT /comments/{id} 응답 | 부분(commentId/content) | commentView 전체 |
| commentView 키 수 | 가변 | 고정 8키 |
A2 — 하이브리드 작성자명 + 탈퇴자 마스킹 (QG-2 해결)
GameCommentsMapper LEFT JOIN users + CASE 3분기:
u.id IS NULL→ 스냅샷 닉네임(레거시 user_id NULL 레코드 역호환)u.is_delete→"(탈퇴한 사용자)"마스킹- else →
u.display_name
QG-2(레거시 user_id NULL 레코드 authorName 빈값) 자동 해결.
A3 — 댓글 edited/updatedAt
댓글 수정 시 updated_at = now() 갱신. edited = (updated_at > created_at). nickname 덮어쓰기 없음.
B1 — 페이지네이션 (limit+1 hasMore 방식)
| 항목 | 이전 | 이후 |
|---|---|---|
| 목록 반환 형태 | { status, items[] } |
{ status, items[], hasMore } |
| 요청 파라미터 | 없음 | ?page=<int≥0>&sort=<enum> |
| 더보기 방식 | 전체 반환 | 20건 고정, limit+1 조회 후 hasMore 판정 |
sort enum:
| 대상 | 허용값 | 기본값 |
|---|---|---|
| 댓글 | oldest, newest |
oldest |
| 리뷰 | newest, rating_desc, rating_asc |
newest |
미허용 값 → 기본값 fallback (400 미반환). ORDER BY는 @SelectProvider 컴파일타임 상수 분기 — ${} 동적치환 0건. offset/limit은 #{} 바인딩.
B2 — 리뷰 본문 최소 10자
TextNormalizer.normalize() 후 trim 10자 미만 → 400. 댓글은 현행 유지(상한 200자만).
B3 — TextNormalizer 제어문자 정규화
신규 com.pandoli365.bibimbap.util.TextNormalizer.normalize(String raw):
- C0/C1 제어문자(U+0000~U+001F, U+007F~U+009F) 중
\t(U+0009)·\n·\r제외 후 제거 - 외곽
strip(). 내부 공백/줄바꿈 보존. - 댓글 content / 리뷰 body 검증 전 적용.
다축 평점 — overall 자동/수동
| 입력 | overall 결정 | is_rating_manual |
|---|---|---|
| overall 미전송/빈값 | rating = Math.round(6축 평균) (서버 계산, HALF_UP) |
false |
| overall 직접 전송 | rating = 전송값(1~5 검증) |
true |
axes 재저장 전략(editReview): deleteReviewAxes(reviewId) → addReviewAxes(reviewId, 6행) (TX 원자성).
프론트엔드 (frontend-design 스킬 패스)
frontend-design 스킬로 처리된 game-detail.jsp 변경 요약. 기존 앰버/크림 디자인 언어 확장, 외부 의존성 추가 없음.
| 컴포넌트 | 내용 |
|---|---|
| 육각형 SVG 레이더 | 인라인 SVG 자체 구현. 요약(viewBox 200×200, R=80) + 개별 리뷰 카드 컴팩트(viewBox 120×120, R=44). 방사 그라디언트·글로우·등장 모션. role="img" aria-label 6축 점수 텍스트 대체(a11y 필수). |
| 6축 radiogroup (C6) | buildStarRadioGroup(axisKey, labelText) 공용 함수. roving tabindex(선택 radio tabindex="0", 나머지 -1). 키보드: ←→↑↓ 이동+즉시선택, Home=1/End=5, Space/Enter 확정, preventDefault() 스크롤 방지. |
| C1 submit 잠금 | 제출 중 버튼 disabled + 로딩 표시, 응답 후 해제. 연타 차단. |
| C2 상대시각 | "n분 전" 표시 + title 절대시각. |
| C4 글자수 카운터 | 댓글 n/200, 리뷰 n/1000 + 최소 10자 안내 실시간 표시. |
| C5 완전소멸 | 삭제 항목 DOM 제거 유지. |
| 서버집계 표시 | game_review_stats 평균 소수 1자리, review_count=0 → "아직 평가 없음". 클라 평균계산(updateSummary 구버전) 제거. |
신규/변경 파일
신규 (4개)
| 파일 | 설명 |
|---|---|
src/.../data/ReviewAxisRow.java |
축 평점 행 POJO (reviewId/axisKey/score) |
src/.../mapper/GameReviewAxesMapper.java |
axes add/deleteAll/listByReviewIds |
src/.../mapper/GameReviewStatsMapper.java |
game_review_stats 1행 조회 |
src/.../util/TextNormalizer.java |
B3 제어문자 정규화 static util |
변경 (13개)
| 파일 | 변경 내용 |
|---|---|
docs/game-reviews-ddl.sql |
고도화 DDL 블록 append(:63 보수주석 갱신) |
db/schema.sql |
동기화(:136-137 동결주석 C3 갱신) |
src/.../data/GameCommentData.java |
updatedAt + edited 필드 추가 |
src/.../data/GameReviewData.java |
ratingManual + axes(Map) 필드 추가 |
src/.../mapper/GameCommentsMapper.java |
A2 LEFT JOIN+CASE, A3 updated_at, B1 offset/limit/sort |
src/.../mapper/GameReviewsMapper.java |
B1 offset/limit/sort, is_rating_manual, @SelectProvider |
src/.../controller/api/GameCommentController.java |
A1 재조회·commentView, B1 page/sort, B3 normalize |
src/.../controller/api/GameReviewController.java |
다축 검증/저장, overall 자동·수동, B2 10자, B3, B1, summary |
src/.../controller/api/GameController.java |
gameDetail SSR 신규 매퍼 주입 (영향맵 누락→보정 완료) |
src/main/webapp/WEB-INF/views/game-detail.jsp |
육각형 SVG, C1~C6, 페이지네이션·sort UI, 서버집계, 6축 입력 |
src/test/.../GameCommentControllerTest.java |
A1·A2·B3 신규 + 기존 12건 유지 |
src/test/.../GameReviewControllerTest.java |
다축·B2·sort 신규 + 기존 13건 유지 |
src/test/.../BibimbapApplicationTests.java |
신규 매퍼(GameReviewAxesMapper·GameReviewStatsMapper) @MockBean 추가 |
API 계약 변화
commentView 스키마 (A1 통일 — POST·PUT·list 동일)
{
"commentId": 100,
"gameId": 1,
"authorName": "표시명 또는 (탈퇴한 사용자) 또는 스냅샷닉",
"userId": 7,
"content": "...",
"createdAt": "ISO-8601",
"edited": false,
"updatedAt": "ISO-8601"
}
reviewView 스키마 (axes 추가)
기존 reviewView(reviewId/gameId/authorName/userId/rating/body/edited/createdAt/updatedAt) + 신규:
{
"ratingManual": false,
"axes": { "immersion":4,"creativity":5,"controls":3,"completeness":4,"sound":2,"visual":5 }
}
GET list 응답 형태
- 댓글:
{ status, comments:[commentView...], hasMore } - 리뷰:
{ status, reviews:[reviewView...], hasMore, summary }summary={ avgRating, reviewCount, axes:{immersion..visual} }(review_count=0 이면null)
요청 파라미터 신규
?page=<int≥0, default 0>&sort=<enum, default 토글기본값>
검증 결과 요약
| 레이어 | 결과 | 비고 |
|---|---|---|
| L1 단위 테스트 (43건) | GREEN — 43/43 Failures 0 | 기존 31건 무회귀. FIX LOOP 1회(테스트 계약 미갱신 8건, 컨트롤러 무버그 — body<10자 정당거부). verification-advisor 2차 독립 재판정 확정. |
| AC-4 sort 5분기 | PASS | @SelectProvider 분기 5건 전수, ORDER BY 상수 |
AC-5 ${} 0건 |
PASS | 신규·변경 매퍼 전수(주석 3건 제외) |
| AC-6 axis_key 6키 정합 | PASS | DDL CHECK·뷰 FILTER·앱 enum·JSP 라벨 4곳 일치 |
| L2 contract-DB | skip | 원격 DB 미기동 |
| L3 브라우저 스모크 | needs_user_verification | DDL 적용 후 dev 실게임 대상 |
| DDL 적용 | needs_user_verification | game_review_axes/stats/updated_at/is_rating_manual |
FIX LOOP 상세: report.md#Invocations implementation-advisor(FIX LOOP §2.6) — 근본원인=테스트 계약 미갱신(컨트롤러 무버그). body<10자 7곳→10자+ 교체, 행위 assertion 보존, 계약 무약화. main 코드 무수정.
범위 밖 / 이월
| 항목 | 사유 |
|---|---|
| 신고·숨김 | W1 운영자 role 선행 필요 |
| 리뷰 이력 테이블 | in-row 마커(updated_at)만. 열람 권한 정책 확정 시 별도 |
| 좋아요 서버화 | 별도 관리 (여전히 localStorage) |
| GET /reviews/mine | applyReviewGate 정밀화 — 본인 리뷰 첫 페이지 밖 폼 노출 가능. 서버 409 최종 차단(비차단). 차기 세션 후보 |
| 댓글 더보기 정렬 정합 | 낙관 삽입 2페이지+ 정렬 어긋남 가능. 리뷰는 page0 재조회로 정합, 비차단. 차기 세션 후보 |
| 기존 리뷰 axes 백필 | 기존 game_reviews 행은 axes 0행 — stats 뷰 6축 평균 NULL. 백필 필요 시 별도 |