feat(lps-admin): 테스트 검색을 앰버로 구분 — '실제 비용 드는 액션' 신호

테스트 검색은 다른 페이지(읽기전용 모니터링)와 달리 실제 크롤·프록시
비용이 발생하는 실행 액션이라 시각적으로 구분(협의).

- 네비 '테스트 검색': 앰버 점 마커 + 앰버 텍스트/활성색, 위에 구분선으로
  모니터링 페이지들과 분리(설정과 함께 실행/유틸 그룹).
- 테스트 페이지 '검색 실행' 버튼: primary(indigo) → warn-700(#a36a00).
  흰 텍스트 대비 4.55:1 로 AA 통과(실측). 상태색은 장식이 아니라
  '비용 주의' 의미로만 사용(디자인 규칙 준수).
- whitespace-nowrap 로 라벨 줄바꿈 방지. 모바일 hscroll 0 유지.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-14 11:44:59 +09:00
parent 479cb862b1
commit e6005b4f94
2 changed files with 14 additions and 6 deletions

View File

@ -4,13 +4,15 @@ import { useQuery } from "@tanstack/react-query";
import { NavLink, Outlet } from "react-router"; import { NavLink, Outlet } from "react-router";
import { healthz } from "../api/client"; import { healthz } from "../api/client";
// action: 실제 비용이 드는 실행 페이지(모니터링과 성격이 다름) — 앰버(warn) 마커로 구분.
// group: 이 항목 앞에 구분선(모니터링 ↔ 실행/설정 경계).
const MENU = [ const MENU = [
{ to: "/", label: "대시보드", end: true }, { to: "/", label: "대시보드", end: true },
{ to: "/jobs", label: "작업 큐" }, { to: "/jobs", label: "작업 큐" },
{ to: "/products", label: "상품·가격" }, { to: "/products", label: "상품·가격" },
{ to: "/crawl", label: "크롤 상태" }, { to: "/crawl", label: "크롤 상태" },
{ to: "/costs", label: "비용" }, { to: "/costs", label: "비용" },
{ to: "/test", label: "테스트 검색" }, { to: "/test", label: "테스트 검색", action: true, group: true },
{ to: "/settings", label: "설정" }, { to: "/settings", label: "설정" },
]; ];
@ -35,18 +37,23 @@ export default function Layout() {
</div> </div>
</div> </div>
<nav aria-label="주 메뉴" className="mt-2 md:mt-0"> <nav aria-label="주 메뉴" className="mt-2 md:mt-0">
<ul className="flex gap-1 overflow-x-auto md:flex-col md:gap-0 md:space-y-0.5"> <ul className="flex items-center gap-1 overflow-x-auto md:flex-col md:items-stretch md:gap-0 md:space-y-0.5">
{MENU.map((m) => ( {MENU.map((m) => (
<li key={m.to} className="shrink-0"> <li key={m.to} className="flex shrink-0 items-center">
{/* 모니터링 ↔ 실행/설정 경계: 모바일=세로 구분선, 데스크톱=가로 구분선 */}
{m.group && <span className="mx-1 h-5 w-px bg-line-200 md:my-1 md:h-px md:w-full" aria-hidden />}
<NavLink <NavLink
to={m.to} to={m.to}
end={m.end} end={m.end}
className={({ isActive }) => className={({ isActive }) =>
`flex min-h-[44px] items-center rounded-lg px-3 text-[13px] font-semibold transition-colors md:min-h-0 md:py-2 ${ `flex min-h-[44px] w-full items-center gap-1.5 whitespace-nowrap rounded-lg px-3 text-[13px] font-semibold transition-colors md:min-h-0 md:py-2 ${
isActive ? "bg-primary-50 text-primary-700" : "text-ink-700 hover:bg-surface-2" m.action
? isActive ? "bg-warn-50 text-warn-700" : "text-warn-700 hover:bg-warn-50"
: isActive ? "bg-primary-50 text-primary-700" : "text-ink-700 hover:bg-surface-2"
}` }`
} }
> >
{m.action && <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-warn-600" aria-hidden />}
{m.label} {m.label}
</NavLink> </NavLink>
</li> </li>

View File

@ -127,8 +127,9 @@ export default function TestSearch() {
<Field id="price" label="현재가(원)" value={form.price} inputMode="numeric" <Field id="price" label="현재가(원)" value={form.price} inputMode="numeric"
onChange={(v) => setForm((f) => ({ ...f, price: v.replace(/[^0-9]/g, "") }))} placeholder="선택 — 있으면 가격밴드 필터 기준" /> onChange={(v) => setForm((f) => ({ ...f, price: v.replace(/[^0-9]/g, "") }))} placeholder="선택 — 있으면 가격밴드 필터 기준" />
<div className="flex gap-2 pt-1"> <div className="flex gap-2 pt-1">
{/* 앰버(warn) — 실제 크롤·프록시 비용이 드는 실행 액션임을 색으로 신호(모니터링 CTA 와 구분). */}
<button type="submit" disabled={!form.product_name.trim() || submit.isPending || (!!jobId && status !== "DONE" && status !== "DEAD")} <button type="submit" disabled={!form.product_name.trim() || submit.isPending || (!!jobId && status !== "DONE" && status !== "DEAD")}
className="flex-1 rounded-lg bg-primary-600 px-3 py-2 text-[13px] font-semibold text-white hover:bg-primary-700 disabled:opacity-50"> className="flex-1 rounded-lg bg-warn-700 px-3 py-2 text-[13px] font-semibold text-white hover:bg-warn-600 disabled:opacity-50">
{submit.isPending ? "제출 중…" : "검색 실행"} {submit.isPending ? "제출 중…" : "검색 실행"}
</button> </button>
{jobId && ( {jobId && (