Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 80 additions & 67 deletions apps/web/app/(pages)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,93 @@ import { api } from "@/lib/api";
import { Badge } from "@/components/shared/badge";
import { Card, CardHeader } from "@/components/shared/card";
import { JobControls } from "@/components/shared/job-controls";
import { PageErrorState } from "@/components/shared/page-error-state";

export default async function AdminPage() {
const data = await api.admin();
try {
const data = await api.admin();

return (
<div className="space-y-6">
<Card>
<CardHeader
eyebrow="Admin / Settings"
title="운영 설정과 승격 게이트"
description="소스 설정, 모델 라우팅, 비용 가드레일, 스케줄러, 수동 승격 승인 상태를 관리합니다."
/>
<div className="grid gap-6 xl:grid-cols-2">
return (
<div className="space-y-6">
<Card>
<CardHeader
eyebrow="Admin / Settings"
title="운영 설정과 승격 게이트"
description="소스 설정, 모델 라우팅, 비용 가드레일, 스케줄러, 수동 승인 상태를 관리합니다."
/>
<div className="grid gap-6 xl:grid-cols-2">
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">활성 소스</p>
<ul className="mt-3 space-y-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{data.sources.map((source) => (
<li key={source.name}>
{source.name} : {source.kind} : 신뢰도 {source.reliabilityScore.toFixed(2)}
</li>
))}
</ul>
</Card>
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">역할별 모델 라우팅</p>
<ul className="mt-3 space-y-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{data.modelRouting.map((route) => (
<li key={route.role}>
{route.role} : {route.model}
</li>
))}
</ul>
</Card>
</div>
</Card>

<Card>
<CardHeader
eyebrow="Operations"
title="운영 작업 실행"
description="필요 시 수집, 분석, 전체 갱신 파이프라인을 즉시 실행할 수 있습니다."
/>
<JobControls compact />
</Card>

<div className="grid gap-6 xl:grid-cols-3">
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">활성 소스</p>
<ul className="mt-3 space-y-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{data.sources.map((source) => (
<li key={source.name}>
{source.name} · {source.kind} · 신뢰도 {source.reliabilityScore.toFixed(2)}
</li>
))}
</ul>
<p className="text-[14px] font-semibold">비용 가드레일</p>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
일일 최대 ${data.costGuardrails.dailyBudgetUsd.toFixed(2)} / 작업당 최대 $
{data.costGuardrails.perJobBudgetUsd.toFixed(2)}
</p>
</Card>
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">역할별 모델 라우팅</p>
<ul className="mt-3 space-y-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{data.modelRouting.map((route) => (
<li key={route.role}>
{route.role} → {route.model}
</li>
))}
</ul>
<p className="text-[14px] font-semibold">스케줄러</p>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
프리마켓 {data.scheduler.premarketCron}
<br />
포스트마켓 {data.scheduler.postmarketCron}
</p>
</Card>
<Card className="rounded-[18px] bg-[color:var(--surface-white)] p-5 text-white">
<div className="flex items-center justify-between gap-4">
<p className="text-[14px] font-semibold">승격 승인</p>
<Badge variant={data.promotion.manualApprovalRequired ? "warn" : "ok"}>
{data.promotion.manualApprovalRequired ? "수동 승인 필요" : "자동 승인"}
</Badge>
</div>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
모든 승격은 평가 아티팩트 저장, 롤링 OOS 검증, 사용자 수동 승인까지 완료되어야 운영 설정에 반영됩니다.
</p>
</Card>
</div>
</Card>

<Card>
<CardHeader
eyebrow="Operations"
title="운영 작업 실행"
description="워커가 살아 있으면 여기서 수집, 분석, 전체 갱신 파이프라인을 즉시 실행할 수 있습니다."
/>
<JobControls compact />
</Card>

<div className="grid gap-6 xl:grid-cols-3">
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">비용 가드레일</p>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
일일 최대 ${data.costGuardrails.dailyBudgetUsd.toFixed(2)} / 작업당 최대 $
{data.costGuardrails.perJobBudgetUsd.toFixed(2)}
</p>
</Card>
<Card className="rounded-[18px] bg-[color:var(--bg-muted)] p-5">
<p className="text-[14px] font-semibold">스케줄러</p>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
프리마켓 {data.scheduler.premarketCron}
<br />
포스트마켓 {data.scheduler.postmarketCron}
</p>
</Card>
<Card className="rounded-[18px] bg-[color:var(--surface-white)] p-5 text-white">
<div className="flex items-center justify-between gap-4">
<p className="text-[14px] font-semibold">승격 승인</p>
<Badge variant={data.promotion.manualApprovalRequired ? "warn" : "ok"}>
{data.promotion.manualApprovalRequired ? "수동 승인 필요" : "자동 아님"}
</Badge>
</div>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
후보 승격은 평가 아티팩트 저장, 롤링 OOS 개선 확인, 사용자 수동 승인 전까지
실운영 라우팅에 반영되지 않습니다.
</p>
</Card>
</div>
</div>
);
);
} catch (error) {
const message = error instanceof Error ? error.message : "알 수 없는 오류";

return (
<PageErrorState
eyebrow="Admin / Settings"
title="관리자 화면을 불러오지 못했습니다"
description="운영 설정 응답이 지연되고 있습니다. 잠시 후 다시 시도해 주세요."
message={message}
/>
);
}
}
Loading