2026-02-13 12:17:35 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { ReactNode } from "react";
|
|
|
|
|
import { LucideIcon } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
interface SettingsCardProps {
|
|
|
|
|
/** 카드 상단에 표시될 아이콘 컴포넌트 */
|
|
|
|
|
icon: LucideIcon;
|
|
|
|
|
/** 카드 제목 */
|
|
|
|
|
title: ReactNode;
|
|
|
|
|
/** 제목 옆에 표시될 배지 (선택 사항) */
|
|
|
|
|
badge?: ReactNode;
|
|
|
|
|
/** 헤더 우측에 표시될 액션 요소 (스위치, 버튼 등) */
|
|
|
|
|
headerAction?: ReactNode;
|
|
|
|
|
/** 카드 설명 텍스트 */
|
|
|
|
|
description?: string;
|
|
|
|
|
/** 카드 본문 컨텐츠 */
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
/** 카드 하단 영역 (액션 버튼 및 상태 메시지 포함) */
|
|
|
|
|
footer?: {
|
|
|
|
|
/** 좌측 액션 버튼들 */
|
|
|
|
|
actions?: ReactNode;
|
|
|
|
|
/** 우측 상태 메시지 */
|
|
|
|
|
status?: ReactNode;
|
|
|
|
|
};
|
|
|
|
|
/** 추가 클래스 */
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 설정 페이지에서 사용되는 통일된 카드 UI 컴포넌트입니다.
|
|
|
|
|
* @remarks 모든 설정 폼(인증, 프로필 등)은 이 컴포넌트를 사용하여 일관된 디자인을 유지해야 합니다.
|
|
|
|
|
*/
|
|
|
|
|
export function SettingsCard({
|
|
|
|
|
icon: Icon,
|
|
|
|
|
title,
|
|
|
|
|
badge,
|
|
|
|
|
headerAction,
|
|
|
|
|
description,
|
|
|
|
|
children,
|
|
|
|
|
footer,
|
|
|
|
|
className,
|
|
|
|
|
}: SettingsCardProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"group relative flex h-full flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm transition-all hover:-translate-y-0.5 hover:border-brand-200 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:border-brand-800 dark:hover:shadow-brand-900/10",
|
|
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-02-13 15:44:41 +09:00
|
|
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-brand-300/70 to-transparent dark:via-brand-600/60" />
|
2026-02-13 12:17:35 +09:00
|
|
|
|
|
|
|
|
<div className="flex flex-1 flex-col p-5 sm:p-6">
|
|
|
|
|
{/* ========== CARD HEADER ========== */}
|
|
|
|
|
<div className="mb-5 flex flex-col gap-4">
|
|
|
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
|
|
|
<div className="flex min-w-0 gap-3">
|
|
|
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50 text-brand-600 ring-1 ring-brand-100 dark:bg-brand-900/20 dark:text-brand-400 dark:ring-brand-800/50">
|
|
|
|
|
<Icon className="h-5 w-5" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
<h2 className="truncate text-base font-bold tracking-tight text-zinc-900 dark:text-zinc-100">
|
|
|
|
|
{title}
|
|
|
|
|
</h2>
|
|
|
|
|
{badge && <div className="shrink-0">{badge}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
{description && (
|
|
|
|
|
<p className="mt-1 text-[13px] font-medium leading-normal text-zinc-500 dark:text-zinc-400">
|
|
|
|
|
{description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{headerAction && (
|
|
|
|
|
<div className="sm:shrink-0 sm:pl-2">{headerAction}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ========== CARD BODY ========== */}
|
|
|
|
|
<div className="flex-1">{children}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ========== CARD FOOTER ========== */}
|
|
|
|
|
{footer && (
|
|
|
|
|
<div className="border-t border-zinc-100 bg-zinc-50/50 px-5 py-3 dark:border-zinc-800/50 dark:bg-zinc-900/30 sm:px-6">
|
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<div className="flex flex-wrap gap-2">{footer.actions}</div>
|
|
|
|
|
<div className="text-left sm:text-right">{footer.status}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|