fix(review): 6축 육각 레이더에 축 이름 + 점수 라벨 직접 표시

미터바 픽스와 별개로 실제 지적 대상은 레이더 차트였음 — 육각형만으론
어떤 축인지, 꼭지점이 어떤 점수인지 식별 불가. buildHexRadar 에
withLabels 옵션 추가(요약 레이더만 적용, 리뷰카드 소형 레이더는
밀도 문제로 제외)해 각 축 끝에 이름을, 꼭지점 옆에 점수를 SVG
text로 배치. 동심 그리드도 4단→5단으로 바꿔 0~5점 정수 스케일과
1:1 대응시킴.

구현 중 우측 축 라벨이 옆 범례(dl)와 겹치는 문제 발견 — 원인은
범례가 radar wrapper 내부에 append되는 구조라 `.game-reviews__radar`
자체에 gap이 없었던 것. gap 추가로 해결, 라이트/다크 양쪽 스크린샷
재확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h6etRXJLx1xdfxmVcHjBg
This commit is contained in:
이정수 2026-07-01 10:41:07 +09:00
parent 93efe21329
commit a6417dd116
2 changed files with 65 additions and 8 deletions

View File

@ -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 대조

View File

@ -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);