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
import { ApiHealthBadge } from '@/components/Health/ApiHealthBadge';

<ApiHealthBadge />
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>
  );
}