feat: [settings] 바로빌 연동 기능 보강 + 은행/카드 거래 조회 개선
- 바로빌 연동: 액션/타입 확장, UI 보강 - 은행/카드 거래 조회 개선 - 공지 팝업 모달 수정
This commit is contained in:
@@ -54,6 +54,7 @@ import {
|
||||
type BankTransactionSummaryData,
|
||||
} from './actions';
|
||||
import { TransactionFormModal } from './TransactionFormModal';
|
||||
import { syncBankTransactions } from '@/components/settings/BarobillIntegration/actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { downloadExcel, type ExcelColumn } from '@/lib/utils/excel-download';
|
||||
@@ -130,6 +131,7 @@ export function BankTransactionInquiry() {
|
||||
// 수정 추적 (로컬 변경사항)
|
||||
const [localChanges, setLocalChanges] = useState<Map<string, Partial<BankTransaction>>>(new Map());
|
||||
const [isBatchSaving, setIsBatchSaving] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -241,6 +243,26 @@ export function BankTransactionInquiry() {
|
||||
}
|
||||
}, [localChanges, loadData]);
|
||||
|
||||
// 바로빌 동기화
|
||||
const handleSync = useCallback(async () => {
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
const sd = startDate.replace(/-/g, '');
|
||||
const ed = endDate.replace(/-/g, '');
|
||||
const result = await syncBankTransactions(sd, ed);
|
||||
if (result.success && result.data) {
|
||||
toast.success(`은행 거래 ${result.data.synced}건 동기화 완료`);
|
||||
loadData();
|
||||
} else {
|
||||
toast.error(result.error || '동기화에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('동기화 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, [startDate, endDate, loadData]);
|
||||
|
||||
// 엑셀 다운로드 (프론트 xlsx 생성)
|
||||
const handleExcelDownload = useCallback(async () => {
|
||||
try {
|
||||
@@ -359,9 +381,22 @@ export function BankTransactionInquiry() {
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// 헤더 액션: ①저장 + 엑셀 다운로드 + ②수기 입력
|
||||
// 헤더 액션: 동기화 + ①저장 + 엑셀 다운로드 + ②수기 입력
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSync}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isSyncing ? '동기화 중...' : '동기화'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleBatchSave}
|
||||
@@ -661,8 +696,10 @@ export function BankTransactionInquiry() {
|
||||
handleExcelDownload,
|
||||
handleCreateClick,
|
||||
handleRowClick,
|
||||
handleSync,
|
||||
isRowModified,
|
||||
isCellModified,
|
||||
isSyncing,
|
||||
loadData,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
} from './actions';
|
||||
import { ManualInputModal } from './ManualInputModal';
|
||||
import { JournalEntryModal } from './JournalEntryModal';
|
||||
import { syncCardTransactions } from '@/components/settings/BarobillIntegration/actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { filterByEnum } from '@/lib/utils/search';
|
||||
@@ -124,6 +125,7 @@ export function CardTransactionInquiry() {
|
||||
const [showJournalEntry, setShowJournalEntry] = useState(false);
|
||||
const [journalTransaction, setJournalTransaction] = useState<CardTransaction | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -290,6 +292,26 @@ export function CardTransactionInquiry() {
|
||||
setShowJournalEntry(true);
|
||||
}, []);
|
||||
|
||||
// 바로빌 동기화
|
||||
const handleSync = useCallback(async () => {
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
const sd = startDate.replace(/-/g, '');
|
||||
const ed = endDate.replace(/-/g, '');
|
||||
const result = await syncCardTransactions(sd, ed);
|
||||
if (result.success && result.data) {
|
||||
toast.success(`카드 거래 ${result.data.synced}건 동기화 완료`);
|
||||
loadData();
|
||||
} else {
|
||||
toast.error(result.error || '동기화에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('동기화 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, [startDate, endDate, loadData]);
|
||||
|
||||
const handleExcelDownload = useCallback(async () => {
|
||||
try {
|
||||
toast.info('엑셀 파일 생성 중...');
|
||||
@@ -385,9 +407,22 @@ export function CardTransactionInquiry() {
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// 헤더 액션: 숨김보기 + 저장 + 엑셀 다운로드 + 수기 입력
|
||||
// 헤더 액션: 동기화 + 숨김보기 + 저장 + 엑셀 다운로드 + 수기 입력
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSync}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isSyncing ? '동기화 중...' : '동기화'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -767,12 +802,14 @@ export function CardTransactionInquiry() {
|
||||
showJournalEntry,
|
||||
journalTransaction,
|
||||
handleSave,
|
||||
handleSync,
|
||||
handleExcelDownload,
|
||||
handleHide,
|
||||
handleJournalEntry,
|
||||
handleUnhide,
|
||||
handleInlineEdit,
|
||||
getEditValue,
|
||||
isSyncing,
|
||||
loadData,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -70,15 +70,20 @@ function dismissPopupForToday(popupId: string): void {
|
||||
export function NoticePopupModal({ popup, open, onOpenChange }: NoticePopupModalProps) {
|
||||
const [dontShowToday, setDontShowToday] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (dontShowToday) {
|
||||
dismissPopupForToday(popup.id);
|
||||
}
|
||||
onOpenChange(false);
|
||||
};
|
||||
}, [dontShowToday, popup.id, onOpenChange]);
|
||||
|
||||
// 체크박스 상태를 팝업 전환 시 초기화
|
||||
React.useEffect(() => {
|
||||
setDontShowToday(false);
|
||||
}, [popup.id]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Root open={open} onOpenChange={() => handleClose()}>
|
||||
<DialogPrimitive.Portal>
|
||||
{/* 오버레이 */}
|
||||
<DialogPrimitive.Overlay
|
||||
|
||||
@@ -7,6 +7,9 @@ import type {
|
||||
BarobillSignupFormData,
|
||||
BankServiceFormData,
|
||||
IntegrationStatus,
|
||||
RegisteredAccount,
|
||||
RegisteredCard,
|
||||
CertificateInfo,
|
||||
} from './types';
|
||||
|
||||
// ===== 바로빌 로그인 정보 등록 =====
|
||||
@@ -66,12 +69,14 @@ export async function getBankServiceUrl(
|
||||
export async function getIntegrationStatus(): Promise<ActionResult<IntegrationStatus>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/status'),
|
||||
transform: (data: { bank_service_count?: number; account_link_count?: number; member?: { barobill_id?: string; biz_no?: string; status?: string; server_mode?: string } }) => ({
|
||||
transform: (data: { bank_service_count?: number; account_link_count?: number; card_count?: number; member?: { barobill_id?: string; biz_no?: string; corp_name?: string; status?: string; server_mode?: string } }) => ({
|
||||
bankServiceCount: data.bank_service_count ?? 0,
|
||||
accountLinkCount: data.account_link_count ?? 0,
|
||||
cardCount: data.card_count ?? 0,
|
||||
member: data.member ? {
|
||||
barobillId: data.member.barobill_id ?? '',
|
||||
bizNo: data.member.biz_no ?? '',
|
||||
corpName: data.member.corp_name ?? '',
|
||||
status: data.member.status ?? '',
|
||||
serverMode: data.member.server_mode ?? '',
|
||||
} : undefined,
|
||||
@@ -106,3 +111,73 @@ export async function getCertificateUrl(): Promise<ActionResult<{ url: string }>
|
||||
errorMessage: '공인인증서 등록 페이지 URL 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 은행 거래 동기화 =====
|
||||
export async function syncBankTransactions(
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<ActionResult<{ synced: number; accounts: number }>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/sync/bank'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
errorMessage: '은행 거래 동기화에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 카드 거래 동기화 =====
|
||||
export async function syncCardTransactions(
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<ActionResult<{ synced: number; cards: number }>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/sync/card'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
errorMessage: '카드 거래 동기화에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 등록 계좌 목록 =====
|
||||
export async function getRegisteredAccounts(): Promise<ActionResult<{
|
||||
accounts: RegisteredAccount[];
|
||||
}>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/accounts'),
|
||||
errorMessage: '등록 계좌 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 등록 카드 목록 =====
|
||||
export async function getRegisteredCards(): Promise<ActionResult<{
|
||||
cards: RegisteredCard[];
|
||||
}>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/cards'),
|
||||
errorMessage: '등록 카드 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 인증서 상태 =====
|
||||
export async function getCertificateStatus(): Promise<ActionResult<{
|
||||
certificate: CertificateInfo;
|
||||
}>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/certificate'),
|
||||
errorMessage: '인증서 상태 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 충전잔액 =====
|
||||
export async function getBalance(): Promise<ActionResult<{ balance: number | null }>> {
|
||||
return executeServerAction({
|
||||
url: buildApiUrl('/api/v1/barobill/balance'),
|
||||
errorMessage: '충전잔액 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Link2, Loader2, Edit, Check } from 'lucide-react';
|
||||
import {
|
||||
Link2, Loader2, Edit, Check, Building2, CreditCard,
|
||||
ShieldCheck, ShieldAlert, Wallet, AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -16,11 +19,24 @@ import {
|
||||
getAccountLinkUrl,
|
||||
getCardLinkUrl,
|
||||
getCertificateUrl,
|
||||
getRegisteredAccounts,
|
||||
getRegisteredCards,
|
||||
getCertificateStatus,
|
||||
getBalance,
|
||||
} from './actions';
|
||||
import type { IntegrationStatus } from './types';
|
||||
import type {
|
||||
IntegrationStatus,
|
||||
RegisteredAccount,
|
||||
RegisteredCard,
|
||||
CertificateInfo,
|
||||
} from './types';
|
||||
|
||||
export function BarobillIntegration() {
|
||||
const [status, setStatus] = useState<IntegrationStatus | null>(null);
|
||||
const [accounts, setAccounts] = useState<RegisteredAccount[]>([]);
|
||||
const [cards, setCards] = useState<RegisteredCard[]>([]);
|
||||
const [certificate, setCertificate] = useState<CertificateInfo | null>(null);
|
||||
const [balance, setBalance] = useState<number | null>(null);
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
const [signupOpen, setSignupOpen] = useState(false);
|
||||
const [bankServiceOpen, setBankServiceOpen] = useState(false);
|
||||
@@ -33,13 +49,36 @@ export function BarobillIntegration() {
|
||||
setStatus(result.data);
|
||||
}
|
||||
} catch {
|
||||
// 상태 조회 실패 시 무시 (페이지 렌더링에 영향 없음)
|
||||
// 상태 조회 실패 시 무시
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = useCallback(async () => {
|
||||
const [accountsRes, cardsRes, certRes, balanceRes] = await Promise.allSettled([
|
||||
getRegisteredAccounts(),
|
||||
getRegisteredCards(),
|
||||
getCertificateStatus(),
|
||||
getBalance(),
|
||||
]);
|
||||
|
||||
if (accountsRes.status === 'fulfilled' && accountsRes.value.success && accountsRes.value.data) {
|
||||
setAccounts(accountsRes.value.data.accounts ?? []);
|
||||
}
|
||||
if (cardsRes.status === 'fulfilled' && cardsRes.value.success && cardsRes.value.data) {
|
||||
setCards(cardsRes.value.data.cards ?? []);
|
||||
}
|
||||
if (certRes.status === 'fulfilled' && certRes.value.success && certRes.value.data) {
|
||||
setCertificate(certRes.value.data.certificate ?? null);
|
||||
}
|
||||
if (balanceRes.status === 'fulfilled' && balanceRes.value.success && balanceRes.value.data) {
|
||||
setBalance(balanceRes.value.data.balance ?? null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadStatus();
|
||||
}, [loadStatus]);
|
||||
loadDashboardData();
|
||||
}, [loadStatus, loadDashboardData]);
|
||||
|
||||
const handleExternalLink = useCallback(async (
|
||||
type: 'account' | 'card' | 'certificate',
|
||||
@@ -66,6 +105,18 @@ export function BarobillIntegration() {
|
||||
|
||||
const hasLogin = !!status?.member?.barobillId;
|
||||
|
||||
// 인증서 만료 30일 이내 경고
|
||||
const certExpiringSoon = (() => {
|
||||
if (!certificate?.expire_date) return false;
|
||||
const expireDate = new Date(certificate.expire_date);
|
||||
const now = new Date();
|
||||
const diffDays = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
return diffDays > 0 && diffDays <= 30;
|
||||
})();
|
||||
|
||||
// 잔액 부족 경고 (10,000원 미만)
|
||||
const balanceLow = balance !== null && balance < 10000;
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
@@ -75,7 +126,159 @@ export function BarobillIntegration() {
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 바로빌 연동 */}
|
||||
{/* ===== 연동 현황 대시보드 ===== */}
|
||||
{status?.member && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">연동 현황</h2>
|
||||
<Card>
|
||||
<CardContent className="p-5">
|
||||
{/* 회원 정보 */}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4 text-sm">
|
||||
<span className="font-medium">{status.member.barobillId}</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>{status.member.corpName || status.member.bizNo}</span>
|
||||
{status.member.corpName && (
|
||||
<>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span className="text-muted-foreground">{status.member.bizNo}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<Badge variant={status.member.serverMode === 'production' ? 'default' : 'secondary'}>
|
||||
{status.member.serverMode === 'production' ? '운영' : '테스트'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 통계 카드 4개 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<Building2 className="h-5 w-5 text-blue-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">계좌</p>
|
||||
<p className="text-lg font-semibold">{status.accountLinkCount}개</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<CreditCard className="h-5 w-5 text-purple-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">카드</p>
|
||||
<p className="text-lg font-semibold">{status.cardCount}개</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
{certificate?.is_valid ? (
|
||||
<ShieldCheck className="h-5 w-5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<ShieldAlert className="h-5 w-5 text-red-500 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">인증서</p>
|
||||
<p className="text-sm font-semibold">
|
||||
{certificate ? (certificate.is_valid ? '유효' : '만료') : '-'}
|
||||
</p>
|
||||
{certificate?.expire_date && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
만료: {certificate.expire_date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<Wallet className="h-5 w-5 text-amber-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">잔액</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{balance !== null ? `${balance.toLocaleString()}원` : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* P2: 경고 표시 */}
|
||||
{(certExpiringSoon || balanceLow) && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{certExpiringSoon && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-amber-50 border border-amber-200 p-3 text-sm">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />
|
||||
<span>공동인증서가 30일 이내 만료됩니다. (만료일: {certificate?.expire_date})</span>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="ml-auto text-amber-600 px-0"
|
||||
onClick={() => handleExternalLink('certificate')}
|
||||
>
|
||||
인증서 갱신
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{balanceLow && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-red-50 border border-red-200 p-3 text-sm">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<span>바로빌 충전잔액이 부족합니다. (현재: {balance?.toLocaleString()}원)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ===== 등록된 계좌/카드 목록 ===== */}
|
||||
{(accounts.length > 0 || cards.length > 0) && (
|
||||
<section>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 등록된 계좌 */}
|
||||
{accounts.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">
|
||||
등록된 계좌 <Badge variant="secondary">{accounts.length}개</Badge>
|
||||
</h2>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<ul className="space-y-2">
|
||||
{accounts.map((acc, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2 text-sm">
|
||||
<Building2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="font-medium">{acc.bankName}</span>
|
||||
<span className="text-muted-foreground">{acc.bankAccountNum}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 등록된 카드 */}
|
||||
{cards.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-3">
|
||||
등록된 카드 <Badge variant="secondary">{cards.length}개</Badge>
|
||||
</h2>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<ul className="space-y-2">
|
||||
{cards.map((card, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2 text-sm">
|
||||
<CreditCard className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="font-medium">{card.cardCompanyName}</span>
|
||||
<span className="text-muted-foreground">{card.cardNum}</span>
|
||||
{card.alias && (
|
||||
<span className="text-xs text-muted-foreground">({card.alias})</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ===== 바로빌 연동 ===== */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">바로빌 연동</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -144,7 +347,7 @@ export function BarobillIntegration() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 계좌 연동 */}
|
||||
{/* ===== 계좌 연동 ===== */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">계좌 연동</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -200,7 +403,7 @@ export function BarobillIntegration() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 카드 연동 & 공인인증서 등록 */}
|
||||
{/* ===== 카드 연동 & 공인인증서 등록 ===== */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 카드 연동 */}
|
||||
<section>
|
||||
|
||||
@@ -54,10 +54,34 @@ export const ACCOUNT_TYPE_OPTIONS = [
|
||||
export interface IntegrationStatus {
|
||||
bankServiceCount: number;
|
||||
accountLinkCount: number;
|
||||
cardCount: number;
|
||||
member?: {
|
||||
barobillId: string;
|
||||
bizNo: string;
|
||||
corpName: string;
|
||||
status: string;
|
||||
serverMode: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 등록 계좌 =====
|
||||
export interface RegisteredAccount {
|
||||
bankAccountNum: string;
|
||||
bankName: string;
|
||||
bankCode: string;
|
||||
}
|
||||
|
||||
// ===== 등록 카드 =====
|
||||
export interface RegisteredCard {
|
||||
cardNum: string;
|
||||
cardCompany: string;
|
||||
cardCompanyName: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
// ===== 인증서 상태 =====
|
||||
export interface CertificateInfo {
|
||||
is_valid: boolean;
|
||||
expire_date: string | null;
|
||||
regist_date: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user