diff --git a/.atp/work-session/20260701-102212/report.md b/.atp/work-session/20260701-102212/report.md index e619b28..2fa7f1c 100644 --- a/.atp/work-session/20260701-102212/report.md +++ b/.atp/work-session/20260701-102212/report.md @@ -54,3 +54,19 @@ game-detail.jsp `.game-reviews__axis-meter` 에 `::before` overlay(repeating-lin ## graph_refresh fresh (graph-refresh-checker 판정) — CSS-only 변경, 구조적 시그널 0, index.md §285 스캔범위 외 확인. 재생성 불필요. + +## Summary (추가 — 육각 레이더 가시성) +사용자 정정: 앞선 미터바 수정과 별개로 원 지적은 "육각 레이더" 자체였음. +추가 지적: 육각형만으론 어떤 축인지도 식별 불가. +AskUserQuestion 2회 (1회는 한글 인코딩 손상으로 재질문) 후 "축 이름 + 점수 둘 다 표시" 선택. + +## Decisions (추가) +- buildHexRadar(scores, cx, cy, R, withLabels) 5번째 파라미터 추가 — summary 레이더만 true, 리뷰카드 소형 레이더는 기존대로 라벨 없음(caption 밀도 문제로 스코프 제외, 사용자 확인 없이 orchestrator 판단 — 필요시 후속 피드백으로). +- 동심 그리드 4단 → 5단 전환(1/5..5/5) — 0~5점 스케일과 정수 격자 1:1 대응 (미터바 눈금 판단과 동일 원리). +- 각 축 끝에 축 이름(AXIS_LABELS_KO) SVG text 배치, 각 데이터 꼭지점 옆에 점수 SVG text 배치. +- 최초 구현 시 우측 라벨(창의성/조작성/사운드)이 우측 dl 범례와 겹침 발견(zoom 스크린샷) → 원인: buildRadarLegend가 radar wrapper div 내부(svg 옆)에 append되는 구조라 panel gap이 아닌 `.game-reviews__radar` 자체 gap 부재가 원인. `.game-reviews__radar { gap: 1.75rem }` 추가로 해결(sr-only 카드 레이더는 position:absolute라 영향 없음 확인). 라벨 배치 ratio도 1.22→1.14로 낮춰 상/하단 라벨 패널 경계 잘림 해소. + +## verified_by_me (추가) +- 라이트/다크 테마 각각 zoom 스크린샷 2라운드(1차 겹침 발견 → 원인 수정 → 2차 재확인 겹침 없음, 라벨 6개 전부 판독 가능) +- 리뷰카드(소형) 레이더 find 로 6개 확인 — 라벨 미부착 유지, 레이아웃 무변경 회귀 없음 +- JSP 런타임 스모크: 매 수정 후 docker compose restart app → curl 200 + grep 대조 diff --git a/src/main/webapp/WEB-INF/views/game-detail.jsp b/src/main/webapp/WEB-INF/views/game-detail.jsp index 038a049..90220ed 100644 --- a/src/main/webapp/WEB-INF/views/game-detail.jsp +++ b/src/main/webapp/WEB-INF/views/game-detail.jsp @@ -1018,6 +1018,7 @@ .game-reviews__radar { display: inline-flex; flex-shrink: 0; + gap: 1.75rem; } .game-reviews__radar svg { display: block; @@ -1056,6 +1057,18 @@ stroke-width: 1.5; animation: game-radar-fade 0.7s ease both; } + .game-reviews__radar .game-axis-axislabel { + font-size: 11px; + font-weight: 600; + fill: var(--text-muted); + } + .game-reviews__radar .game-axis-scorelabel { + font-size: 10px; + font-weight: 700; + fill: var(--accent); + font-variant-numeric: tabular-nums; + 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); } @@ -1618,7 +1631,7 @@ // ===== 육각형 레이더 SVG (요약/카드 공용) ===== // scores: AXIS_KEYS 순 6값(1~5, null/0=데이터 없음). cx/cy/R 좌표. var hexSeq = 0; // svg 별 그라디언트 id 고유화 - function buildHexRadar(scores, cx, cy, R) { + function buildHexRadar(scores, cx, cy, R, withLabels) { var canvas = cx * 2; // cx==cy 가정. 요약 200 / 카드 120 var svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('viewBox', '0 0 ' + canvas + ' ' + canvas); @@ -1664,16 +1677,18 @@ defs.appendChild(grad); svg.appendChild(defs); - // 동심 그리드 (4단계, 외곽 강조) - for (var level = 1; level <= 4; level++) { + // 동심 그리드 (5단계 = 1~5점 정수 등급과 1:1 대응, 외곽 강조) + for (var level = 1; level <= 5; level++) { var grid = document.createElementNS(SVG_NS, 'polygon'); - grid.setAttribute('class', 'game-axis-grid' + (level === 4 ? ' game-axis-grid--outer' : '')); + grid.setAttribute('class', 'game-axis-grid' + (level === 5 ? ' game-axis-grid--outer' : '')); (function (lv) { - grid.setAttribute('points', polyPoints(function () { return lv / 4; })); + grid.setAttribute('points', polyPoints(function () { return lv / 5; })); })(level); svg.appendChild(grid); } - // 축선 + // 축선 + (withLabels 시) 축 이름 라벨 — 육각형만으론 어떤 축인지 식별 불가(§6.6) + // 6축 순서상 위치 고정: 0=위, 1=우상, 2=우하, 3=아래, 4=좌하, 5=좌상 + var AXIS_LABEL_ANCHOR = ['middle', 'start', 'start', 'middle', 'end', 'end']; AXIS_KEYS.forEach(function (k, i) { var line = document.createElementNS(SVG_NS, 'line'); line.setAttribute('class', 'game-axis-line'); @@ -1683,6 +1698,18 @@ line.setAttribute('x2', p.x.toFixed(2)); line.setAttribute('y2', p.y.toFixed(2)); svg.appendChild(line); + + if (withLabels) { + var lp = ptOnAxis(i, 1.14); + var label = document.createElementNS(SVG_NS, 'text'); + label.setAttribute('class', 'game-axis-axislabel'); + label.setAttribute('x', lp.x.toFixed(2)); + label.setAttribute('y', lp.y.toFixed(2)); + label.setAttribute('text-anchor', AXIS_LABEL_ANCHOR[i]); + label.setAttribute('dominant-baseline', i === 0 ? 'text-after-edge' : (i === 3 ? 'text-before-edge' : 'middle')); + label.textContent = AXIS_LABELS_KO[k]; + svg.appendChild(label); + } }); // score 폴리곤 + 꼭지점 점 (데이터 있을 때만) if (hasData) { @@ -1693,13 +1720,27 @@ svg.appendChild(poly); AXIS_KEYS.forEach(function (k, i) { if (nums[i] <= 0) return; - var p = ptOnAxis(i, nums[i] / 5); + var ratio = nums[i] / 5; + var p = ptOnAxis(i, ratio); 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); + + if (withLabels) { + // 꼭지점에서 살짝 바깥으로 띄워 점 위에 겹치지 않게 배치 + var sp = ptOnAxis(i, ratio + 14 / R); + var score = document.createElementNS(SVG_NS, 'text'); + score.setAttribute('class', 'game-axis-scorelabel'); + score.setAttribute('x', sp.x.toFixed(2)); + score.setAttribute('y', sp.y.toFixed(2)); + score.setAttribute('text-anchor', AXIS_LABEL_ANCHOR[i]); + score.setAttribute('dominant-baseline', 'middle'); + score.textContent = String(nums[i]); + svg.appendChild(score); + } }); } @@ -2277,7 +2318,7 @@ if (rSummaryPanel && rSummaryRadar) { rSummaryRadar.innerHTML = ''; var scores = AXIS_KEYS.map(function (k) { return summary.axes ? Number(summary.axes[k]) || 0 : 0; }); - var radar = buildHexRadar(scores, 100, 100, 80); + var radar = buildHexRadar(scores, 100, 100, 80, true); rSummaryPanel.hidden = false; if (radar.hasData) { rSummaryRadar.appendChild(radar.svg);