feat: CEO 대시보드 리팩토링 및 회계 관리 개선

- CEO 대시보드: 컴포넌트 분리(DashboardSettingsSections, DetailModalSections), 모달/섹션 개선, useCEODashboard 최적화
- 회계: 부실채권/매출/매입/일일보고 UI 및 타입 개선
- 공통: Sidebar, useDashboardFetch 훅 추가, amount/status-config 유틸 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-03-01 12:17:40 +09:00
parent 8c0a655906
commit 1bccaffe27
39 changed files with 2599 additions and 2997 deletions

View File

@@ -32,8 +32,7 @@ import {
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { LineItemsTable, useLineItems } from '@/components/organisms/LineItemsTable';
import { salesConfig } from './salesConfig';
import type { SalesRecord, SalesItem, SalesType } from './types';
import { SALES_TYPE_OPTIONS } from './types';
import type { SalesRecord, SalesItem } from './types';
import { getSaleById, createSale, updateSale, deleteSale } from './actions';
import { toast } from 'sonner';
import { getClients } from '../VendorManagement/actions';
@@ -78,7 +77,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
const [salesDate, setSalesDate] = useState(format(new Date(), 'yyyy-MM-dd'));
const [vendorId, setVendorId] = useState('');
const [vendorName, setVendorName] = useState('');
const [salesType, setSalesType] = useState<SalesType>('product');
const [items, setItems] = useState<SalesItem[]>([createEmptyItem()]);
const [taxInvoiceIssued, setTaxInvoiceIssued] = useState(false);
const [transactionStatementIssued, setTransactionStatementIssued] = useState(false);
@@ -126,7 +124,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
setSalesDate(data.salesDate);
setVendorId(data.vendorId);
setVendorName(data.vendorName);
setSalesType(data.salesType);
setItems(data.items.length > 0 ? data.items : [createEmptyItem()]);
setTaxInvoiceIssued(data.taxInvoiceIssued);
setTransactionStatementIssued(data.transactionStatementIssued);
@@ -158,7 +155,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
const saleData: Partial<SalesRecord> = {
salesDate,
vendorId,
salesType,
items,
totalSupplyAmount: totals.supplyAmount,
totalVat: totals.vat,
@@ -189,7 +185,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
} finally {
setIsSaving(false);
}
}, [salesDate, vendorId, salesType, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]);
}, [salesDate, vendorId, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]);
// ===== 삭제 (IntegratedDetailTemplate 호환) =====
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
@@ -268,23 +264,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
</SelectContent>
</Select>
</div>
{/* 매출 유형 */}
<div className="space-y-2">
<Label htmlFor="salesType"> </Label>
<Select value={salesType} onValueChange={(v) => setSalesType(v as SalesType)} disabled={isViewMode}>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{SALES_TYPE_OPTIONS.filter(o => o.value !== 'all').map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
@@ -318,28 +297,42 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Label htmlFor="taxInvoice"> </Label>
<Switch
id="taxInvoice"
checked={taxInvoiceIssued}
onCheckedChange={setTaxInvoiceIssued}
disabled={isViewMode}
/>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Label htmlFor="taxInvoice"> </Label>
<Switch
id="taxInvoice"
checked={taxInvoiceIssued}
onCheckedChange={setTaxInvoiceIssued}
disabled={isViewMode}
/>
</div>
<div className="flex items-center gap-2">
{taxInvoiceIssued ? (
<span className="text-sm text-green-600 flex items-center gap-1">
<FileText className="h-4 w-4" />
</span>
) : (
<span className="text-sm text-gray-500 flex items-center gap-1">
<FileText className="h-4 w-4" />
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
{taxInvoiceIssued ? (
<span className="text-sm text-green-600 flex items-center gap-1">
<FileText className="h-4 w-4" />
</span>
) : (
<span className="text-sm text-gray-500 flex items-center gap-1">
<FileText className="h-4 w-4" />
</span>
)}
<div className="flex justify-end">
<Button
variant="default"
className="bg-gray-900 hover:bg-gray-800 text-white"
onClick={() => {
toast.info('세금계산서 발행 기능 준비 중입니다.');
}}
>
<FileText className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</CardContent>

View File

@@ -25,7 +25,6 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
@@ -55,11 +54,8 @@ import { MobileCard } from '@/components/organisms/MobileCard';
import type { SalesRecord } from './types';
import {
SORT_OPTIONS,
SALES_STATUS_LABELS,
SALES_STATUS_COLORS,
SALES_TYPE_LABELS,
SALES_TYPE_FILTER_OPTIONS,
ISSUANCE_FILTER_OPTIONS,
TAX_INVOICE_FILTER_OPTIONS,
TRANSACTION_STATEMENT_FILTER_OPTIONS,
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
} from './types';
import { getSales, deleteSale, toggleSaleIssuance } from './actions';
@@ -83,7 +79,6 @@ const tableColumns = [
{ key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true },
{ key: 'vat', label: '부가세', className: 'text-right', sortable: true },
{ key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true },
{ key: 'salesType', label: '매출유형', className: 'text-center', sortable: true },
{ key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' },
{ key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' },
];
@@ -113,8 +108,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
// 필터 초기값 (filterConfig 기반 - ULP가 내부 state로 관리)
const initialFilterValues: Record<string, string | string[]> = {
vendor: 'all',
salesType: 'all',
issuance: 'all',
taxInvoice: 'all',
transactionStatement: 'all',
sort: 'latest',
};
@@ -148,17 +143,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
allOptionLabel: '거래처 전체',
},
{
key: 'salesType',
label: '매출유형',
key: 'taxInvoice',
label: '세금계산서 발행여부',
type: 'single',
options: SALES_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
options: TAX_INVOICE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
allOptionLabel: '전체',
},
{
key: 'issuance',
label: '발행여부',
key: 'transactionStatement',
label: '거래명세서 발행여부',
type: 'single',
options: ISSUANCE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
options: TRANSACTION_STATEMENT_FILTER_OPTIONS.filter(o => o.value !== 'all'),
allOptionLabel: '전체',
},
{
@@ -322,18 +317,24 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
// 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리
customFilterFn: (items, fv) => {
if (!items || items.length === 0) return items;
const issuanceVal = fv.issuance as string;
const taxInvoiceVal = fv.taxInvoice as string;
const transactionStatementVal = fv.transactionStatement as string;
let result = applyFilters(items, [
enumFilter('vendorName', fv.vendor as string),
enumFilter('salesType', fv.salesType as string),
]);
// 발행여부 필터 (특수 로직 - enumFilter로 대체 불가)
if (issuanceVal === 'taxInvoicePending') {
// 세금계산서 발행여부 필터
if (taxInvoiceVal === 'issued') {
result = result.filter(item => item.taxInvoiceIssued);
} else if (taxInvoiceVal === 'notIssued') {
result = result.filter(item => !item.taxInvoiceIssued);
}
if (issuanceVal === 'transactionStatementPending') {
// 거래명세서 발행여부 필터
if (transactionStatementVal === 'issued') {
result = result.filter(item => item.transactionStatementIssued);
} else if (transactionStatementVal === 'notIssued') {
result = result.filter(item => !item.transactionStatementIssued);
}
@@ -411,7 +412,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
),
@@ -443,9 +443,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
<TableCell className="text-right">{formatNumber(item.totalSupplyAmount)}</TableCell>
<TableCell className="text-right">{formatNumber(item.totalVat)}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{SALES_TYPE_LABELS[item.salesType]}</Badge>
</TableCell>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-center gap-1">
<Switch
@@ -480,8 +477,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
key={item.id}
title={item.vendorName}
subtitle={item.salesNo}
badge={SALES_TYPE_LABELS[item.salesType]}
badgeVariant="outline"
isSelected={handlers.isSelected}
onToggle={handlers.onToggle}
onClick={() => handleRowClick(item)}

View File

@@ -133,13 +133,22 @@ export const ACCOUNT_SUBJECT_SELECTOR_OPTIONS = [
{ value: 'other', label: '기타매출' },
];
// ===== 발행여부 필터 =====
export type IssuanceFilter = 'all' | 'taxInvoicePending' | 'transactionStatementPending';
// ===== 세금계산서 발행여부 필터 =====
export type TaxInvoiceFilter = 'all' | 'issued' | 'notIssued';
export const ISSUANCE_FILTER_OPTIONS: { value: IssuanceFilter; label: string }[] = [
export const TAX_INVOICE_FILTER_OPTIONS: { value: TaxInvoiceFilter; label: string }[] = [
{ value: 'all', label: '전체' },
{ value: 'taxInvoicePending', label: '세금계산서 미발행' },
{ value: 'transactionStatementPending', label: '거래명세서 미발행' },
{ value: 'issued', label: '발행완료' },
{ value: 'notIssued', label: '미발행' },
];
// ===== 거래명세서 발행여부 필터 =====
export type TransactionStatementFilter = 'all' | 'issued' | 'notIssued';
export const TRANSACTION_STATEMENT_FILTER_OPTIONS: { value: TransactionStatementFilter; label: string }[] = [
{ value: 'all', label: '전체' },
{ value: 'issued', label: '발행완료' },
{ value: 'notIssued', label: '미발행' },
];
// ===== 매출유형 필터 옵션 (스크린샷 기준) =====