ApiHealthBadge
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
API 상태 대기 중
실시간 /health 응답 상태
ApiHealthBadge
Real-time status indicator for API/DB connectivity.
Live Monitoring
Dynamic status badge based on server response.
Source Code
API 상태 대기 중
Dynamically reflects the /health status.
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
ApiHealthBadgetypescript
"use client";
import { useEffect, useState } from "react";
import { clientGET } from "@/lib/api/client";
type BadgeState = "idle" | "loading" | "ok" | "error";
type HealthResult = { status: "ok" | "error"; message?: string };
async function checkHealth(): Promise<HealthResult> {
try {
const [health, nav, footer] = await Promise.all([
clientGET<unknown>("/health"),
clientGET<unknown>("/navigation"),
clientGET<unknown>("/footer"),
]);
if (!health.ok) return { status: "error", message: health.error.message };
if (!nav.ok) return { status: "error", message: nav.error.message };
if (!footer.ok) return { status: "error", message: footer.error.message };
return { status: "ok" };
} catch (e) {
return { status: "error", message: e instanceof Error ? e.message : "Health check failed" };
}
}
/**
* Iroul API/DB 상태 뱃지. 마운트 시 /health, /navigation, /footer 순으로 BFF 경유 확인.
*/
export function ApiHealthBadge() {
const [state, setState] = useState<BadgeState>("idle");
const [result, setResult] = useState<HealthResult | null>(null);
useEffect(() => {
let mounted = true;
setState("loading");
checkHealth().then((res) => {
if (!mounted) return;
setResult(res);
setState(res.status === "ok" ? "ok" : "error");
}).catch(() => {
if (!mounted) return;
setState("error");
});
return () => { mounted = false; };
}, []);
const label =
state === "loading"
? "API 상태 확인 중..."
: state === "ok"
? "API/DB 상태: 정상"
: state === "error"
? "API/DB 상태: 오류"
: "API 상태 대기 중";
const colorClass =
state === "ok"
? "bg-emerald-500/10 text-emerald-600 border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20"
: state === "error"
? "bg-rose-500/10 text-rose-600 border-rose-500/20 shadow-[0_0_10px_rgba(225,29,72,0.15)] dark:bg-rose-500/10 dark:text-rose-400 dark:border-rose-500/20"
: "bg-slate-500/10 text-slate-600 border-slate-500/20 shadow-sm dark:bg-slate-500/10 dark:text-slate-400 dark:border-slate-500/20";
return (
<span
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-bold tracking-wide backdrop-blur-md transition-all duration-300 hover:scale-105 ${colorClass}`}
title={typeof result?.message === 'string' ? result.message : undefined}
>
<span className="relative flex h-2 w-2">
{state === "ok" && <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75 duration-1000"></span>}
{state === "error" && <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-rose-400 opacity-75 duration-1000"></span>}
{state === "loading" && <span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-slate-400 opacity-75"></span>}
<span
className={`relative inline-flex h-2 w-2 rounded-full ${
state === "ok" ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.8)]" : state === "error" ? "bg-rose-500 shadow-[0_0_8px_rgba(225,29,72,0.8)]" : "bg-slate-400"
}`}
/>
</span>
<span>{label}</span>
</span>
);
}