Files
auto-trade/features/dashboard/store/use-kis-runtime-store.ts

230 lines
7.4 KiB
TypeScript
Raw Normal View History

2026-02-06 17:50:35 +09:00
"use client";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import type { KisTradingEnv } from "@/features/dashboard/types/dashboard.types";
2026-02-10 11:16:39 +09:00
import { fetchKisWebSocketApproval } from "@/features/dashboard/apis/kis-auth.api";
2026-02-06 17:50:35 +09:00
/**
* @file features/dashboard/store/use-kis-runtime-store.ts
* @description KIS / zustand로 .
*/
export interface KisRuntimeCredentials {
appKey: string;
appSecret: string;
tradingEnv: KisTradingEnv;
2026-02-10 11:16:39 +09:00
accountNo: string;
2026-02-06 17:50:35 +09:00
}
interface KisRuntimeStoreState {
// [State] 입력 폼 상태
kisTradingEnvInput: KisTradingEnv;
kisAppKeyInput: string;
kisAppSecretInput: string;
2026-02-10 11:16:39 +09:00
kisAccountNoInput: string;
2026-02-06 17:50:35 +09:00
// [State] 검증/연동 상태
verifiedCredentials: KisRuntimeCredentials | null;
isKisVerified: boolean;
tradingEnv: KisTradingEnv;
2026-02-10 11:16:39 +09:00
// [State] 웹소켓 승인키
wsApprovalKey: string | null;
2026-02-06 17:50:35 +09:00
}
interface KisRuntimeStoreActions {
/**
* .
* @param tradingEnv
* @see features/dashboard/components/dashboard-main.tsx onClick
*/
setKisTradingEnvInput: (tradingEnv: KisTradingEnv) => void;
/**
* .
* @param appKey
* @see features/dashboard/components/dashboard-main.tsx App Key onChange
*/
setKisAppKeyInput: (appKey: string) => void;
/**
* 릿 .
* @param appSecret 릿
* @see features/dashboard/components/dashboard-main.tsx App Secret onChange
*/
setKisAppSecretInput: (appSecret: string) => void;
2026-02-10 11:16:39 +09:00
/**
* .
* @param accountNo (8-2)
*/
setKisAccountNoInput: (accountNo: string) => void;
2026-02-06 17:50:35 +09:00
/**
* .
* @param credentials
* @param tradingEnv
* @see features/dashboard/components/dashboard-main.tsx handleValidateKis
*/
2026-02-10 11:16:39 +09:00
setVerifiedKisSession: (
credentials: KisRuntimeCredentials,
tradingEnv: KisTradingEnv,
) => void;
2026-02-06 17:50:35 +09:00
/**
* .
* @see features/dashboard/components/dashboard-main.tsx handleValidateKis catch
*/
invalidateKisVerification: () => void;
/**
* / .
* @param tradingEnv
* @see features/dashboard/components/dashboard-main.tsx handleRevokeKis
*/
clearKisRuntimeSession: (tradingEnv: KisTradingEnv) => void;
2026-02-10 11:16:39 +09:00
/**
* .
* @returns approvalKey
*/
getOrFetchApprovalKey: () => Promise<string | null>;
2026-02-06 17:50:35 +09:00
}
const INITIAL_STATE: KisRuntimeStoreState = {
kisTradingEnvInput: "real",
kisAppKeyInput: "",
kisAppSecretInput: "",
2026-02-10 11:16:39 +09:00
kisAccountNoInput: "",
2026-02-06 17:50:35 +09:00
verifiedCredentials: null,
isKisVerified: false,
tradingEnv: "real",
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
};
2026-02-10 11:16:39 +09:00
// 동시 요청 방지를 위한 모듈 스코프 변수
let approvalPromise: Promise<string | null> | null = null;
export const useKisRuntimeStore = create<
KisRuntimeStoreState & KisRuntimeStoreActions
>()(
2026-02-06 17:50:35 +09:00
persist(
2026-02-10 11:16:39 +09:00
(set, get) => ({
2026-02-06 17:50:35 +09:00
...INITIAL_STATE,
setKisTradingEnvInput: (tradingEnv) =>
set({
kisTradingEnvInput: tradingEnv,
verifiedCredentials: null,
isKisVerified: false,
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
setKisAppKeyInput: (appKey) =>
set({
kisAppKeyInput: appKey,
verifiedCredentials: null,
isKisVerified: false,
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
setKisAppSecretInput: (appSecret) =>
set({
kisAppSecretInput: appSecret,
verifiedCredentials: null,
isKisVerified: false,
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
}),
setKisAccountNoInput: (accountNo) =>
set({
kisAccountNoInput: accountNo,
verifiedCredentials: null,
isKisVerified: false,
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
setVerifiedKisSession: (credentials, tradingEnv) =>
set({
verifiedCredentials: credentials,
isKisVerified: true,
tradingEnv,
2026-02-10 11:16:39 +09:00
// 인증이 바뀌면 승인키도 초기화
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
invalidateKisVerification: () =>
set({
verifiedCredentials: null,
isKisVerified: false,
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
clearKisRuntimeSession: (tradingEnv) =>
set({
kisTradingEnvInput: tradingEnv,
kisAppKeyInput: "",
kisAppSecretInput: "",
2026-02-10 11:16:39 +09:00
kisAccountNoInput: "",
2026-02-06 17:50:35 +09:00
verifiedCredentials: null,
isKisVerified: false,
tradingEnv,
2026-02-10 11:16:39 +09:00
wsApprovalKey: null,
2026-02-06 17:50:35 +09:00
}),
2026-02-10 11:16:39 +09:00
getOrFetchApprovalKey: async () => {
const { wsApprovalKey, verifiedCredentials } = get();
// 1. 이미 키가 있으면 반환
if (wsApprovalKey) {
return wsApprovalKey;
}
// 2. 인증 정보가 없으면 실패
if (!verifiedCredentials) {
return null;
}
// 3. 이미 진행 중인 요청이 있다면 해당 Promise 반환 (Deduping)
if (approvalPromise) {
return approvalPromise;
}
// 4. API 호출
approvalPromise = (async () => {
try {
const data = await fetchKisWebSocketApproval(verifiedCredentials);
if (data.approvalKey) {
set({ wsApprovalKey: data.approvalKey });
return data.approvalKey;
}
return null;
} catch (error) {
console.error(error);
return null;
} finally {
approvalPromise = null;
}
})();
return approvalPromise;
},
2026-02-06 17:50:35 +09:00
}),
{
name: "autotrade-kis-runtime-store",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
kisTradingEnvInput: state.kisTradingEnvInput,
kisAppKeyInput: state.kisAppKeyInput,
kisAppSecretInput: state.kisAppSecretInput,
2026-02-10 11:16:39 +09:00
kisAccountNoInput: state.kisAccountNoInput,
2026-02-06 17:50:35 +09:00
verifiedCredentials: state.verifiedCredentials,
isKisVerified: state.isKisVerified,
tradingEnv: state.tradingEnv,
2026-02-10 11:16:39 +09:00
// wsApprovalKey도 로컬 스토리지에 저장하여 새로고침 후에도 유지 (선택사항이나 유지하는 게 유리)
// 단, 승인키 유효기간 문제가 있을 수 있으나 API 실패 시 재발급 로직을 넣거나,
// 현재 로직상 인증 정보가 바뀌면 초기화되므로 저장해도 무방.
// 하지만 유효기간 만료 처리가 없으므로 일단 저장하지 않는 게 안전할 수도 있음.
// 사용자가 "새로고침"을 하는 빈도보다 "일반적인 사용"이 많으므로 저장하지 않음 (partialize에서 제외)
// -> 코드를 보니 여기 포함시키지 않으면 저장이 안 됨.
// 유효기간 처리가 없으니 승인키는 메모리에만 유지하도록 함 (새로고침 시 재발급)
2026-02-06 17:50:35 +09:00
}),
},
),
);