스킬 정리 및 리팩토링

This commit is contained in:
2026-02-26 09:05:17 +09:00
parent 4c52d6d82f
commit 406af7408a
71 changed files with 3776 additions and 3934 deletions

View File

@@ -0,0 +1,82 @@
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import {
DOMESTIC_KIS_SESSION_OVERRIDE_HEADER,
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,
parseDomesticKisSession,
} from "@/lib/kis/domestic-market-session";
export interface KisApiErrorPayload {
ok?: boolean;
message?: string;
error?: string;
errorCode?: string;
}
interface BuildKisRequestHeadersOptions {
jsonContentType?: boolean;
includeAccountNo?: boolean;
includeSessionOverride?: boolean;
}
/**
* @description KIS API 응답에서 사용자 노출용 에러 메시지를 추출합니다.
* @see features/trade/apis/kis-stock.api.ts 종목/주문 API 실패 처리
* @see features/dashboard/apis/dashboard.api.ts 대시보드 API 실패 처리
*/
export function resolveKisApiErrorMessage(
payload: unknown,
fallbackMessage: string,
) {
if (!payload || typeof payload !== "object") {
return fallbackMessage;
}
const response = payload as KisApiErrorPayload;
return response.message || response.error || fallbackMessage;
}
/**
* @description KIS API 호출용 공통 헤더를 생성합니다.
* @see features/dashboard/apis/dashboard.api.ts 잔고/지수/활동 조회 공통 헤더
* @see features/trade/apis/kis-stock.api.ts 종목/호가/차트/주문 공통 헤더
*/
export function buildKisRequestHeaders(
credentials: KisRuntimeCredentials,
options?: BuildKisRequestHeadersOptions,
) {
const headers: Record<string, string> = {
"x-kis-app-key": credentials.appKey,
"x-kis-app-secret": credentials.appSecret,
"x-kis-trading-env": credentials.tradingEnv,
};
if (options?.jsonContentType) {
headers["content-type"] = "application/json";
}
if (options?.includeAccountNo && credentials.accountNo.trim()) {
headers["x-kis-account-no"] = credentials.accountNo.trim();
}
if (options?.includeSessionOverride) {
const sessionOverride = readSessionOverrideForDev();
if (sessionOverride) {
headers[DOMESTIC_KIS_SESSION_OVERRIDE_HEADER] = sessionOverride;
}
}
return headers;
}
function readSessionOverrideForDev() {
if (typeof window === "undefined") return null;
try {
const raw = window.localStorage.getItem(
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,
);
return parseDomesticKisSession(raw);
} catch {
return null;
}
}

View File

@@ -1,4 +1,8 @@
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import {
resolveKisApiErrorMessage,
type KisApiErrorPayload,
} from "@/features/settings/apis/kis-api-utils";
import type {
DashboardKisProfileValidateResponse,
DashboardKisRevokeResponse,
@@ -25,13 +29,13 @@ async function postKisAuthApi<T extends KisApiBaseResponse>(
cache: "no-store",
});
const payload = (await response.json()) as T;
const payload = (await response.json()) as T | KisApiErrorPayload;
if (!response.ok || !payload.ok) {
throw new Error(payload.message || fallbackErrorMessage);
throw new Error(resolveKisApiErrorMessage(payload, fallbackErrorMessage));
}
return payload;
return payload as T;
}
/**

View File

@@ -240,11 +240,14 @@ export const useKisRuntimeStore = create<
}),
{
name: "autotrade-kis-runtime-store",
storage: createJSONStorage(() => localStorage),
// 민감정보(appKey/appSecret/accountNo)는 브라우저 세션 범위로만 유지합니다.
storage: createJSONStorage(() => sessionStorage),
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true);
},
partialize: (state) => ({
// 새로고침 시 인증이 풀리지 않도록, "세션 범위"에서만 인증/입력 상태를 유지합니다.
// 브라우저 종료 시 sessionStorage가 비워지므로 장기 영속(localStorage)은 하지 않습니다.
kisTradingEnvInput: state.kisTradingEnvInput,
kisAppKeyInput: state.kisAppKeyInput,
kisAppSecretInput: state.kisAppSecretInput,
@@ -254,7 +257,6 @@ export const useKisRuntimeStore = create<
isKisProfileVerified: state.isKisProfileVerified,
verifiedAccountNo: state.verifiedAccountNo,
tradingEnv: state.tradingEnv,
// wsApprovalKey/wsUrl are kept in memory only (expiration-sensitive).
}),
},
),