대시보드 실시간 기능 추가
This commit is contained in:
@@ -3,6 +3,7 @@ import { useShallow } from "zustand/react/shallow";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
|
||||
import {
|
||||
revokeKisCredentials,
|
||||
@@ -10,15 +11,19 @@ import {
|
||||
} from "@/features/settings/apis/kis-auth.api";
|
||||
import {
|
||||
KeyRound,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Lock,
|
||||
Sparkles,
|
||||
Zap,
|
||||
Link2,
|
||||
Unlink2,
|
||||
Activity,
|
||||
Zap,
|
||||
KeySquare,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { InlineSpinner } from "@/components/ui/loading-spinner";
|
||||
import { SettingsCard } from "./SettingsCard";
|
||||
|
||||
/**
|
||||
* @description 한국투자증권 앱키/앱시크릿키 인증 폼입니다.
|
||||
@@ -62,9 +67,6 @@ export function KisAuthForm() {
|
||||
const [isValidating, startValidateTransition] = useTransition();
|
||||
const [isRevoking, startRevokeTransition] = useTransition();
|
||||
|
||||
// 입력 필드 Focus 상태 관리를 위한 State
|
||||
const [focusedField, setFocusedField] = useState<"appKey" | "appSecret" | null>(null);
|
||||
|
||||
function handleValidate() {
|
||||
startValidateTransition(async () => {
|
||||
try {
|
||||
@@ -123,134 +125,21 @@ export function KisAuthForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative w-full overflow-hidden rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm transition-all 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">
|
||||
{/* Inner Content Container - Compact spacing */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg 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">
|
||||
<KeyRound className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="flex items-center gap-2 text-base font-bold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
한국투자증권 앱키 연결
|
||||
{isKisVerified && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-green-50 px-1.5 py-0.5 text-[10px] font-medium text-green-600 ring-1 ring-green-600/10 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/30">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
연결됨
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-[11px] font-medium text-zinc-500 dark:text-zinc-400">
|
||||
한국투자증권 Open API에서 발급받은 앱키/앱시크릿키를 입력해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Mode Switch (Segmented Control - Compact) */}
|
||||
<div className="flex shrink-0 items-center rounded-md bg-zinc-100 p-0.5 ring-1 ring-zinc-200 dark:bg-zinc-800 dark:ring-zinc-700">
|
||||
<button
|
||||
onClick={() => setKisTradingEnvInput("real")}
|
||||
className={cn(
|
||||
"relative flex items-center gap-1.5 rounded-[5px] px-2.5 py-1 text-[11px] font-semibold transition-all duration-200",
|
||||
kisTradingEnvInput === "real"
|
||||
? "bg-white text-brand-600 shadow-sm ring-1 ring-black/5 dark:bg-brand-500 dark:text-white dark:ring-brand-400"
|
||||
: "text-zinc-500 hover:text-zinc-700 hover:bg-black/5 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<Zap
|
||||
className={cn(
|
||||
"h-3 w-3",
|
||||
kisTradingEnvInput === "real"
|
||||
? "text-brand-500 dark:text-white"
|
||||
: "text-zinc-400",
|
||||
)}
|
||||
/>
|
||||
실전 투자
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setKisTradingEnvInput("mock")}
|
||||
className={cn(
|
||||
"relative flex items-center gap-1.5 rounded-[5px] px-2.5 py-1 text-[11px] font-semibold transition-all duration-200",
|
||||
kisTradingEnvInput === "mock"
|
||||
? "bg-white text-zinc-800 shadow-sm ring-1 ring-black/5 dark:bg-zinc-600 dark:text-white dark:ring-zinc-500"
|
||||
: "text-zinc-500 hover:text-zinc-700 hover:bg-black/5 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<Activity
|
||||
className={cn(
|
||||
"h-3 w-3",
|
||||
kisTradingEnvInput === "mock"
|
||||
? "text-zinc-800 dark:text-zinc-200"
|
||||
: "text-zinc-400",
|
||||
)}
|
||||
/>
|
||||
모의 투자
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Fields Section (Compact Stacked Layout) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* ========== APP KEY INPUT ========== */}
|
||||
<div
|
||||
className={cn(
|
||||
"group/input relative flex items-center overflow-hidden rounded-lg border bg-white transition-all duration-200 dark:bg-zinc-900/30",
|
||||
focusedField === "appKey"
|
||||
? "border-brand-500 ring-1 ring-brand-500"
|
||||
: "border-zinc-200 hover:border-zinc-300 dark:border-zinc-700 dark:hover:border-zinc-600",
|
||||
)}
|
||||
>
|
||||
<div className="hidden h-9 w-9 items-center justify-center border-r border-zinc-100 bg-zinc-50 text-zinc-400 transition-colors group-focus-within/input:text-brand-500 dark:border-zinc-800 dark:bg-zinc-800/50 dark:text-zinc-500 dark:group-focus-within/input:text-brand-400 sm:flex">
|
||||
<Shield className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div className="flex h-9 min-w-[70px] items-center justify-center bg-zinc-50 px-2 text-[11px] font-semibold text-zinc-500 border-r border-zinc-100 sm:hidden dark:bg-zinc-800/50 dark:text-zinc-500 dark:border-zinc-800">
|
||||
앱키
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={kisAppKeyInput}
|
||||
onChange={(e) => setKisAppKeyInput(e.target.value)}
|
||||
onFocus={() => setFocusedField("appKey")}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
placeholder="한국투자증권 앱키 입력"
|
||||
className="h-9 flex-1 border-none bg-transparent px-3 text-xs text-zinc-900 placeholder:text-zinc-400 focus-visible:ring-0 dark:text-zinc-100 dark:placeholder:text-zinc-600"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ========== APP SECRET INPUT ========== */}
|
||||
<div
|
||||
className={cn(
|
||||
"group/input relative flex items-center overflow-hidden rounded-lg border bg-white transition-all duration-200 dark:bg-zinc-900/30",
|
||||
focusedField === "appSecret"
|
||||
? "border-brand-500 ring-1 ring-brand-500"
|
||||
: "border-zinc-200 hover:border-zinc-300 dark:border-zinc-700 dark:hover:border-zinc-600",
|
||||
)}
|
||||
>
|
||||
<div className="hidden h-9 w-9 items-center justify-center border-r border-zinc-100 bg-zinc-50 text-zinc-400 transition-colors group-focus-within/input:text-brand-500 dark:border-zinc-800 dark:bg-zinc-800/50 dark:text-zinc-500 dark:group-focus-within/input:text-brand-400 sm:flex">
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div className="flex h-9 min-w-[70px] items-center justify-center bg-zinc-50 px-2 text-[11px] font-semibold text-zinc-500 border-r border-zinc-100 sm:hidden dark:bg-zinc-800/50 dark:text-zinc-500 dark:border-zinc-800">
|
||||
시크릿키
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
value={kisAppSecretInput}
|
||||
onChange={(e) => setKisAppSecretInput(e.target.value)}
|
||||
onFocus={() => setFocusedField("appSecret")}
|
||||
onBlur={() => setFocusedField(null)}
|
||||
placeholder="한국투자증권 앱시크릿키 입력"
|
||||
className="h-9 flex-1 border-none bg-transparent px-3 text-xs text-zinc-900 placeholder:text-zinc-400 focus-visible:ring-0 dark:text-zinc-100 dark:placeholder:text-zinc-600"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action & Status Section */}
|
||||
<div className="flex items-center justify-between border-t border-zinc-100 pt-4 dark:border-zinc-800/50">
|
||||
<div className="flex gap-2">
|
||||
<SettingsCard
|
||||
icon={KeyRound}
|
||||
title="한국투자증권 앱키 연결"
|
||||
description="Open API에서 발급받은 앱키와 앱시크릿키를 입력해 연결을 완료하세요."
|
||||
badge={
|
||||
isKisVerified ? (
|
||||
<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
|
||||
}
|
||||
footer={{
|
||||
actions: (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={
|
||||
@@ -262,52 +151,160 @@ export function KisAuthForm() {
|
||||
>
|
||||
{isValidating ? (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<InlineSpinner className="text-white h-3 w-3" />
|
||||
<InlineSpinner className="h-3 w-3 text-white" />
|
||||
검증 중
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Sparkles className="h-3 w-3 fill-brand-200 text-brand-200" />
|
||||
앱키 연결 확인
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Link2 className="h-3.5 w-3.5 text-brand-100" />
|
||||
앱키 연결 확인
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isKisVerified && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRevoke}
|
||||
disabled={isRevoking}
|
||||
className="h-9 rounded-lg border-zinc-200 bg-white 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"
|
||||
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"
|
||||
>
|
||||
{isRevoking ? "해제 중" : "연결 해제"}
|
||||
{isRevoking ? (
|
||||
"해제 중"
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Unlink2 className="h-3.5 w-3.5" />
|
||||
연결 해제
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Messages - Compact */}
|
||||
<div className="flex-1 text-right">
|
||||
),
|
||||
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 justify-end gap-1.5 text-[11px] font-semibold text-red-500">
|
||||
<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 justify-end gap-1.5 text-[11px] font-semibold text-brand-600 dark:text-brand-400">
|
||||
<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>
|
||||
)}
|
||||
{!errorMessage && !statusMessage && !isKisVerified && (
|
||||
<p className="flex justify-end gap-1.5 text-[11px] text-zinc-400 dark:text-zinc-600">
|
||||
<span className="h-1.5 w-1.5 translate-y-1.5 rounded-full bg-zinc-300 dark:bg-zinc-700" />
|
||||
미연결
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* ========== TRADING MODE ========== */}
|
||||
<section className="rounded-xl border border-zinc-200 bg-zinc-50/70 p-3 dark:border-zinc-800 dark:bg-zinc-900/30">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-zinc-700 dark:text-zinc-200">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-brand-500" />
|
||||
투자 모드 선택
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setKisTradingEnvInput("real")}
|
||||
className={cn(
|
||||
"flex h-9 items-center justify-center gap-1.5 rounded-lg border text-xs font-semibold transition",
|
||||
kisTradingEnvInput === "real"
|
||||
? "border-brand-500 bg-brand-600 text-white shadow-sm"
|
||||
: "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300",
|
||||
)}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
실전 투자
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setKisTradingEnvInput("mock")}
|
||||
className={cn(
|
||||
"flex h-9 items-center justify-center gap-1.5 rounded-lg border text-xs font-semibold transition",
|
||||
kisTradingEnvInput === "mock"
|
||||
? "border-zinc-700 bg-zinc-800 text-white shadow-sm dark:border-zinc-500 dark:bg-zinc-700"
|
||||
: "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300",
|
||||
)}
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
모의 투자
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ========== APP KEY INPUTS ========== */}
|
||||
<div className="space-y-3">
|
||||
<CredentialInput
|
||||
id="kis-app-key"
|
||||
label="앱키"
|
||||
placeholder="한국투자증권 앱키 입력"
|
||||
value={kisAppKeyInput}
|
||||
onChange={setKisAppKeyInput}
|
||||
icon={KeySquare}
|
||||
/>
|
||||
<CredentialInput
|
||||
id="kis-app-secret"
|
||||
label="앱시크릿키"
|
||||
placeholder="한국투자증권 앱시크릿키 입력"
|
||||
value={kisAppSecretInput}
|
||||
onChange={setKisAppSecretInput}
|
||||
icon={Lock}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 앱키/시크릿키 입력 전용 필드 블록입니다.
|
||||
* @see features/settings/components/KisAuthForm.tsx 입력 UI 렌더링
|
||||
*/
|
||||
function CredentialInput({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
icon: Icon,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
icon: LucideIcon;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={id} className="text-xs font-semibold text-zinc-600">
|
||||
{label}
|
||||
</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">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id={id}
|
||||
type="password"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="h-10 border-none bg-transparent px-3 text-sm shadow-none focus-visible:ring-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user