style(design): FINDING-010 — Crawl.tsx 하드코딩 hex 를 @theme 토큰으로 단일화 [subagent]

Crawl 이 dead-600/ink-400/cost-ai 와 동일한 값을 raw hex 로 재복제해
토큰 변경 시 어긋나는 이중 정의 상태였다. 종료사유 팔레트를 상태
토큰(ok/run/warn/dead)+신규 neutral-300/400 으로 치환, ReferenceLine·
막대·폴백 hex 전부 var() 참조로. src 내 raw hex 0 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-13 22:53:58 +09:00
parent 433b973505
commit 10ea8ddd45
2 changed files with 18 additions and 13 deletions

View File

@ -34,6 +34,10 @@
--color-dead-600: #d03b3b;
--color-dead-50: #fdeaea;
/* 중립 계열(차트 보조 — 유휴/종료 등 '무해한 상태') */
--color-neutral-400: #898781;
--color-neutral-300: #c3c2b7;
/* 차트 시리즈(엔터티 고정) */
--color-naver: #008a43;
--color-coupang: #f0455b;

View File

@ -10,15 +10,16 @@ import type { BotStatsRes, IpSessionStatsRes } from "../api/types";
import { Card, Empty, ErrorNote, Loading } from "../components/ui";
import { dateShort, durationSec, hhmm } from "../lib/format";
// 종료 사유 — 의미 기반 상태색(정상 회전=초록 계열, 차단=적색, 포트사망=주황, 중립=회색/파랑).
// 종료 사유 — 의미 기반 상태색. 색은 전부 index.css @theme 토큰 참조(단일 소스) —
// raw hex 재정의 금지(토큰을 바꾸면 여기도 함께 바뀌어야 한다).
const REASONS: Record<string, { label: string; color: string; desc: string }> = {
budget: { label: "예산 선제 회전", color: "#0ca30c", desc: "정상 — 차단 전에 IP 교체(평판 보존)" },
window: { label: "시간창 만료", color: "#2a78d6", desc: "정상 — sticky 10분 주기 교체" },
idle: { label: "유휴 정리", color: "#898781", desc: "정상 — 한동안 검색 없어 브라우저 회수" },
shutdown: { label: "종료", color: "#c3c2b7", desc: "정상 — 워커 재시작/종료" },
rotate: { label: "기타 회전", color: "#9aa1ac", desc: "웜업 재시도 등" },
proxy_error: { label: "포트 사망", color: "#ec835a", desc: "주의 — 프록시 전송 실패로 교체" },
block: { label: "차단됨", color: "#d03b3b", desc: "위험 — 예산 안에서도 차단(예산 하향 검토)" },
budget: { label: "예산 선제 회전", color: "var(--color-ok-600)", desc: "정상 — 차단 전에 IP 교체(평판 보존)" },
window: { label: "시간창 만료", color: "var(--color-run-600)", desc: "정상 — sticky 10분 주기 교체" },
idle: { label: "유휴 정리", color: "var(--color-neutral-400)", desc: "정상 — 한동안 검색 없어 브라우저 회수" },
shutdown: { label: "종료", color: "var(--color-neutral-300)", desc: "정상 — 워커 재시작/종료" },
rotate: { label: "기타 회전", color: "var(--color-ink-400)", desc: "웜업 재시도 등" },
proxy_error: { label: "포트 사망", color: "var(--color-warn-600)", desc: "주의 — 프록시 전송 실패로 교체" },
block: { label: "차단됨", color: "var(--color-dead-600)", desc: "위험 — 예산 안에서도 차단(예산 하향 검토)" },
};
const RANGES = [
@ -41,7 +42,7 @@ export default function Crawl() {
});
const reasons = Object.entries(ip.data?.by_reason ?? {})
.map(([k, v]) => ({ key: k, ...(REASONS[k] ?? { label: k, color: "#9aa1ac", desc: "" }), value: v }))
.map(([k, v]) => ({ key: k, ...(REASONS[k] ?? { label: k, color: "var(--color-ink-400)", desc: "" }), value: v }))
.sort((a, b) => b.value - a.value);
const totalSessions = reasons.reduce((s, r) => s + r.value, 0);
const blockCount = ip.data?.by_reason?.block ?? 0;
@ -122,8 +123,8 @@ export default function Crawl() {
<Tooltip formatter={(v: number) => [`${v}`, "세션 수"]} labelFormatter={(l) => `IP당 ${l}회 요청`} />
<Bar dataKey="count" fill="var(--color-cost-ai)" radius={[4, 4, 0, 0]} maxBarSize={36} isAnimationActive={false} />
{ip.data.block_min_requests != null && (
<ReferenceLine x={ip.data.block_min_requests} stroke="#d03b3b" strokeDasharray="4 3"
label={{ value: `차단 시작 ${ip.data.block_min_requests}`, position: "top", fill: "#d03b3b", fontSize: 11, fontWeight: 600 }} />
<ReferenceLine x={ip.data.block_min_requests} stroke="var(--color-dead-600)" strokeDasharray="4 3"
label={{ value: `차단 시작 ${ip.data.block_min_requests}`, position: "top", fill: "var(--color-dead-600)", fontSize: 11, fontWeight: 600 }} />
)}
</BarChart>
</ResponsiveContainer>
@ -147,7 +148,7 @@ export default function Crawl() {
<XAxis dataKey="x" tick={{ fill: "var(--color-ink-400)" }} tickLine={false} axisLine={{ stroke: "var(--color-line-200)" }} minTickGap={40} />
<YAxis allowDecimals={false} tick={{ fill: "var(--color-ink-400)" }} tickLine={false} axisLine={false} />
<Tooltip formatter={(v: number) => [`${v}`, "차단"]} />
<Bar dataKey="count" fill="#d03b3b" radius={[4, 4, 0, 0]} maxBarSize={24} isAnimationActive={false} />
<Bar dataKey="count" fill="var(--color-dead-600)" radius={[4, 4, 0, 0]} maxBarSize={24} isAnimationActive={false} />
</BarChart>
</ResponsiveContainer>
)}
@ -202,7 +203,7 @@ export default function Crawl() {
</thead>
<tbody>
{ip.data.sessions.map((s, i) => {
const r = REASONS[s.end_reason] ?? { label: s.end_reason, color: "#9aa1ac" };
const r = REASONS[s.end_reason] ?? { label: s.end_reason, color: "var(--color-ink-400)" };
return (
<tr key={i} className="border-b border-line-100">
<td className="tnum py-1.5 pr-2 text-ink-500">{dateShort(s.created_at)}</td>