style(design): FINDING-011 — 타이포 스케일 정수 6단으로 통일 (본문 14px) [subagent]
작은 앱에 0.5px 간격 10종(10.5~22)이 혼재 — 12 vs 12.5 는 위계를
만들지 못하고 half-pixel 은 zoom/서브픽셀에서 반올림이 갈린다.
{11,12,13,14,17,22} 정수 6단으로 정규화(순서 보존 매핑), 본문
13.5→14px(FINDING-004). 빌드·렌더 검증.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
29e54ad838
commit
645a0362b0
@ -25,10 +25,10 @@ export default function Layout() {
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-gradient-to-br from-primary-600 to-primary-400 text-[14px] font-extrabold text-white">L</span>
|
||||
<div>
|
||||
<div className="text-[14px] font-bold leading-tight">LPS 관리자</div>
|
||||
<div className="text-[10.5px] font-medium text-ink-400">인터넷 최저가 검색</div>
|
||||
<div className="text-[11px] font-medium text-ink-400">인터넷 최저가 검색</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-2 text-[11.5px] font-semibold text-ink-500 md:hidden">
|
||||
<div className="flex items-center gap-2 px-2 text-[12px] font-semibold text-ink-500 md:hidden">
|
||||
<span className={`h-2 w-2 rounded-full ${up ? "bg-ok-600" : "bg-dead-600"}`} aria-hidden />
|
||||
{up ? "연결됨" : "연결 안 됨"}
|
||||
</div>
|
||||
@ -52,7 +52,7 @@ export default function Layout() {
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="mt-8 hidden items-center gap-2 px-3 text-[11.5px] font-semibold text-ink-500 md:flex">
|
||||
<div className="mt-8 hidden items-center gap-2 px-3 text-[12px] font-semibold text-ink-500 md:flex">
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${up ? "bg-ok-600" : "bg-dead-600"}`}
|
||||
aria-hidden
|
||||
|
||||
@ -32,7 +32,7 @@ export function StatTile({ label, value, sub, tone = "default" }: {
|
||||
}[tone];
|
||||
return (
|
||||
<div className="rounded-xl border border-line-200 bg-surface px-4 py-3">
|
||||
<div className="text-[11.5px] font-semibold text-ink-500">{label}</div>
|
||||
<div className="text-[12px] font-semibold text-ink-500">{label}</div>
|
||||
<div className={`mt-0.5 text-[22px] font-bold leading-tight ${toneCls}`}>{value}</div>
|
||||
{sub && <div className="mt-0.5 text-[11px] text-ink-400">{sub}</div>}
|
||||
</div>
|
||||
@ -57,17 +57,17 @@ export function StatusBadge({ status }: { status: JobStatusName }) {
|
||||
}
|
||||
|
||||
export function Empty({ children }: { children: ReactNode }) {
|
||||
return <p className="py-8 text-center text-[12.5px] text-ink-400">{children}</p>;
|
||||
return <p className="py-8 text-center text-[13px] text-ink-400">{children}</p>;
|
||||
}
|
||||
|
||||
export function ErrorNote({ error }: { error: unknown }) {
|
||||
return (
|
||||
<p role="alert" className="rounded-lg bg-dead-50 px-3 py-2 text-[12.5px] font-medium text-dead-600">
|
||||
<p role="alert" className="rounded-lg bg-dead-50 px-3 py-2 text-[13px] font-medium text-dead-600">
|
||||
{error instanceof Error ? error.message : "요청에 실패했습니다"}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function Loading() {
|
||||
return <p className="py-8 text-center text-[12.5px] text-ink-400" aria-busy="true">불러오는 중…</p>;
|
||||
return <p className="py-8 text-center text-[13px] text-ink-400" aria-busy="true">불러오는 중…</p>;
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-page text-ink-900 font-sans text-[13.5px] antialiased;
|
||||
@apply bg-page text-ink-900 font-sans text-[14px] antialiased;
|
||||
}
|
||||
|
||||
/* 테이블 숫자 열 정렬 */
|
||||
@ -61,5 +61,5 @@ body {
|
||||
/* recharts 기본 폰트 */
|
||||
.recharts-wrapper,
|
||||
.recharts-tooltip-wrapper {
|
||||
font-size: 11.5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -65,11 +65,11 @@ export default function Crawl() {
|
||||
|
||||
{ip.data && (
|
||||
blockCount === 0 ? (
|
||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[12.5px] font-semibold text-ok-600">
|
||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[13px] font-semibold text-ok-600">
|
||||
✓ 기간 내 차단된 IP 세션 0건 — 요청 예산(선제 회전)이 잘 작동하고 있습니다
|
||||
</p>
|
||||
) : (
|
||||
<p role="alert" className="rounded-lg border border-dead-600/20 bg-dead-50 px-3 py-2 text-[12.5px] font-semibold text-dead-600">
|
||||
<p role="alert" className="rounded-lg border border-dead-600/20 bg-dead-50 px-3 py-2 text-[13px] font-semibold text-dead-600">
|
||||
⚠ 예산 안에서도 차단된 세션 {blockCount}건 — 최소 {ip.data.block_min_requests}회 요청에서 차단됐습니다.
|
||||
요청 예산을 그보다 낮게 유지하세요(현재 서버 설정은 toml [DecodoConfig].ip_request_budget)
|
||||
</p>
|
||||
@ -212,7 +212,7 @@ export default function Crawl() {
|
||||
<td className="tnum py-1.5 pr-2 text-right text-ink-500">{s.ok_count}/{s.blocked_count}</td>
|
||||
<td className="tnum py-1.5 pr-2 text-right text-ink-500">{durationSec(s.elapsed_sec)}</td>
|
||||
<td className="py-1.5">
|
||||
<span className="inline-flex items-center gap-1.5 text-[11.5px] font-semibold" style={{ color: r.color }}>
|
||||
<span className="inline-flex items-center gap-1.5 text-[12px] font-semibold" style={{ color: r.color }}>
|
||||
<span className="h-1.5 w-1.5 rounded-full" style={{ background: r.color }} aria-hidden />
|
||||
{r.label}
|
||||
</span>
|
||||
|
||||
@ -50,12 +50,12 @@ export default function Dashboard() {
|
||||
{ops.isError && <ErrorNote error={ops.error} />}
|
||||
{alerts.map((a) => (
|
||||
<p key={a.key} role="alert"
|
||||
className="rounded-lg border border-dead-600/20 bg-dead-50 px-3 py-2 text-[12.5px] font-semibold text-dead-600">
|
||||
className="rounded-lg border border-dead-600/20 bg-dead-50 px-3 py-2 text-[13px] font-semibold text-dead-600">
|
||||
⚠ {a.msg}
|
||||
</p>
|
||||
))}
|
||||
{d && alerts.length === 0 && (
|
||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[12.5px] font-semibold text-ok-600">
|
||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[13px] font-semibold text-ok-600">
|
||||
✓ 모든 지표가 임계 안에 있습니다
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -52,7 +52,7 @@ export default function Jobs() {
|
||||
</div>
|
||||
<form className="ml-auto flex gap-1.5" onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()); setPage(0); }}>
|
||||
<input value={qInput} onChange={(e) => setQInput(e.target.value)} placeholder="상품코드·상품명 검색"
|
||||
className="w-56 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[12.5px] outline-none focus:border-primary-600" />
|
||||
className="w-56 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[13px] outline-none focus:border-primary-600" />
|
||||
<button type="submit" className="rounded-lg bg-primary-600 px-3 py-1.5 text-[12px] font-semibold text-white hover:bg-primary-700">검색</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -64,7 +64,7 @@ export default function Jobs() {
|
||||
{list.data && list.data.items.length === 0 && <Empty>조건에 맞는 작업이 없습니다</Empty>}
|
||||
{list.data && list.data.items.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-[12.5px]">
|
||||
<table className="w-full text-left text-[13px]">
|
||||
<thead>
|
||||
<tr className="border-b border-line-200 text-[11px] text-ink-400">
|
||||
<th className="pb-1.5 pr-3 font-semibold">상품</th>
|
||||
@ -87,7 +87,7 @@ export default function Jobs() {
|
||||
<td className="py-2 pr-3">{j.outcome === "found" ? "찾음" : j.outcome === "not_found" ? "없음" : "—"}</td>
|
||||
<td className="tnum py-2 pr-3 text-right">{won(j.final_lowest)}</td>
|
||||
<td className="tnum py-2 pr-3 text-right text-ink-500">{usd(j.cost_usd)}</td>
|
||||
<td className="tnum py-2 text-[11.5px] text-ink-500">{dateShort(j.created_at)}</td>
|
||||
<td className="tnum py-2 text-[12px] text-ink-500">{dateShort(j.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -128,8 +128,8 @@ function JobDetail({ job, onClose }: { job: JobListItem | null; onClose: () => v
|
||||
const out = detail.data?.output as Record<string, any> | undefined;
|
||||
return (
|
||||
<Card title="상세" hint={<button onClick={onClose} className="font-semibold text-ink-400 hover:text-ink-700">닫기</button>}>
|
||||
<dl className="space-y-2 text-[12.5px]">
|
||||
<Row k="작업 ID"><span className="tnum break-all text-[11.5px]">{job.job_id}</span></Row>
|
||||
<dl className="space-y-2 text-[13px]">
|
||||
<Row k="작업 ID"><span className="tnum break-all text-[12px]">{job.job_id}</span></Row>
|
||||
<Row k="상품">{job.product_name || "—"}{job.product_code && <span className="text-ink-400"> ({job.product_code})</span>}</Row>
|
||||
<Row k="상태"><StatusBadge status={job.status} /> <span className="text-ink-400">시도 {job.attempts}/{job.max_attempts}</span></Row>
|
||||
{job.last_error && <Row k="오류"><span className="break-all text-dead-600">{job.last_error}</span></Row>}
|
||||
@ -146,7 +146,7 @@ function JobDetail({ job, onClose }: { job: JobListItem | null; onClose: () => v
|
||||
{job.status === "DEAD" && (
|
||||
<div className="mt-4 border-t border-line-100 pt-3">
|
||||
<button onClick={() => requeue.mutate()} disabled={requeue.isPending}
|
||||
className="w-full rounded-lg bg-primary-600 px-3 py-2 text-[12.5px] font-semibold text-white hover:bg-primary-700 disabled:opacity-50">
|
||||
className="w-full rounded-lg bg-primary-600 px-3 py-2 text-[13px] font-semibold text-white hover:bg-primary-700 disabled:opacity-50">
|
||||
{requeue.isPending ? "재시도 요청 중…" : "다시 검색하기 (재큐)"}
|
||||
</button>
|
||||
{requeue.isError && <p className="mt-2 text-[12px] font-medium text-dead-600">{(requeue.error as Error).message}</p>}
|
||||
@ -156,8 +156,8 @@ function JobDetail({ job, onClose }: { job: JobListItem | null; onClose: () => v
|
||||
|
||||
{out && (
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer text-[11.5px] font-semibold text-ink-400">원본 결과(JSON)</summary>
|
||||
<pre className="mt-1 max-h-64 overflow-auto rounded-lg bg-surface-2 p-2 text-[10.5px] leading-relaxed">{JSON.stringify(out, null, 2)}</pre>
|
||||
<summary className="cursor-pointer text-[12px] font-semibold text-ink-400">원본 결과(JSON)</summary>
|
||||
<pre className="mt-1 max-h-64 overflow-auto rounded-lg bg-surface-2 p-2 text-[11px] leading-relaxed">{JSON.stringify(out, null, 2)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</Card>
|
||||
@ -167,7 +167,7 @@ function JobDetail({ job, onClose }: { job: JobListItem | null; onClose: () => v
|
||||
function Row({ k, children }: { k: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<dt className="w-16 shrink-0 text-[11.5px] font-semibold text-ink-400">{k}</dt>
|
||||
<dt className="w-16 shrink-0 text-[12px] font-semibold text-ink-400">{k}</dt>
|
||||
<dd className="min-w-0 flex-1">{children}</dd>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -33,7 +33,7 @@ export default function Products() {
|
||||
|
||||
<form className="flex gap-1.5" onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()); }}>
|
||||
<input value={qInput} onChange={(e) => setQInput(e.target.value)} placeholder="상품코드 검색"
|
||||
className="w-64 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[12.5px] outline-none focus:border-primary-600" />
|
||||
className="w-64 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[13px] outline-none focus:border-primary-600" />
|
||||
<button type="submit" className="rounded-lg bg-primary-600 px-3 py-1.5 text-[12px] font-semibold text-white hover:bg-primary-700">검색</button>
|
||||
</form>
|
||||
|
||||
@ -49,8 +49,8 @@ export default function Products() {
|
||||
<button onClick={() => setSelected(p)}
|
||||
className={`w-full px-1 py-2 text-left hover:bg-surface-2 ${selected?.product_code === p.product_code ? "bg-primary-50/60" : ""}`}>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="truncate text-[12.5px] font-semibold">{p.display_name || p.product_code}</span>
|
||||
<span className="tnum shrink-0 text-[12.5px] font-bold">{won(p.final_lowest)}</span>
|
||||
<span className="truncate text-[13px] font-semibold">{p.display_name || p.product_code}</span>
|
||||
<span className="tnum shrink-0 text-[13px] font-bold">{won(p.final_lowest)}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 flex justify-between text-[11px] text-ink-400">
|
||||
<span>{p.product_code} · 검색 {p.searches}회</span>
|
||||
@ -96,7 +96,7 @@ function ProductDetail({ product }: { product: ProductItem | null }) {
|
||||
title={<>가격 추이 — {product.display_name || product.product_code}</>}
|
||||
hint={
|
||||
<button onClick={() => research.mutate()} disabled={research.isPending}
|
||||
className="rounded-md bg-primary-600 px-2.5 py-1 text-[11.5px] font-semibold text-white hover:bg-primary-700 disabled:opacity-50">
|
||||
className="rounded-md bg-primary-600 px-2.5 py-1 text-[12px] font-semibold text-white hover:bg-primary-700 disabled:opacity-50">
|
||||
{research.isPending ? "요청 중…" : "지금 다시 검색"}
|
||||
</button>
|
||||
}>
|
||||
@ -160,7 +160,7 @@ function ProductDetail({ product }: { product: ProductItem | null }) {
|
||||
</Card>
|
||||
|
||||
<Card title="검증 링크 (최근 검색)">
|
||||
<ul className="space-y-2 text-[12.5px]">
|
||||
<ul className="space-y-2 text-[13px]">
|
||||
{latest.naver_url && (
|
||||
<li>
|
||||
<a href={latest.naver_url} target="_blank" rel="noreferrer" className="font-semibold text-primary-600 hover:underline">
|
||||
|
||||
@ -26,7 +26,7 @@ export default function Settings() {
|
||||
<label htmlFor="base" className="mb-1 block text-[12px] font-semibold text-ink-700">API 주소</label>
|
||||
<input id="base" value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="비우면 현재 주소 기준 (개발: vite 프록시 → :9600)"
|
||||
className="w-full rounded-lg border border-line-200 bg-surface px-3 py-2 text-[12.5px] outline-none focus:border-primary-600" />
|
||||
className="w-full rounded-lg border border-line-200 bg-surface px-3 py-2 text-[13px] outline-none focus:border-primary-600" />
|
||||
<p className="mt-1 text-[11px] text-ink-400">예: https://lps.example.com — dev 서버를 직접 보려면 해당 주소 입력</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -34,10 +34,10 @@ export default function Settings() {
|
||||
<input id="key" type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="개발(개방 모드)은 비워둠 — prod 만 필요"
|
||||
autoComplete="off"
|
||||
className="w-full rounded-lg border border-line-200 bg-surface px-3 py-2 text-[12.5px] outline-none focus:border-primary-600" />
|
||||
className="w-full rounded-lg border border-line-200 bg-surface px-3 py-2 text-[13px] outline-none focus:border-primary-600" />
|
||||
<p className="mt-1 text-[11px] text-ink-400">서버 toml 의 [WebServerConfig].api_keys 중 하나. 이 브라우저에만 저장됩니다.</p>
|
||||
</div>
|
||||
<button type="submit" className="rounded-lg bg-primary-600 px-4 py-2 text-[12.5px] font-semibold text-white hover:bg-primary-700">
|
||||
<button type="submit" className="rounded-lg bg-primary-600 px-4 py-2 text-[13px] font-semibold text-white hover:bg-primary-700">
|
||||
저장
|
||||
</button>
|
||||
{saved && <span className="ml-2 text-[12px] font-medium text-ok-600">저장했습니다</span>}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user