feat(WEB): 회계/설정/카드 관리 페이지 대규모 기능 추가 및 리팩토링

- 일반전표입력, 상품권관리, 세금계산서 발행/조회 신규 페이지 추가
- 바로빌 연동 설정 페이지 추가
- 카드관리/계좌관리 리스트 UniversalListPage 공통 구조로 전환
- 카드거래조회/은행거래조회 리팩토링 (모달 분리, 액션 확장)
- 계좌 상세 폼(AccountDetailForm) 신규 구현
- 카드 상세(CardDetail) 신규 구현 + CardNumberInput 적용
- DateRangeSelector, StatCards, IntegratedListTemplateV2 공통 컴포넌트 개선
- 레거시 파일 정리 (CardManagementUnified, cardConfig, _legacy 등)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-15 23:18:45 +09:00
parent 7ce4efa146
commit 7f39f3066f
81 changed files with 12848 additions and 2749 deletions

View File

@@ -1,13 +0,0 @@
'use client';
import { use } from 'react';
import CardTransactionDetailClient from '@/components/accounting/CardTransactionInquiry/CardTransactionDetailClient';
interface PageProps {
params: Promise<{ id: string }>;
}
export default function CardTransactionEditPage({ params }: PageProps) {
const { id } = use(params);
return <CardTransactionDetailClient transactionId={id} initialMode="edit" />;
}

View File

@@ -1,13 +0,0 @@
'use client';
import { use } from 'react';
import CardTransactionDetailClient from '@/components/accounting/CardTransactionInquiry/CardTransactionDetailClient';
interface PageProps {
params: Promise<{ id: string }>;
}
export default function CardTransactionDetailPage({ params }: PageProps) {
const { id } = use(params);
return <CardTransactionDetailClient transactionId={id} initialMode="view" />;
}

View File

@@ -1,7 +0,0 @@
'use client';
import CardTransactionDetailClient from '@/components/accounting/CardTransactionInquiry/CardTransactionDetailClient';
export default function CardTransactionNewPage() {
return <CardTransactionDetailClient initialMode="create" />;
}

View File

@@ -1,16 +1,7 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry';
import CardTransactionDetailClient from '@/components/accounting/CardTransactionInquiry/CardTransactionDetailClient';
export default function CardTransactionsPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
if (mode === 'new') {
return <CardTransactionDetailClient initialMode="create" />;
}
return <CardTransactionInquiry />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { GeneralJournalEntry } from '@/components/accounting/GeneralJournalEntry';
export default function GeneralJournalEntryPage() {
return <GeneralJournalEntry />;
}

View File

@@ -0,0 +1,54 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { GiftCertificateManagement } from '@/components/accounting/GiftCertificateManagement';
import { GiftCertificateDetail } from '@/components/accounting/GiftCertificateManagement/GiftCertificateDetail';
import { getGiftCertificateById } from '@/components/accounting/GiftCertificateManagement/actions';
import type { GiftCertificateFormData } from '@/components/accounting/GiftCertificateManagement/types';
export default function GiftCertificatesPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const id = searchParams.get('id');
const [editData, setEditData] = useState<GiftCertificateFormData | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (mode === 'edit' && id) {
setIsLoading(true);
getGiftCertificateById(id)
.then((result) => {
if (result.success && result.data) {
setEditData(result.data);
}
})
.finally(() => setIsLoading(false));
}
}, [mode, id]);
if (mode === 'new') {
return <GiftCertificateDetail mode="new" />;
}
if (mode === 'edit' && id) {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-muted-foreground"> ...</div>
</div>
);
}
return (
<GiftCertificateDetail
mode="edit"
id={id}
initialData={editData ?? undefined}
/>
);
}
// 목록 - 컴포넌트가 자체 데이터 로딩 처리
return <GiftCertificateManagement />;
}

View File

@@ -0,0 +1,78 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { TaxInvoiceIssuancePage } from '@/components/accounting/TaxInvoiceIssuance';
import { TaxInvoiceDetail } from '@/components/accounting/TaxInvoiceIssuance/TaxInvoiceDetail';
import {
getTaxInvoices,
getSupplierSettings,
getTaxInvoiceById,
} from '@/components/accounting/TaxInvoiceIssuance/actions';
import type { TaxInvoiceRecord, TaxInvoiceFormData, SupplierSettings } from '@/components/accounting/TaxInvoiceIssuance/types';
import { createEmptyBusinessEntity } from '@/components/accounting/TaxInvoiceIssuance/types';
type TaxInvoiceDetailData = TaxInvoiceFormData & {
id: string;
invoiceNumber: string;
status: TaxInvoiceRecord['status'];
};
export default function TaxInvoiceIssuanceRoute() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const id = searchParams.get('id');
const [data, setData] = useState<TaxInvoiceRecord[]>([]);
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings>(createEmptyBusinessEntity());
const [editData, setEditData] = useState<TaxInvoiceDetailData | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (mode === 'edit' && id) {
getTaxInvoiceById(id)
.then((result) => {
if (result.success && result.data) {
setEditData(result.data);
}
})
.finally(() => setIsLoading(false));
return;
}
Promise.all([getTaxInvoices(), getSupplierSettings()])
.then(([invoicesResult, settingsResult]) => {
if (invoicesResult.success && invoicesResult.data) {
setData(invoicesResult.data);
}
if (settingsResult.success && settingsResult.data) {
setSupplierSettings(settingsResult.data);
}
})
.finally(() => setIsLoading(false));
}, [mode, id]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-muted-foreground"> ...</div>
</div>
);
}
if (mode === 'edit' && id) {
return (
<TaxInvoiceDetail
id={id}
initialData={editData ?? undefined}
/>
);
}
return (
<TaxInvoiceIssuancePage
initialData={data}
initialSupplierSettings={supplierSettings}
/>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { TaxInvoiceManagement } from '@/components/accounting/TaxInvoiceManagement';
export default function TaxInvoicesPage() {
return <TaxInvoiceManagement />;
}

View File

@@ -1,35 +1,19 @@
'use client';
/**
* 카드 상세/수정 페이지 - IntegratedDetailTemplate 적용
*/
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { cardConfig } from '@/components/hr/CardManagement/cardConfig';
import {
getCard,
updateCard,
deleteCard,
} from '@/components/hr/CardManagement/actions';
import type { Card, CardFormData } from '@/components/hr/CardManagement/types';
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate';
import { useParams } from 'next/navigation';
import { CardDetail } from '@/components/hr/CardManagement/CardDetail';
import { getCard } from '@/components/hr/CardManagement/actions';
import type { Card } from '@/components/hr/CardManagement/types';
export default function CardDetailPage() {
const params = useParams();
const searchParams = useSearchParams();
const cardId = params.id as string;
const [card, setCard] = useState<Card | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// URL에서 mode 파라미터 확인 (?mode=edit)
const urlMode = searchParams.get('mode');
const initialMode: DetailMode = urlMode === 'edit' ? 'edit' : 'view';
// 데이터 로드
useEffect(() => {
async function loadCard() {
setIsLoading(true);
@@ -40,49 +24,28 @@ export default function CardDetailPage() {
} else {
setError(result.error || '카드를 찾을 수 없습니다.');
}
} catch (err) {
console.error('Failed to load card:', err);
} catch {
setError('카드 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
loadCard();
}, [cardId]);
// 수정 핸들러
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await updateCard(cardId, data as unknown as CardFormData);
return { success: result.success, error: result.error };
};
// 삭제 핸들러
const handleDelete = async () => {
const result = await deleteCard(cardId);
return { success: result.success, error: result.error };
};
// 에러 상태
if (error && !isLoading) {
return (
<div className="p-6">
<div className="text-center py-8 text-muted-foreground">
{error}
</div>
<div className="text-center py-8 text-muted-foreground">{error}</div>
</div>
);
}
return (
<IntegratedDetailTemplate
config={cardConfig}
mode={initialMode}
initialData={(card as unknown as Record<string, unknown>) || undefined}
itemId={cardId}
<CardDetail
card={card ?? undefined}
mode="view"
isLoading={isLoading}
onSubmit={handleSubmit}
onDelete={handleDelete}
/>
);
}

View File

@@ -2,29 +2,14 @@
import { useSearchParams } from 'next/navigation';
import { CardManagement } from '@/components/hr/CardManagement';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { cardConfig } from '@/components/hr/CardManagement/cardConfig';
import { createCard } from '@/components/hr/CardManagement/actions';
import type { CardFormData } from '@/components/hr/CardManagement/types';
import { CardDetail } from '@/components/hr/CardManagement/CardDetail';
export default function CardManagementPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await createCard(data as unknown as CardFormData);
return { success: result.success, error: result.error };
};
return (
<IntegratedDetailTemplate
config={cardConfig}
mode="create"
onSubmit={handleSubmit}
/>
);
return <CardDetail mode="create" />;
}
return <CardManagement />;

View File

@@ -1,20 +1,18 @@
'use client';
/**
* 계좌 상세/수정 페이지 - IntegratedDetailTemplate 적용
* 계좌 상세/수정 페이지 - AccountDetailForm 적용
*/
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { accountConfig } from '@/components/settings/AccountManagement/accountConfig';
import { AccountDetailForm } from '@/components/settings/AccountManagement/AccountDetailForm';
import {
getBankAccount,
updateBankAccount,
deleteBankAccount,
} from '@/components/settings/AccountManagement/actions';
import type { Account, AccountFormData } from '@/components/settings/AccountManagement/types';
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate';
export default function AccountDetailPage() {
const params = useParams();
@@ -25,11 +23,9 @@ export default function AccountDetailPage() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// URL에서 mode 파라미터 확인 (?mode=edit)
const urlMode = searchParams.get('mode');
const initialMode: DetailMode = urlMode === 'edit' ? 'edit' : 'view';
const initialMode: 'view' | 'edit' = urlMode === 'edit' ? 'edit' : 'view';
// 데이터 로드
useEffect(() => {
async function loadAccount() {
setIsLoading(true);
@@ -40,50 +36,40 @@ export default function AccountDetailPage() {
} else {
setError(result.error || '계좌를 찾을 수 없습니다.');
}
} catch (err) {
console.error('Failed to load account:', err);
} catch {
setError('계좌 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
loadAccount();
}, [accountId]);
// 수정 핸들러
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await updateBankAccount(accountId, data as unknown as Partial<AccountFormData>);
const handleSubmit = async (data: AccountFormData) => {
const result = await updateBankAccount(accountId, data);
return { success: result.success, error: result.error };
};
// 삭제 핸들러
const handleDelete = async () => {
const result = await deleteBankAccount(accountId);
return { success: result.success, error: result.error };
};
// 에러 상태
if (error && !isLoading) {
return (
<div className="p-6">
<div className="text-center py-8 text-muted-foreground">
{error}
</div>
<div className="text-center py-8 text-muted-foreground">{error}</div>
</div>
);
}
return (
<IntegratedDetailTemplate
config={accountConfig}
<AccountDetailForm
mode={initialMode}
initialData={(account as unknown as Record<string, unknown>) || undefined}
itemId={accountId}
initialData={account || undefined}
isLoading={isLoading}
onSubmit={handleSubmit}
onDelete={handleDelete}
stickyButtons={true}
/>
);
}

View File

@@ -1,26 +1,18 @@
'use client';
/**
* 계좌 등록 페이지 - IntegratedDetailTemplate 적용
* 계좌 등록 페이지 - AccountDetailForm 적용
*/
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { accountConfig } from '@/components/settings/AccountManagement/accountConfig';
import { AccountDetailForm } from '@/components/settings/AccountManagement/AccountDetailForm';
import { createBankAccount } from '@/components/settings/AccountManagement/actions';
import type { AccountFormData } from '@/components/settings/AccountManagement/types';
export default function NewAccountPage() {
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await createBankAccount(data as unknown as AccountFormData);
const handleSubmit = async (data: AccountFormData) => {
const result = await createBankAccount(data);
return { success: result.success, error: result.error };
};
return (
<IntegratedDetailTemplate
config={accountConfig}
mode="create"
onSubmit={handleSubmit}
stickyButtons={true}
/>
);
return <AccountDetailForm mode="create" onSubmit={handleSubmit} />;
}

View File

@@ -2,8 +2,7 @@
import { useSearchParams } from 'next/navigation';
import { AccountManagement } from '@/components/settings/AccountManagement';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { accountConfig } from '@/components/settings/AccountManagement/accountConfig';
import { AccountDetailForm } from '@/components/settings/AccountManagement/AccountDetailForm';
import { createBankAccount } from '@/components/settings/AccountManagement/actions';
import type { AccountFormData } from '@/components/settings/AccountManagement/types';
@@ -12,18 +11,12 @@ export default function AccountsPage() {
const mode = searchParams.get('mode');
if (mode === 'new') {
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await createBankAccount(data as unknown as AccountFormData);
const handleSubmit = async (data: AccountFormData) => {
const result = await createBankAccount(data);
return { success: result.success, error: result.error };
};
return (
<IntegratedDetailTemplate
config={accountConfig}
mode="create"
onSubmit={handleSubmit}
/>
);
return <AccountDetailForm mode="create" onSubmit={handleSubmit} />;
}
return <AccountManagement />;

View File

@@ -0,0 +1,5 @@
import { BarobillIntegration } from '@/components/settings/BarobillIntegration';
export default function BarobillIntegrationPage() {
return <BarobillIntegration />;
}