대시보드 추가기능 + 계좌인증
This commit is contained in:
218
features/settings/components/KisProfileForm.tsx
Normal file
218
features/settings/components/KisProfileForm.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { CreditCard, CheckCircle2, SearchCheck, ShieldOff, XCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InlineSpinner } from "@/components/ui/loading-spinner";
|
||||
import { validateKisProfile } from "@/features/settings/apis/kis-auth.api";
|
||||
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
|
||||
|
||||
/**
|
||||
* @description 한국투자증권 계좌번호 검증 폼입니다.
|
||||
* @remarks UI 흐름: /settings -> 계좌번호 입력 -> 계좌 확인 버튼 -> validate-profile API -> store 반영 -> 대시보드 반영
|
||||
* @see app/api/kis/validate-profile/route.ts 계좌번호 검증 서버 라우트
|
||||
* @see features/settings/store/use-kis-runtime-store.ts 검증 성공값을 전역 상태에 저장합니다.
|
||||
*/
|
||||
export function KisProfileForm() {
|
||||
const {
|
||||
kisAccountNoInput,
|
||||
verifiedCredentials,
|
||||
isKisVerified,
|
||||
isKisProfileVerified,
|
||||
verifiedAccountNo,
|
||||
setKisAccountNoInput,
|
||||
setVerifiedKisProfile,
|
||||
invalidateKisProfileVerification,
|
||||
} = useKisRuntimeStore(
|
||||
useShallow((state) => ({
|
||||
kisAccountNoInput: state.kisAccountNoInput,
|
||||
verifiedCredentials: state.verifiedCredentials,
|
||||
isKisVerified: state.isKisVerified,
|
||||
isKisProfileVerified: state.isKisProfileVerified,
|
||||
verifiedAccountNo: state.verifiedAccountNo,
|
||||
setKisAccountNoInput: state.setKisAccountNoInput,
|
||||
setVerifiedKisProfile: state.setVerifiedKisProfile,
|
||||
invalidateKisProfileVerification: state.invalidateKisProfileVerification,
|
||||
})),
|
||||
);
|
||||
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isValidating, startValidateTransition] = useTransition();
|
||||
|
||||
/**
|
||||
* @description 계좌번호 인증을 해제하고 입력값을 비웁니다.
|
||||
* @see features/settings/store/use-kis-runtime-store.ts setKisAccountNoInput
|
||||
* @see features/settings/store/use-kis-runtime-store.ts invalidateKisProfileVerification
|
||||
*/
|
||||
function handleDisconnectAccount() {
|
||||
setStatusMessage("계좌 인증을 해제했습니다.");
|
||||
setErrorMessage(null);
|
||||
setKisAccountNoInput("");
|
||||
invalidateKisProfileVerification();
|
||||
}
|
||||
|
||||
function handleValidateProfile() {
|
||||
startValidateTransition(async () => {
|
||||
try {
|
||||
setStatusMessage(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
if (!verifiedCredentials || !isKisVerified) {
|
||||
throw new Error("먼저 앱키 연결을 완료해 주세요.");
|
||||
}
|
||||
|
||||
const accountNo = kisAccountNoInput.trim();
|
||||
|
||||
if (!accountNo) {
|
||||
throw new Error("계좌번호를 입력해 주세요.");
|
||||
}
|
||||
|
||||
if (!isValidAccountNo(accountNo)) {
|
||||
throw new Error(
|
||||
"계좌번호 형식이 올바르지 않습니다. 8-2 형식(예: 12345678-01)으로 입력해 주세요.",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await validateKisProfile({
|
||||
...verifiedCredentials,
|
||||
accountNo,
|
||||
});
|
||||
|
||||
setVerifiedKisProfile({
|
||||
accountNo: result.account.normalizedAccountNo,
|
||||
});
|
||||
|
||||
setStatusMessage(result.message);
|
||||
} catch (error) {
|
||||
invalidateKisProfileVerification();
|
||||
setErrorMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "계좌 확인 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-brand-200 bg-background p-4 shadow-sm dark:border-brand-800/45 dark:bg-brand-900/14">
|
||||
{/* ========== HEADER ========== */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold tracking-tight text-foreground">
|
||||
한국투자증권 계좌 인증
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
앱키 연결 완료 후 계좌번호를 확인합니다.
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{isKisProfileVerified ? "인증 완료" : "미인증"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ========== INPUTS ========== */}
|
||||
<div className="mt-4">
|
||||
<div className="relative">
|
||||
<CreditCard className="pointer-events-none absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="password"
|
||||
value={kisAccountNoInput}
|
||||
onChange={(event) => setKisAccountNoInput(event.target.value)}
|
||||
placeholder="계좌번호 (예: 12345678-01)"
|
||||
className="h-9 pl-8 text-xs"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ========== ACTION ========== */}
|
||||
<div className="mt-4 flex items-center justify-between gap-3 border-t border-border/70 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleValidateProfile}
|
||||
disabled={
|
||||
!isKisVerified ||
|
||||
isValidating ||
|
||||
!kisAccountNoInput.trim()
|
||||
}
|
||||
className="h-9 rounded-lg bg-brand-600 px-4 text-xs font-semibold text-white hover:bg-brand-700"
|
||||
>
|
||||
{isValidating ? (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<InlineSpinner className="h-3 w-3 text-white" />
|
||||
확인 중
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<SearchCheck className="h-3.5 w-3.5" />
|
||||
계좌 인증하기
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDisconnectAccount}
|
||||
disabled={!isKisProfileVerified && !kisAccountNoInput.trim()}
|
||||
className="h-9 rounded-lg text-xs"
|
||||
>
|
||||
<ShieldOff className="h-3.5 w-3.5" />
|
||||
계좌 인증 해제
|
||||
</Button>
|
||||
|
||||
<div className="flex-1 text-right">
|
||||
{errorMessage && (
|
||||
<p className="flex justify-end gap-1.5 text-[11px] font-medium text-red-500">
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
{statusMessage && (
|
||||
<p className="flex justify-end gap-1.5 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
{statusMessage}
|
||||
</p>
|
||||
)}
|
||||
{!statusMessage && !errorMessage && !isKisVerified && (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
먼저 앱키 연결을 완료해 주세요.
|
||||
</p>
|
||||
)}
|
||||
{!statusMessage && !errorMessage && isKisProfileVerified && (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
확인된 계좌: {maskAccountNo(verifiedAccountNo)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description KIS 계좌번호(8-2) 입력 포맷을 검증합니다.
|
||||
* @param value 사용자 입력 계좌번호
|
||||
* @returns 형식 유효 여부
|
||||
* @see features/settings/components/KisProfileForm.tsx handleValidateProfile
|
||||
*/
|
||||
function isValidAccountNo(value: string) {
|
||||
const digits = value.replace(/\D/g, "");
|
||||
return digits.length === 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 표시용 계좌번호를 마스킹 처리합니다.
|
||||
* @param value 계좌번호(8-2)
|
||||
* @returns 마스킹 계좌번호
|
||||
* @see features/settings/components/KisProfileForm.tsx 확인된 값 표시
|
||||
*/
|
||||
function maskAccountNo(value: string | null) {
|
||||
if (!value) return "-";
|
||||
const digits = value.replace(/\D/g, "");
|
||||
if (digits.length !== 10) return "********";
|
||||
return "********-**";
|
||||
}
|
||||
Reference in New Issue
Block a user