import { useState, useTransition } from "react"; 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, validateKisCredentials, } from "@/features/settings/apis/kis-auth.api"; import { KeyRound, ShieldCheck, CheckCircle2, XCircle, Lock, 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 한국투자증권 앱키/앱시크릿키 인증 폼입니다. * @remarks UI 흐름: /settings -> 앱키/앱시크릿키 입력 -> 연결 확인 버튼 -> /api/kis/validate -> 연결 상태 반영 * @see app/api/kis/validate/route.ts 앱키 검증 API * @see features/settings/store/use-kis-runtime-store.ts 인증 상태 저장소 */ export function KisAuthForm() { const { kisTradingEnvInput, kisAppKeyInput, kisAppSecretInput, verifiedAccountNo, verifiedCredentials, isKisVerified, setKisTradingEnvInput, setKisAppKeyInput, setKisAppSecretInput, setVerifiedKisSession, invalidateKisVerification, clearKisRuntimeSession, } = useKisRuntimeStore( useShallow((state) => ({ kisTradingEnvInput: state.kisTradingEnvInput, kisAppKeyInput: state.kisAppKeyInput, kisAppSecretInput: state.kisAppSecretInput, verifiedAccountNo: state.verifiedAccountNo, verifiedCredentials: state.verifiedCredentials, isKisVerified: state.isKisVerified, setKisTradingEnvInput: state.setKisTradingEnvInput, setKisAppKeyInput: state.setKisAppKeyInput, setKisAppSecretInput: state.setKisAppSecretInput, setVerifiedKisSession: state.setVerifiedKisSession, invalidateKisVerification: state.invalidateKisVerification, clearKisRuntimeSession: state.clearKisRuntimeSession, })), ); const [statusMessage, setStatusMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [isValidating, startValidateTransition] = useTransition(); const [isRevoking, startRevokeTransition] = useTransition(); function handleValidate() { startValidateTransition(async () => { try { setErrorMessage(null); setStatusMessage(null); const appKey = kisAppKeyInput.trim(); const appSecret = kisAppSecretInput.trim(); if (!appKey || !appSecret) { throw new Error("앱키와 앱시크릿키를 모두 입력해 주세요."); } const credentials = { appKey, appSecret, tradingEnv: kisTradingEnvInput, accountNo: verifiedAccountNo ?? "", }; const result = await validateKisCredentials(credentials); setVerifiedKisSession(credentials, result.tradingEnv); setStatusMessage( `${result.message} (${result.tradingEnv === "real" ? "실전" : "모의"})`, ); } catch (err) { invalidateKisVerification(); setErrorMessage( err instanceof Error ? err.message : "앱키 확인 중 오류가 발생했습니다.", ); } }); } function handleRevoke() { if (!verifiedCredentials) return; startRevokeTransition(async () => { try { setErrorMessage(null); setStatusMessage(null); const result = await revokeKisCredentials(verifiedCredentials); clearKisRuntimeSession(result.tradingEnv); setStatusMessage( `${result.message} (${result.tradingEnv === "real" ? "실전" : "모의"})`, ); } catch (err) { setErrorMessage( err instanceof Error ? err.message : "연결 해제 중 오류가 발생했습니다.", ); } }); } return ( 연결됨 ) : undefined } footer={{ actions: (
{isKisVerified && ( )}
), status: (
{errorMessage && (

{errorMessage}

)} {statusMessage && (

{statusMessage}

)} {!errorMessage && !statusMessage && !isKisVerified && (

미연결 상태입니다

)}
), }} className="h-full" >
{/* ========== TRADING MODE ========== */}
투자 모드 선택
{/* ========== APP KEY INPUTS ========== */}
); } /** * @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 (
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" />
); }