전체적인 리팩토링

This commit is contained in:
2026-03-12 09:26:27 +09:00
parent 406af7408a
commit e51d767878
97 changed files with 13651 additions and 363 deletions

View File

@@ -1,14 +1,21 @@
import { useState, useTransition } from "react";
import { useEffect, useState, useTransition } from "react";
import { useShallow } from "zustand/react/shallow";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
import {
revokeKisCredentials,
validateKisCredentials,
} from "@/features/settings/apis/kis-auth.api";
import {
getRememberedKisValue,
isKisRememberEnabled,
setKisRememberEnabled,
setRememberedKisValue,
} from "@/features/settings/lib/kis-remember-storage";
import {
KeyRound,
ShieldCheck,
@@ -37,6 +44,7 @@ export function KisAuthForm() {
kisAppKeyInput,
kisAppSecretInput,
verifiedAccountNo,
hasHydrated,
verifiedCredentials,
isKisVerified,
setKisTradingEnvInput,
@@ -51,6 +59,7 @@ export function KisAuthForm() {
kisAppKeyInput: state.kisAppKeyInput,
kisAppSecretInput: state.kisAppSecretInput,
verifiedAccountNo: state.verifiedAccountNo,
hasHydrated: state._hasHydrated,
verifiedCredentials: state.verifiedCredentials,
isKisVerified: state.isKisVerified,
setKisTradingEnvInput: state.setKisTradingEnvInput,
@@ -66,6 +75,74 @@ export function KisAuthForm() {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isValidating, startValidateTransition] = useTransition();
const [isRevoking, startRevokeTransition] = useTransition();
// [State] 앱키 입력값을 브라우저 재접속 후 자동 복원할지 여부
const [rememberAppKey, setRememberAppKey] = useState(() =>
isKisRememberEnabled("appKey"),
);
// [State] 앱시크릿키 입력값을 브라우저 재접속 후 자동 복원할지 여부
const [rememberAppSecret, setRememberAppSecret] = useState(() =>
isKisRememberEnabled("appSecret"),
);
useEffect(() => {
if (!hasHydrated || kisAppKeyInput.trim()) {
return;
}
// [Step 1] 세션 입력값이 비어 있을 때만 저장된 앱키를 복원합니다.
const rememberedAppKey = getRememberedKisValue("appKey");
if (rememberedAppKey) {
setKisAppKeyInput(rememberedAppKey);
}
}, [hasHydrated, kisAppKeyInput, setKisAppKeyInput]);
useEffect(() => {
if (!hasHydrated || kisAppSecretInput.trim()) {
return;
}
// [Step 1] 세션 입력값이 비어 있을 때만 저장된 앱시크릿키를 복원합니다.
const rememberedAppSecret = getRememberedKisValue("appSecret");
if (rememberedAppSecret) {
setKisAppSecretInput(rememberedAppSecret);
}
}, [hasHydrated, kisAppSecretInput, setKisAppSecretInput]);
useEffect(() => {
if (!hasHydrated) {
return;
}
// [Step 2] 앱키 기억하기 체크 상태를 저장/해제합니다.
setKisRememberEnabled("appKey", rememberAppKey);
}, [hasHydrated, rememberAppKey]);
useEffect(() => {
if (!hasHydrated || !rememberAppKey) {
return;
}
// [Step 2] 앱키 입력값이 바뀔 때 기억하기가 켜져 있으면 값을 갱신합니다.
setRememberedKisValue("appKey", kisAppKeyInput);
}, [hasHydrated, rememberAppKey, kisAppKeyInput]);
useEffect(() => {
if (!hasHydrated) {
return;
}
// [Step 2] 앱시크릿키 기억하기 체크 상태를 저장/해제합니다.
setKisRememberEnabled("appSecret", rememberAppSecret);
}, [hasHydrated, rememberAppSecret]);
useEffect(() => {
if (!hasHydrated || !rememberAppSecret) {
return;
}
// [Step 2] 앱시크릿키 입력값이 바뀔 때 기억하기가 켜져 있으면 값을 갱신합니다.
setRememberedKisValue("appSecret", kisAppSecretInput);
}, [hasHydrated, rememberAppSecret, kisAppSecretInput]);
function handleValidate() {
startValidateTransition(async () => {
@@ -243,22 +320,39 @@ export function KisAuthForm() {
{/* ========== APP KEY INPUTS ========== */}
<div className="space-y-3">
<CredentialInput
id="kis-app-key"
label="앱키"
placeholder="한국투자증권 앱키 입력"
value={kisAppKeyInput}
onChange={setKisAppKeyInput}
icon={KeySquare}
/>
<CredentialInput
id="kis-app-secret"
label="앱시크릿키"
placeholder="한국투자증권 앱시크릿키 입력"
value={kisAppSecretInput}
onChange={setKisAppSecretInput}
icon={Lock}
/>
<div className="space-y-2">
<CredentialInput
id="kis-app-key"
label="앱키"
placeholder="한국투자증권 앱키 입력"
value={kisAppKeyInput}
onChange={setKisAppKeyInput}
icon={KeySquare}
/>
<RememberCheckbox
id="remember-kis-app-key"
checked={rememberAppKey}
onChange={setRememberAppKey}
label="앱키 기억하기"
/>
</div>
<div className="space-y-2">
<CredentialInput
id="kis-app-secret"
label="앱시크릿키"
placeholder="한국투자증권 앱시크릿키 입력"
value={kisAppSecretInput}
onChange={setKisAppSecretInput}
icon={Lock}
/>
<RememberCheckbox
id="remember-kis-app-secret"
checked={rememberAppSecret}
onChange={setRememberAppSecret}
label="앱시크릿키 기억하기"
/>
</div>
</div>
</div>
</SettingsCard>
@@ -306,3 +400,31 @@ function CredentialInput({
</div>
);
}
function RememberCheckbox({
id,
checked,
onChange,
label,
}: {
id: string;
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
}) {
return (
<div className="flex items-center gap-2 pl-1">
<Checkbox
id={id}
checked={checked}
onCheckedChange={(next) => onChange(next === true)}
/>
<Label
htmlFor={id}
className="cursor-pointer text-xs font-medium text-zinc-500 dark:text-zinc-300"
>
{label}
</Label>
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useTransition } from "react";
import { useEffect, useState, useTransition } from "react";
import { useShallow } from "zustand/react/shallow";
import {
CreditCard,
@@ -13,8 +13,15 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { InlineSpinner } from "@/components/ui/loading-spinner";
import { validateKisProfile } from "@/features/settings/apis/kis-auth.api";
import {
getRememberedKisValue,
isKisRememberEnabled,
setKisRememberEnabled,
setRememberedKisValue,
} from "@/features/settings/lib/kis-remember-storage";
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
import { SettingsCard } from "./SettingsCard";
@@ -28,6 +35,7 @@ export function KisProfileForm() {
const {
kisAccountNoInput,
verifiedCredentials,
hasHydrated,
isKisVerified,
isKisProfileVerified,
verifiedAccountNo,
@@ -38,6 +46,7 @@ export function KisProfileForm() {
useShallow((state) => ({
kisAccountNoInput: state.kisAccountNoInput,
verifiedCredentials: state.verifiedCredentials,
hasHydrated: state._hasHydrated,
isKisVerified: state.isKisVerified,
isKisProfileVerified: state.isKisProfileVerified,
verifiedAccountNo: state.verifiedAccountNo,
@@ -50,6 +59,40 @@ export function KisProfileForm() {
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isValidating, startValidateTransition] = useTransition();
// [State] 계좌번호 입력값을 브라우저 재접속 후 자동 복원할지 여부
const [rememberAccountNo, setRememberAccountNo] = useState(() =>
isKisRememberEnabled("accountNo"),
);
useEffect(() => {
if (!hasHydrated || kisAccountNoInput.trim()) {
return;
}
// [Step 1] 세션 입력값이 비어 있을 때만 저장된 계좌번호를 복원합니다.
const rememberedAccountNo = getRememberedKisValue("accountNo");
if (rememberedAccountNo) {
setKisAccountNoInput(rememberedAccountNo);
}
}, [hasHydrated, kisAccountNoInput, setKisAccountNoInput]);
useEffect(() => {
if (!hasHydrated) {
return;
}
// [Step 2] 계좌 기억하기 체크 상태를 저장/해제합니다.
setKisRememberEnabled("accountNo", rememberAccountNo);
}, [hasHydrated, rememberAccountNo]);
useEffect(() => {
if (!hasHydrated || !rememberAccountNo) {
return;
}
// [Step 2] 계좌번호 입력값이 바뀔 때 기억하기가 켜져 있으면 값을 갱신합니다.
setRememberedKisValue("accountNo", kisAccountNoInput);
}, [hasHydrated, rememberAccountNo, kisAccountNoInput]);
/**
* @description 계좌번호 인증을 해제하고 입력값을 비웁니다.
@@ -220,6 +263,21 @@ export function KisProfileForm() {
autoComplete="off"
/>
</div>
<div className="flex items-center gap-2 pl-1 pt-0.5">
<Checkbox
id="remember-kis-account-no"
checked={rememberAccountNo}
onCheckedChange={(checked) =>
setRememberAccountNo(checked === true)
}
/>
<Label
htmlFor="remember-kis-account-no"
className="cursor-pointer text-xs font-medium text-zinc-500 dark:text-zinc-300"
>
</Label>
</div>
</div>
</div>
</SettingsCard>

View File

@@ -0,0 +1,95 @@
/**
* [파일 역할]
* KIS 설정 화면의 "기억하기" 항목(localStorage) 키와 읽기/쓰기 유틸을 관리합니다.
*
* [주요 책임]
* - 앱키/앱시크릿키/계좌번호의 기억하기 체크 상태 저장
* - 기억하기 값 저장/조회/삭제
* - 로그아웃/세션만료 시 일괄 정리할 키 목록 제공
*/
export type KisRememberField = "appKey" | "appSecret" | "accountNo";
const KIS_REMEMBER_ENABLED_KEY_MAP = {
appKey: "autotrade-kis-remember-app-key-enabled",
appSecret: "autotrade-kis-remember-app-secret-enabled",
accountNo: "autotrade-kis-remember-account-no-enabled",
} as const;
const KIS_REMEMBER_VALUE_KEY_MAP = {
appKey: "autotrade-kis-remember-app-key",
appSecret: "autotrade-kis-remember-app-secret",
accountNo: "autotrade-kis-remember-account-no",
} as const;
export const KIS_REMEMBER_LOCAL_STORAGE_KEYS = [
...Object.values(KIS_REMEMBER_ENABLED_KEY_MAP),
...Object.values(KIS_REMEMBER_VALUE_KEY_MAP),
] as const;
function getBrowserLocalStorage() {
if (typeof window === "undefined") {
return null;
}
return window.localStorage;
}
function readLocalStorage(key: string) {
const storage = getBrowserLocalStorage();
if (!storage) {
return "";
}
return storage.getItem(key) ?? "";
}
function writeLocalStorage(key: string, value: string) {
const storage = getBrowserLocalStorage();
if (!storage) {
return;
}
storage.setItem(key, value);
}
function removeLocalStorage(key: string) {
const storage = getBrowserLocalStorage();
if (!storage) {
return;
}
storage.removeItem(key);
}
export function isKisRememberEnabled(field: KisRememberField) {
return readLocalStorage(KIS_REMEMBER_ENABLED_KEY_MAP[field]) === "1";
}
export function setKisRememberEnabled(field: KisRememberField, enabled: boolean) {
const enabledKey = KIS_REMEMBER_ENABLED_KEY_MAP[field];
if (enabled) {
writeLocalStorage(enabledKey, "1");
return;
}
removeLocalStorage(enabledKey);
removeLocalStorage(KIS_REMEMBER_VALUE_KEY_MAP[field]);
}
export function getRememberedKisValue(field: KisRememberField) {
return readLocalStorage(KIS_REMEMBER_VALUE_KEY_MAP[field]);
}
export function setRememberedKisValue(field: KisRememberField, value: string) {
const normalized = value.trim();
const valueKey = KIS_REMEMBER_VALUE_KEY_MAP[field];
if (!normalized) {
removeLocalStorage(valueKey);
return;
}
writeLocalStorage(valueKey, normalized);
}