style(design): FINDING-012 — 접근성 통일: 검색 라벨·행 키보드 선택·이모지 숨김 [subagent]
Settings 는 label 연결이 모범인데 Jobs/Products 검색 인풋만 접근
가능한 이름이 없었고, Products 목록(button)과 달리 Jobs 행(tr onClick)
은 키보드로 선택 불가였다. aria-label 2곳, tr tabIndex+Enter/Space
핸들러+focus-visible, 장식 이모지(⚠✓🎉) aria-hidden 처리.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
645a0362b0
commit
1006ad11ea
@ -66,11 +66,11 @@ export default function Crawl() {
|
|||||||
{ip.data && (
|
{ip.data && (
|
||||||
blockCount === 0 ? (
|
blockCount === 0 ? (
|
||||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[13px] 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건 — 요청 예산(선제 회전)이 잘 작동하고 있습니다
|
<span aria-hidden>✓ </span>기간 내 차단된 IP 세션 0건 — 요청 예산(선제 회전)이 잘 작동하고 있습니다
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<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}회 요청에서 차단됐습니다.
|
<span aria-hidden>⚠ </span>예산 안에서도 차단된 세션 {blockCount}건 — 최소 {ip.data.block_min_requests}회 요청에서 차단됐습니다.
|
||||||
요청 예산을 그보다 낮게 유지하세요(현재 서버 설정은 toml [DecodoConfig].ip_request_budget)
|
요청 예산을 그보다 낮게 유지하세요(현재 서버 설정은 toml [DecodoConfig].ip_request_budget)
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
@ -139,7 +139,7 @@ export default function Crawl() {
|
|||||||
<Card title="차단 발생 추이" hint="시간대별">
|
<Card title="차단 발생 추이" hint="시간대별">
|
||||||
{bot.isPending && <Loading />}
|
{bot.isPending && <Loading />}
|
||||||
{bot.isError && <ErrorNote error={bot.error} />}
|
{bot.isError && <ErrorNote error={bot.error} />}
|
||||||
{bot.data && bot.data.hourly.length === 0 && <Empty>기간 내 차단 없음 🎉</Empty>}
|
{bot.data && bot.data.hourly.length === 0 && <Empty>기간 내 차단 없음 <span aria-hidden>🎉</span></Empty>}
|
||||||
{bot.data && bot.data.hourly.length > 0 && (
|
{bot.data && bot.data.hourly.length > 0 && (
|
||||||
<ResponsiveContainer width="100%" height={150}>
|
<ResponsiveContainer width="100%" height={150}>
|
||||||
<BarChart data={bot.data.hourly.map((h) => ({ ...h, x: dateShort(h.bucket) }))} margin={{ top: 6, right: 12, bottom: 0, left: -22 }}>
|
<BarChart data={bot.data.hourly.map((h) => ({ ...h, x: dateShort(h.bucket) }))} margin={{ top: 6, right: 12, bottom: 0, left: -22 }}>
|
||||||
|
|||||||
@ -51,12 +51,12 @@ export default function Dashboard() {
|
|||||||
{alerts.map((a) => (
|
{alerts.map((a) => (
|
||||||
<p key={a.key} role="alert"
|
<p key={a.key} role="alert"
|
||||||
className="rounded-lg border border-dead-600/20 bg-dead-50 px-3 py-2 text-[13px] 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}
|
<span aria-hidden>⚠ </span>{a.msg}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{d && alerts.length === 0 && (
|
{d && alerts.length === 0 && (
|
||||||
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[13px] font-semibold text-ok-600">
|
<p className="rounded-lg bg-ok-50 px-3 py-2 text-[13px] font-semibold text-ok-600">
|
||||||
✓ 모든 지표가 임계 안에 있습니다
|
<span aria-hidden>✓ </span>모든 지표가 임계 안에 있습니다
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export default function Jobs() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<form className="ml-auto flex gap-1.5" onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()); setPage(0); }}>
|
<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="상품코드·상품명 검색"
|
<input value={qInput} onChange={(e) => setQInput(e.target.value)} placeholder="상품코드·상품명 검색" aria-label="상품코드·상품명 검색"
|
||||||
className="w-56 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[13px] 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>
|
<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>
|
</form>
|
||||||
@ -77,8 +77,10 @@ export default function Jobs() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{list.data.items.map((j) => (
|
{list.data.items.map((j) => (
|
||||||
<tr key={j.job_id} onClick={() => setSelected(j)}
|
<tr key={j.job_id} onClick={() => setSelected(j)} tabIndex={0}
|
||||||
className={`cursor-pointer border-b border-line-100 hover:bg-surface-2 ${selected?.job_id === j.job_id ? "bg-primary-50/60" : ""}`}>
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setSelected(j); } }}
|
||||||
|
aria-selected={selected?.job_id === j.job_id}
|
||||||
|
className={`cursor-pointer border-b border-line-100 hover:bg-surface-2 focus-visible:bg-primary-50/60 ${selected?.job_id === j.job_id ? "bg-primary-50/60" : ""}`}>
|
||||||
<td className="py-2 pr-3">
|
<td className="py-2 pr-3">
|
||||||
<div className="font-semibold">{j.product_name || "—"}</div>
|
<div className="font-semibold">{j.product_name || "—"}</div>
|
||||||
<div className="text-[11px] text-ink-400">{j.product_code}</div>
|
<div className="text-[11px] text-ink-400">{j.product_code}</div>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function Products() {
|
|||||||
<h1 className="text-[17px] font-bold tracking-tight">상품·가격</h1>
|
<h1 className="text-[17px] font-bold tracking-tight">상품·가격</h1>
|
||||||
|
|
||||||
<form className="flex gap-1.5" onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()); }}>
|
<form className="flex gap-1.5" onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()); }}>
|
||||||
<input value={qInput} onChange={(e) => setQInput(e.target.value)} placeholder="상품코드 검색"
|
<input value={qInput} onChange={(e) => setQInput(e.target.value)} placeholder="상품코드 검색" aria-label="상품코드 검색"
|
||||||
className="w-64 rounded-lg border border-line-200 bg-surface px-3 py-1.5 text-[13px] 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>
|
<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>
|
</form>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user