feat: 회계/결재/생산/출하/대시보드 다수 개선 및 QA 수정
- BadDebtCollection, BillManagement, CardTransaction, TaxInvoice 회계 개선 - VendorManagement/VendorDetailClient 소폭 추가 - DocumentCreate/DraftBox 결재 기능 개선 - WorkOrder Create/Detail/Edit, ShipmentEdit 생산/출하 개선 - CEO 대시보드: PurchaseStatusSection, receivable/status-issue transformer 정비 - dashboard types/invalidation 확장 - LoginPage, Sidebar, HeaderFavoritesBar 레이아웃 수정 - QMS 페이지, StockStatusDetail, OrderRegistration 소폭 수정 - AttendanceManagement, VacationManagement HR 수정 - ConstructionDetailClient 건설 상세 개선 - claudedocs: 주간 구현내역, 대시보드 QA/수정계획, 결재/품질/생산/출하 문서 추가
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
@@ -137,12 +138,14 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
if (isNewMode) {
|
||||
const result = await createBadDebt(formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateBadDebt(recordId!, formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '수정에 실패했습니다.' };
|
||||
@@ -159,6 +162,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
try {
|
||||
const result = await deleteBadDebt(String(id));
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
||||
|
||||
@@ -14,6 +14,7 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -176,6 +177,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteBadDebt(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
setData((prev) => prev.filter((item) => item.id !== id));
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import {
|
||||
@@ -90,25 +88,6 @@ export function BillManagementClient({
|
||||
const [currentPage, setCurrentPage] = useState(initialPagination.currentPage);
|
||||
const itemsPerPage = initialPagination.perPage;
|
||||
|
||||
// 삭제 다이얼로그
|
||||
const deleteDialog = useDeleteDialog({
|
||||
onDelete: async (id) => {
|
||||
const result = await deleteBill(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('bill');
|
||||
// 서버에서 재조회 (로컬 필터링 대신 - 페이지네이션 정합성 보장)
|
||||
await loadData(currentPage);
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
entityName: '어음',
|
||||
});
|
||||
|
||||
// 날짜 범위 상태
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
@@ -337,6 +316,25 @@ export function BillManagementClient({
|
||||
totalCount: pagination.total,
|
||||
};
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteBill(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('bill');
|
||||
await loadData(currentPage);
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
},
|
||||
|
||||
// 삭제 확인 메시지
|
||||
deleteConfirmMessage: {
|
||||
title: '어음 삭제',
|
||||
description: '이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
@@ -448,6 +446,7 @@ export function BillManagementClient({
|
||||
isLoading,
|
||||
router,
|
||||
loadData,
|
||||
currentPage,
|
||||
handleSave,
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
@@ -474,14 +473,6 @@ export function BillManagementClient({
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialog.single.isOpen}
|
||||
onOpenChange={deleteDialog.single.onOpenChange}
|
||||
onConfirm={deleteDialog.single.confirm}
|
||||
title="어음 삭제"
|
||||
description="이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={deleteDialog.isPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const tableColumns = [
|
||||
{ key: 'deductionType', label: '공제', className: 'min-w-[95px]', sortable: false },
|
||||
{ key: 'businessNumber', label: '사업자번호', className: 'min-w-[110px]' },
|
||||
{ key: 'merchantName', label: '가맹점명', className: 'min-w-[100px]' },
|
||||
{ key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[130px]', sortable: false },
|
||||
{ key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[160px]', sortable: false },
|
||||
{ key: 'description', label: '내역', className: 'min-w-[120px]', sortable: false },
|
||||
{ key: 'totalAmount', label: '합계금액', className: 'min-w-[100px] text-right' },
|
||||
{ key: 'supplyAmount', label: '공급가액', className: 'min-w-[110px] text-right', sortable: false },
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
import { BusinessNumberInput } from '@/components/ui/business-number-input';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -199,12 +200,14 @@ export function ManualEntryModal({
|
||||
onChange={(value) => handleChange('vendorName', value)}
|
||||
placeholder="공급자명"
|
||||
/>
|
||||
<FormField
|
||||
label="사업자번호"
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="사업자번호"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">사업자번호</Label>
|
||||
<BusinessNumberInput
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="000-00-00000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -257,10 +257,14 @@ export function transformFrontendToApi(data: ManualEntryFormData): Record<string
|
||||
const isSales = data.division === 'sales';
|
||||
return {
|
||||
direction: isSales ? 'sales' : 'purchases',
|
||||
issue_type: 'normal',
|
||||
issue_date: data.writeDate,
|
||||
...(isSales
|
||||
? { buyer_corp_name: data.vendorName, buyer_corp_num: data.vendorBusinessNumber }
|
||||
: { supplier_corp_name: data.vendorName, supplier_corp_num: data.vendorBusinessNumber }),
|
||||
// 매출: 거래처=공급받는자(buyer), 매입: 거래처=공급자(supplier)
|
||||
// DB 컬럼이 NOT NULL이므로 빈 문자열로 전송
|
||||
supplier_corp_name: isSales ? '' : data.vendorName,
|
||||
supplier_corp_num: isSales ? '' : data.vendorBusinessNumber,
|
||||
buyer_corp_name: isSales ? data.vendorName : '',
|
||||
buyer_corp_num: isSales ? data.vendorBusinessNumber : '',
|
||||
supply_amount: data.supplyAmount,
|
||||
tax_amount: data.taxAmount,
|
||||
total_amount: data.totalAmount,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { Plus, Trash2, Upload } from 'lucide-react';
|
||||
@@ -194,6 +195,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -214,6 +216,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
@@ -129,6 +130,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteClient(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('client');
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
|
||||
Reference in New Issue
Block a user