"use client"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import type { KisTradingEnv } from "@/features/dashboard/types/dashboard.types"; import { fetchKisWebSocketApproval } from "@/features/dashboard/apis/kis-auth.api"; /** * @file features/dashboard/store/use-kis-runtime-store.ts * @description KIS 키 입력/검증 상태를 zustand로 관리하고 새로고침 시 복원합니다. */ export interface KisRuntimeCredentials { appKey: string; appSecret: string; tradingEnv: KisTradingEnv; accountNo: string; } interface KisRuntimeStoreState { // [State] 입력 폼 상태 kisTradingEnvInput: KisTradingEnv; kisAppKeyInput: string; kisAppSecretInput: string; kisAccountNoInput: string; // [State] 검증/연동 상태 verifiedCredentials: KisRuntimeCredentials | null; isKisVerified: boolean; tradingEnv: KisTradingEnv; // [State] 웹소켓 승인키 wsApprovalKey: string | null; } 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; /** * 계좌번호 입력값을 변경하고 기존 검증 상태를 무효화합니다. * @param accountNo 계좌번호 (8자리-2자리) */ setKisAccountNoInput: (accountNo: string) => void; /** * 검증 성공 상태를 저장합니다. * @param credentials 검증 완료된 키 * @param tradingEnv 현재 연동 모드 * @see features/dashboard/components/dashboard-main.tsx handleValidateKis */ setVerifiedKisSession: ( credentials: KisRuntimeCredentials, tradingEnv: KisTradingEnv, ) => void; /** * 검증 실패 또는 입력 변경 시 검증 상태만 초기화합니다. * @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; /** * 웹소켓 승인키를 가져오거나 없으면 발급받습니다. * @returns approvalKey */ getOrFetchApprovalKey: () => Promise; } const INITIAL_STATE: KisRuntimeStoreState = { kisTradingEnvInput: "real", kisAppKeyInput: "", kisAppSecretInput: "", kisAccountNoInput: "", verifiedCredentials: null, isKisVerified: false, tradingEnv: "real", wsApprovalKey: null, }; // 동시 요청 방지를 위한 모듈 스코프 변수 let approvalPromise: Promise | null = null; export const useKisRuntimeStore = create< KisRuntimeStoreState & KisRuntimeStoreActions >()( persist( (set, get) => ({ ...INITIAL_STATE, setKisTradingEnvInput: (tradingEnv) => set({ kisTradingEnvInput: tradingEnv, verifiedCredentials: null, isKisVerified: false, wsApprovalKey: null, }), setKisAppKeyInput: (appKey) => set({ kisAppKeyInput: appKey, verifiedCredentials: null, isKisVerified: false, wsApprovalKey: null, }), setKisAppSecretInput: (appSecret) => set({ kisAppSecretInput: appSecret, verifiedCredentials: null, isKisVerified: false, wsApprovalKey: null, }), setKisAccountNoInput: (accountNo) => set({ kisAccountNoInput: accountNo, verifiedCredentials: null, isKisVerified: false, wsApprovalKey: null, }), setVerifiedKisSession: (credentials, tradingEnv) => set({ verifiedCredentials: credentials, isKisVerified: true, tradingEnv, // 인증이 바뀌면 승인키도 초기화 wsApprovalKey: null, }), invalidateKisVerification: () => set({ verifiedCredentials: null, isKisVerified: false, wsApprovalKey: null, }), clearKisRuntimeSession: (tradingEnv) => set({ kisTradingEnvInput: tradingEnv, kisAppKeyInput: "", kisAppSecretInput: "", kisAccountNoInput: "", verifiedCredentials: null, isKisVerified: false, tradingEnv, wsApprovalKey: null, }), 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; }, }), { name: "autotrade-kis-runtime-store", storage: createJSONStorage(() => localStorage), partialize: (state) => ({ kisTradingEnvInput: state.kisTradingEnvInput, kisAppKeyInput: state.kisAppKeyInput, kisAppSecretInput: state.kisAppSecretInput, kisAccountNoInput: state.kisAccountNoInput, verifiedCredentials: state.verifiedCredentials, isKisVerified: state.isKisVerified, tradingEnv: state.tradingEnv, // wsApprovalKey도 로컬 스토리지에 저장하여 새로고침 후에도 유지 (선택사항이나 유지하는 게 유리) // 단, 승인키 유효기간 문제가 있을 수 있으나 API 실패 시 재발급 로직을 넣거나, // 현재 로직상 인증 정보가 바뀌면 초기화되므로 저장해도 무방. // 하지만 유효기간 만료 처리가 없으므로 일단 저장하지 않는 게 안전할 수도 있음. // 사용자가 "새로고침"을 하는 빈도보다 "일반적인 사용"이 많으므로 저장하지 않음 (partialize에서 제외) // -> 코드를 보니 여기 포함시키지 않으면 저장이 안 됨. // 유효기간 처리가 없으니 승인키는 메모리에만 유지하도록 함 (새로고침 시 재발급) }), }, ), );