대시보드 중간 커밋

This commit is contained in:
2026-02-10 11:16:39 +09:00
parent 851a2acd69
commit 89b13ac308
52 changed files with 6955 additions and 1287 deletions

View File

@@ -1,4 +1,6 @@
import { createHash } from "node:crypto";
import { createHash } from "node:crypto";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { KisCredentialInput } from "@/lib/kis/config";
import { clearKisApprovalKeyCache } from "@/lib/kis/approval";
import { getKisConfig } from "@/lib/kis/config";
@@ -32,6 +34,7 @@ interface KisRevokeResponse {
const tokenCacheMap = new Map<string, KisTokenCache>();
const tokenIssueInFlightMap = new Map<string, Promise<KisTokenCache>>();
const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;
const TOKEN_CACHE_FILE_PATH = join(process.cwd(), ".tmp", "kis-token-cache.json");
function hashKey(value: string) {
return createHash("sha256").update(value).digest("hex");
@@ -42,6 +45,62 @@ function getTokenCacheKey(credentials?: KisCredentialInput) {
return `${config.tradingEnv}:${hashKey(config.appKey)}`;
}
interface PersistedTokenCache {
[cacheKey: string]: KisTokenCache;
}
async function readPersistedTokenCache() {
try {
const raw = await readFile(TOKEN_CACHE_FILE_PATH, "utf8");
return JSON.parse(raw) as PersistedTokenCache;
} catch {
return {};
}
}
async function writePersistedTokenCache(next: PersistedTokenCache) {
await mkdir(join(process.cwd(), ".tmp"), { recursive: true });
await writeFile(TOKEN_CACHE_FILE_PATH, JSON.stringify(next), "utf8");
}
async function getPersistedToken(cacheKey: string) {
const cache = await readPersistedTokenCache();
const token = cache[cacheKey];
if (!token) return null;
if (token.expiresAt - TOKEN_REFRESH_BUFFER_MS <= Date.now()) {
delete cache[cacheKey];
await writePersistedTokenCache(cache);
return null;
}
return token;
}
async function setPersistedToken(cacheKey: string, token: KisTokenCache) {
const cache = await readPersistedTokenCache();
cache[cacheKey] = token;
await writePersistedTokenCache(cache);
}
async function clearPersistedToken(cacheKey: string) {
const cache = await readPersistedTokenCache();
if (!(cacheKey in cache)) return;
delete cache[cacheKey];
if (Object.keys(cache).length === 0) {
try {
await unlink(TOKEN_CACHE_FILE_PATH);
} catch {
// ignore
}
return;
}
await writePersistedTokenCache(cache);
}
/**
* KIS access token 발급
* @param credentials 사용자 입력 키(선택)
@@ -159,6 +218,12 @@ export async function getKisAccessToken(credentials?: KisCredentialInput) {
return cached.token;
}
const persisted = await getPersistedToken(cacheKey);
if (persisted) {
tokenCacheMap.set(cacheKey, persisted);
return persisted.token;
}
// 같은 키로 동시에 요청이 들어오면 토큰 발급을 1회로 합칩니다.
const inFlight = tokenIssueInFlightMap.get(cacheKey);
if (inFlight) {
@@ -173,6 +238,7 @@ export async function getKisAccessToken(credentials?: KisCredentialInput) {
});
tokenCacheMap.set(cacheKey, next);
await setPersistedToken(cacheKey, next);
return next.token;
}
@@ -216,6 +282,7 @@ export async function revokeKisAccessToken(credentials?: KisCredentialInput) {
tokenCacheMap.delete(cacheKey);
tokenIssueInFlightMap.delete(cacheKey);
await clearPersistedToken(cacheKey);
clearKisApprovalKeyCache(credentials);
return payload.message ?? "접근토큰 폐기에 성공하였습니다.";