diff --git a/lps-admin/src/components/Layout.tsx b/lps-admin/src/components/Layout.tsx index 83b13b2..a81e469 100644 --- a/lps-admin/src/components/Layout.tsx +++ b/lps-admin/src/components/Layout.tsx @@ -4,15 +4,14 @@ import { useQuery } from "@tanstack/react-query"; import { NavLink, Outlet } from "react-router"; import { healthz } from "../api/client"; -// action: 실제 비용이 드는 실행 페이지(모니터링과 성격이 다름) — 앰버(warn) 마커로 구분. -// group: 이 항목 앞에 구분선(모니터링 ↔ 실행/설정 경계). +// action: 실제 비용이 드는 실행 페이지(모니터링과 성격이 다름) — 다른 메뉴와 배경색(앰버)만 다르게. const MENU = [ { to: "/", label: "대시보드", end: true }, { to: "/jobs", label: "작업 큐" }, { to: "/products", label: "상품·가격" }, { to: "/crawl", label: "크롤 상태" }, { to: "/costs", label: "비용" }, - { to: "/test", label: "테스트 검색", action: true, group: true }, + { to: "/test", label: "테스트 검색", action: true }, { to: "/settings", label: "설정" }, ]; @@ -20,55 +19,49 @@ export default function Layout() { const health = useQuery({ queryKey: ["healthz"], queryFn: healthz, refetchInterval: 10_000 }); const up = health.data === true; return ( -
불러오는 중…
; } + +// ── 페이지 헤더 — h1 + 우측 컨트롤 슬롯(기간 탭 등). 모든 페이지 제목을 이걸로 통일. ── +export function PageHeader({ title, children }: { title: ReactNode; children?: ReactNode }) { + return ( ++ {items.map((it, i) => ( + + + {it.label} + + ))} + {note && {note}} +
+ ); +} + +// ── 스크롤 컨테이너 — height 숫자면 고정 높이, "fill" 이면 부모의 남는 높이를 채움. ── +export function ScrollBox({ height = 420, className = "", children }: { + height?: number | "fill"; className?: string; children: ReactNode; +}) { + const fill = height === "fill"; + return ( +| + {c.label} + | + ))} +
|---|
- AI (판정·검색어) - 프록시 대역폭 (DECODO) -
+X축 = 한 IP로 보낸 요청 수. 분포가 예산값에 몰려 있으면 정상, 붉은 기준선(차단 시작점)보다 예산이 낮아야 안전.
@@ -136,50 +162,45 @@ export default function Crawl() {| 시각 | -검색어 | -요청# | -포트 | -감지 근거 | -
|---|---|---|---|---|
| {hhmm(b.created_at)} | -{b.query || "—"} | -{b.ip_request_no ?? "—"} | -{b.proxy_port ?? "—"} | -{b.marker} | -
| 종료 시각 | -소스 | -포트 | -요청 | -성공/차단 | -지속 | -종료 사유 | +|||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {dateShort(s.created_at)} | +{s.source} | +{s.proxy_port ?? "—"} | +{s.requests} | +{s.ok_count}/{s.blocked_count} | +{durationSec(s.elapsed_sec)} | ++ + + {r.label} + + | |||||||
| {dateShort(s.created_at)} | -{s.source} | -{s.proxy_port ?? "—"} | -{s.requests} | -{s.ok_count}/{s.blocked_count} | -{durationSec(s.elapsed_sec)} | -- - - {r.label} - - | -
수집 중… 잠시 후 그래프가 나타납니다
- ) : ( -- 대기 - 처리중 -
+수집 중… 잠시 후 그래프가 나타납니다
++
임계 배너는 서버 알림 기본값 기준입니다(서버 toml 에서 조정했으면 실제 알림과 다를 수 있음). 전체 룰은 lps/docs/operations.md 참고.
diff --git a/lps-admin/src/pages/Jobs.tsx b/lps-admin/src/pages/Jobs.tsx index b4f2aa4..a52d63e 100644 --- a/lps-admin/src/pages/Jobs.tsx +++ b/lps-admin/src/pages/Jobs.tsx @@ -1,12 +1,24 @@ -/** 작업 큐 — 잡 목록(필터·검색·페이지네이션) + 상세 패널 + DEAD 재큐. */ +/** 작업 큐 — 잡 목록(필터·검색·페이지네이션) 표 + 행별 상세/JSON 모달 + DEAD 재큐. */ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { get, post } from "../api/client"; import type { JobDetailRes, JobListItem, JobListRes, JobStatusName, RequeueRes } from "../api/types"; -import { Card, Empty, ErrorNote, Loading, StatusBadge } from "../components/ui"; +import { Button, Card, Empty, ErrorNote, Loading, Modal, PageHeader, ScrollTable, SearchForm, Segmented, StatusBadge } from "../components/ui"; import { dateShort, usd, won } from "../lib/format"; +type DetailView = "detail" | "json"; + +const COLS = [ + { key: "product", label: "상품" }, + { key: "status", label: "상태" }, + { key: "outcome", label: "결과" }, + { key: "price", label: "최저가", align: "right" }, + { key: "cost", label: "원가", align: "right" }, + { key: "time", label: "요청 시각" }, + { key: "actions", label: "", align: "right" }, +] as const; + const PAGE = 20; const TABS: { v: JobStatusName | ""; label: string }[] = [ { v: "", label: "전체" }, @@ -21,7 +33,10 @@ export default function Jobs() { const [q, setQ] = useState(""); const [qInput, setQInput] = useState(""); const [page, setPage] = useState(0); - const [selected, setSelected] = useState| 상품 | -상태 | -결과 | -최저가 | -원가 | -요청 시각 | -
|---|---|---|---|---|---|
|
- {j.product_name || "—"}
- {j.product_code}
- |
- {j.outcome === "found" ? "찾음" : j.outcome === "not_found" ? "없음" : "—"} | -{won(j.final_lowest)} | -{usd(j.cost_usd)} | -{dateShort(j.created_at)} | -
{(requeue.error as Error).message}
} - {requeue.isSuccess &&대기열에 다시 넣었습니다 — 워커가 곧 처리합니다
} -{(requeue.error as Error).message}
} + {requeue.isSuccess &&대기열에 다시 넣었습니다 — 워커가 곧 처리합니다
} +{jsonText}
+ ) : (
+ {JSON.stringify(out, null, 2)}
- +
{research.data.accepted > 0 ? "검색을 접수했습니다 — 잠시 후 새 점이 추가됩니다" : "이미 같은 상품 검색이 진행 중입니다"}
)} - {research.isError &&- {SERIES.map((s) => ( - - - {s.label} - - ))} - 빈 구간 = 그 시점에 해당 소스에서 못 찾음 -
- > + )} ++ {dateShort(point.triggered_at)} 기준 · 그래프의 시점을 클릭해 이동 +
{shown.length === 0 ? ( -몰 {malls.length}개 중 상위 {shown.length}개 표시 — '전체'로 모두 보기
+몰 {malls.length}개 중 상위 {shown.length}개 — '전체'로 모두 보기
)} > )} diff --git a/lps-admin/src/pages/Settings.tsx b/lps-admin/src/pages/Settings.tsx index 549ca16..f2a5f7c 100644 --- a/lps-admin/src/pages/Settings.tsx +++ b/lps-admin/src/pages/Settings.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { settings } from "../api/client"; -import { Card } from "../components/ui"; +import { Button, Card, PageHeader } from "../components/ui"; export default function Settings() { const [baseUrl, setBaseUrl] = useState(settings.baseUrl()); @@ -11,7 +11,7 @@ export default function Settings() { return (서버 toml 의 [WebServerConfig].api_keys 중 하나. 이 브라우저에만 저장됩니다.
- 임의의 상품을 실제 검색 파이프라인에 흘려보내 셀렉터·프록시·AI 매칭이 정상인지 점검합니다.
- 워커가 실제로 크롤하므로 프록시 대역폭 비용이 실제로 발생하고, 결과는 비용 통계에 집계됩니다
- (상품코드는 TEST- 접두로 구분).
-
+ 실제 크롤이라 프록시 비용 발생 · 비용 통계 집계 (상품코드 TEST-)
+