Files
auto-trade/features/dashboard/components/auth/KisAuthForm.tsx

237 lines
8.8 KiB
TypeScript
Raw Normal View History

2026-02-10 11:16:39 +09:00
import { useState, useTransition } from "react";
import { useShallow } from "zustand/react/shallow";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store";
import {
revokeKisCredentials,
validateKisCredentials,
} from "@/features/dashboard/apis/kis-auth.api";
/**
* @description KIS
* @see features/dashboard/store/use-kis-runtime-store.ts / .
*/
export function KisAuthForm() {
const {
kisTradingEnvInput,
kisAppKeyInput,
kisAppSecretInput,
verifiedCredentials,
isKisVerified,
setKisTradingEnvInput,
setKisAppKeyInput,
setKisAppSecretInput,
setVerifiedKisSession,
invalidateKisVerification,
clearKisRuntimeSession,
} = useKisRuntimeStore(
useShallow((state) => ({
kisTradingEnvInput: state.kisTradingEnvInput,
kisAppKeyInput: state.kisAppKeyInput,
kisAppSecretInput: state.kisAppSecretInput,
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<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(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("App Key와 App Secret을 모두 입력해 주세요.");
}
// 주문 기능에서 계좌번호가 필요할 수 있어 구조는 유지하되, 인증 단계에서는 입력받지 않습니다.
const credentials = {
appKey,
appSecret,
tradingEnv: kisTradingEnvInput,
accountNo: verifiedCredentials?.accountNo ?? "",
};
const result = await validateKisCredentials(credentials);
setVerifiedKisSession(credentials, result.tradingEnv);
setStatusMessage(
`${result.message} (${result.tradingEnv === "real" ? "실전" : "모의"})`,
);
} catch (err) {
invalidateKisVerification();
setErrorMessage(
err instanceof Error
? err.message
: "API 키 검증 중 오류가 발생했습니다.",
);
}
});
}
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 (
2026-02-11 14:06:06 +09:00
<Card className="border-brand-200 bg-linear-to-r from-brand-50/60 to-background dark:border-brand-700/50 dark:from-brand-900/35 dark:to-background">
2026-02-10 11:16:39 +09:00
<CardHeader>
2026-02-11 14:06:06 +09:00
<CardTitle className="text-foreground dark:text-brand-50">KIS API </CardTitle>
<CardDescription className="text-muted-foreground dark:text-brand-100/80">
2026-02-10 11:16:39 +09:00
, API .
.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* ========== CREDENTIAL INPUTS ========== */}
<div className="grid gap-3 md:grid-cols-3">
<div>
2026-02-11 14:06:06 +09:00
<label className="mb-1 block text-xs text-muted-foreground dark:text-brand-100/72">
2026-02-10 11:16:39 +09:00
</label>
<div className="flex gap-2">
<Button
type="button"
variant={kisTradingEnvInput === "real" ? "default" : "outline"}
className={cn(
2026-02-11 14:06:06 +09:00
"flex-1 border transition-all",
"dark:border-brand-700/70 dark:bg-black/20 dark:text-brand-100/80 dark:hover:bg-brand-900/45",
2026-02-10 11:16:39 +09:00
kisTradingEnvInput === "real"
2026-02-11 14:06:06 +09:00
? "bg-brand-600 text-white shadow-sm ring-2 ring-brand-300/45 hover:bg-brand-500 dark:bg-brand-500 dark:text-white dark:ring-brand-300/55"
: "text-brand-700 hover:text-brand-800 dark:text-brand-100/80",
2026-02-10 11:16:39 +09:00
)}
onClick={() => setKisTradingEnvInput("real")}
>
</Button>
<Button
type="button"
variant={kisTradingEnvInput === "mock" ? "default" : "outline"}
className={cn(
2026-02-11 14:06:06 +09:00
"flex-1 border transition-all",
"dark:border-brand-700/70 dark:bg-black/20 dark:text-brand-100/80 dark:hover:bg-brand-900/45",
2026-02-10 11:16:39 +09:00
kisTradingEnvInput === "mock"
2026-02-11 14:06:06 +09:00
? "bg-brand-600 text-white shadow-sm ring-2 ring-brand-300/45 hover:bg-brand-500 dark:bg-brand-500 dark:text-white dark:ring-brand-300/55"
: "text-brand-700 hover:text-brand-800 dark:text-brand-100/80",
2026-02-10 11:16:39 +09:00
)}
onClick={() => setKisTradingEnvInput("mock")}
>
</Button>
</div>
</div>
<div>
<label className="mb-1 block text-xs text-muted-foreground">
KIS App Key
</label>
<Input
type="password"
2026-02-11 14:06:06 +09:00
className="dark:border-brand-700/60 dark:bg-black/20 dark:text-brand-50 dark:placeholder:text-brand-200/55"
2026-02-10 11:16:39 +09:00
value={kisAppKeyInput}
onChange={(e) => setKisAppKeyInput(e.target.value)}
placeholder="App Key 입력"
autoComplete="off"
/>
</div>
<div>
<label className="mb-1 block text-xs text-muted-foreground">
KIS App Secret
</label>
<Input
type="password"
2026-02-11 14:06:06 +09:00
className="dark:border-brand-700/60 dark:bg-black/20 dark:text-brand-50 dark:placeholder:text-brand-200/55"
2026-02-10 11:16:39 +09:00
value={kisAppSecretInput}
onChange={(e) => setKisAppSecretInput(e.target.value)}
placeholder="App Secret 입력"
autoComplete="off"
/>
</div>
</div>
{/* ========== ACTIONS ========== */}
<div className="flex flex-wrap items-center gap-3">
<Button
type="button"
onClick={handleValidate}
disabled={
isValidating || !kisAppKeyInput.trim() || !kisAppSecretInput.trim()
}
className="bg-brand-600 hover:bg-brand-700"
>
{isValidating ? "검증 중..." : "API 키 검증"}
</Button>
<Button
type="button"
variant="outline"
onClick={handleRevoke}
disabled={isRevoking || !isKisVerified || !verifiedCredentials}
2026-02-11 14:06:06 +09:00
className="border-brand-200 text-brand-700 hover:bg-brand-50 hover:text-brand-800 dark:border-brand-700/60 dark:text-brand-200 dark:hover:bg-brand-900/35 dark:hover:text-brand-100"
2026-02-10 11:16:39 +09:00
>
{isRevoking ? "해제 중..." : "연결 끊기"}
</Button>
{isKisVerified ? (
2026-02-11 14:06:06 +09:00
<span className="flex items-center text-sm font-medium text-brand-700 dark:text-brand-200">
<span className="mr-1.5 h-2 w-2 rounded-full bg-brand-500 ring-2 ring-brand-100 dark:ring-brand-900" />
2026-02-10 11:16:39 +09:00
({verifiedCredentials?.tradingEnv === "real" ? "실전" : "모의"})
</span>
) : (
<span className="text-sm text-muted-foreground"></span>
)}
</div>
{errorMessage && (
<div className="text-sm font-medium text-destructive">{errorMessage}</div>
)}
2026-02-11 14:06:06 +09:00
{statusMessage && (
<div className="text-sm text-brand-700 dark:text-brand-200">{statusMessage}</div>
)}
2026-02-10 11:16:39 +09:00
</CardContent>
</Card>
);
}