대시보드 실시간 기능 추가

This commit is contained in:
2026-02-13 12:17:35 +09:00
parent 12feeb2775
commit 1ac907cd27
35 changed files with 2790 additions and 1032 deletions

View File

@@ -2,12 +2,21 @@
import { useState, useTransition } from "react";
import { useShallow } from "zustand/react/shallow";
import { CreditCard, CheckCircle2, SearchCheck, ShieldOff, XCircle } from "lucide-react";
import {
CreditCard,
CheckCircle2,
SearchCheck,
ShieldOff,
XCircle,
FileLock2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { InlineSpinner } from "@/components/ui/loading-spinner";
import { validateKisProfile } from "@/features/settings/apis/kis-auth.api";
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
import { SettingsCard } from "./SettingsCard";
/**
* @description 한국투자증권 계좌번호 검증 폼입니다.
@@ -98,98 +107,122 @@ export function KisProfileForm() {
}
return (
<div className="rounded-2xl border border-brand-200 bg-background p-4 shadow-sm dark:border-brand-800/45 dark:bg-brand-900/14">
{/* ========== HEADER ========== */}
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-sm font-semibold tracking-tight text-foreground">
</h2>
<p className="mt-1 text-xs text-muted-foreground">
.
<SettingsCard
icon={CreditCard}
title="한국투자증권 계좌 인증"
description="앱키 연결 후 계좌번호를 검증하면 잔고/대시보드 기능을 사용할 수 있습니다."
badge={
isKisProfileVerified ? (
<span className="inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full bg-green-50 px-2 py-0.5 text-[11px] font-medium text-green-700 ring-1 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/30">
<CheckCircle2 className="h-3 w-3" />
</span>
) : undefined
}
className={
!isKisVerified ? "opacity-60 grayscale pointer-events-none" : undefined
}
footer={{
actions: (
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={handleValidateProfile}
disabled={
!isKisVerified || isValidating || !kisAccountNoInput.trim()
}
className="h-9 rounded-lg bg-brand-600 px-4 text-xs font-semibold text-white shadow-sm transition-all hover:bg-brand-700 hover:shadow disabled:opacity-50 disabled:shadow-none dark:bg-brand-600 dark:hover:bg-brand-500"
>
{isValidating ? (
<span className="flex items-center gap-1.5">
<InlineSpinner className="h-3 w-3 text-white" />
</span>
) : (
<span className="flex items-center gap-1.5">
<SearchCheck className="h-3.5 w-3.5" />
</span>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={handleDisconnectAccount}
disabled={!isKisProfileVerified && !kisAccountNoInput.trim()}
className="h-9 rounded-lg border-zinc-200 bg-white px-4 text-xs text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
>
<ShieldOff className="h-3.5 w-3.5" />
</Button>
</div>
),
status: (
<div className="flex min-h-5 items-center justify-start sm:justify-end">
{errorMessage && (
<p className="animate-in fade-in slide-in-from-right-4 flex items-center gap-1.5 text-xs font-semibold text-red-500">
<XCircle className="h-3.5 w-3.5" />
{errorMessage}
</p>
)}
{statusMessage && (
<p className="animate-in fade-in slide-in-from-right-4 flex items-center gap-1.5 text-xs font-semibold text-brand-600 dark:text-brand-400">
<CheckCircle2 className="h-3.5 w-3.5" />
{statusMessage}
</p>
)}
{!statusMessage && !errorMessage && !isKisVerified && (
<p className="flex items-center gap-1.5 text-xs text-zinc-400 dark:text-zinc-600">
<span className="h-1.5 w-1.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
</p>
)}
{!statusMessage && !errorMessage && isKisProfileVerified && (
<p className="animate-in fade-in slide-in-from-right-4 flex items-center gap-1.5 text-xs font-medium text-zinc-500 dark:text-zinc-400">
: {maskAccountNo(verifiedAccountNo)}
</p>
)}
</div>
),
}}
>
<div className="space-y-4">
{/* ========== ACCOUNT GUIDE ========== */}
<section className="rounded-xl border border-zinc-200 bg-zinc-50/70 p-3 dark:border-zinc-800 dark:bg-zinc-900/30">
<p className="flex items-center gap-1.5 text-xs font-semibold text-zinc-700 dark:text-zinc-200">
<FileLock2 className="h-3.5 w-3.5 text-brand-500" />
</p>
</div>
<span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
{isKisProfileVerified ? "인증 완료" : "미인증"}
</span>
</div>
<p className="mt-1 text-xs leading-relaxed text-zinc-600 dark:text-zinc-300">
8-2 . : <span className="font-medium">12345678-01</span>
</p>
</section>
{/* ========== INPUTS ========== */}
<div className="mt-4">
<div className="relative">
<CreditCard className="pointer-events-none absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
<Input
type="password"
value={kisAccountNoInput}
onChange={(event) => setKisAccountNoInput(event.target.value)}
placeholder="계좌번호 (예: 12345678-01)"
className="h-9 pl-8 text-xs"
autoComplete="off"
/>
{/* ========== ACCOUNT NO INPUT ========== */}
<div className="space-y-1.5">
<Label
htmlFor="kis-account-no"
className="text-xs font-semibold text-zinc-600"
>
</Label>
<div className="group/input flex items-center overflow-hidden rounded-xl border border-zinc-200 bg-white transition-colors focus-within:border-brand-500 focus-within:ring-1 focus-within:ring-brand-500 dark:border-zinc-700 dark:bg-zinc-900/20">
<div className="flex h-10 w-10 shrink-0 items-center justify-center border-r border-zinc-100 bg-zinc-50 text-zinc-400 group-focus-within/input:text-brand-500 dark:border-zinc-800 dark:bg-zinc-800/40">
<CreditCard className="h-4 w-4" />
</div>
<Input
id="kis-account-no"
type="password"
value={kisAccountNoInput}
onChange={(e) => setKisAccountNoInput(e.target.value)}
placeholder="계좌번호 (예: 12345678-01)"
className="h-10 border-none bg-transparent px-3 text-sm shadow-none focus-visible:ring-0"
autoComplete="off"
/>
</div>
</div>
</div>
{/* ========== ACTION ========== */}
<div className="mt-4 flex items-center justify-between gap-3 border-t border-border/70 pt-4">
<Button
type="button"
onClick={handleValidateProfile}
disabled={
!isKisVerified ||
isValidating ||
!kisAccountNoInput.trim()
}
className="h-9 rounded-lg bg-brand-600 px-4 text-xs font-semibold text-white hover:bg-brand-700"
>
{isValidating ? (
<span className="flex items-center gap-1.5">
<InlineSpinner className="h-3 w-3 text-white" />
</span>
) : (
<span className="flex items-center gap-1.5">
<SearchCheck className="h-3.5 w-3.5" />
</span>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={handleDisconnectAccount}
disabled={!isKisProfileVerified && !kisAccountNoInput.trim()}
className="h-9 rounded-lg text-xs"
>
<ShieldOff className="h-3.5 w-3.5" />
</Button>
<div className="flex-1 text-right">
{errorMessage && (
<p className="flex justify-end gap-1.5 text-[11px] font-medium text-red-500">
<XCircle className="h-3.5 w-3.5" />
{errorMessage}
</p>
)}
{statusMessage && (
<p className="flex justify-end gap-1.5 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
<CheckCircle2 className="h-3.5 w-3.5" />
{statusMessage}
</p>
)}
{!statusMessage && !errorMessage && !isKisVerified && (
<p className="text-[11px] text-muted-foreground">
.
</p>
)}
{!statusMessage && !errorMessage && isKisProfileVerified && (
<p className="text-[11px] text-muted-foreground">
: {maskAccountNo(verifiedAccountNo)}
</p>
)}
</div>
</div>
</div>
</SettingsCard>
);
}