feat: 레이아웃/출하/생산/회계/대시보드 전반 개선

- HeaderFavoritesBar 대폭 개선
- Sidebar/AuthenticatedLayout 소폭 수정
- ShipmentCreate, VehicleDispatch 출하 관련 개선
- WorkOrderCreate/Edit, WorkerScreen 생산 관련 개선
- InspectionCreate 자재 입고검사 개선
- DailyReport, VendorDetail 회계 수정
- CEO 대시보드: CardManagement/DailyProduction/DailyAttendance 섹션 개선
- useCEODashboard, expense transformer 정비
- DocumentViewer, PDF generate route 소폭 수정
- bill-prototype 개발 페이지 추가
- mockData 불필요 데이터 제거
This commit is contained in:
유병철
2026-03-05 13:35:48 +09:00
parent c18c68b6b7
commit 00a6209347
23 changed files with 1689 additions and 517 deletions

View File

@@ -20,6 +20,7 @@ import { Input } from '@/components/ui/input';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { formatNumber as formatAmount } from '@/lib/utils/amount';
import { printElement } from '@/lib/print-utils';
import type { NoteReceivableItem, DailyAccountItem } from './types';
import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions';
import { toast } from 'sonner';
@@ -204,9 +205,19 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
}, []);
// ===== 인쇄 =====
const printAreaRef = useRef<HTMLDivElement>(null);
const handlePrint = useCallback(() => {
window.print();
}, []);
if (printAreaRef.current) {
printElement(printAreaRef.current, {
title: `일일일보_${startDate}`,
styles: `
.print-container { font-size: 11px; }
table { width: 100%; margin-bottom: 12px; }
h3 { margin-bottom: 8px; }
`,
});
}
}, [startDate]);
// ===== USD 금액 포맷 =====
const formatUsd = useCallback((value: number) => `$ ${formatAmount(value)}`, []);
@@ -299,6 +310,8 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
</CardContent>
</Card>
{/* 인쇄 영역 */}
<div ref={printAreaRef} className="print-area space-y-4 md:space-y-6">
{/* 일자별 입출금 합계 */}
<Card>
<CardContent className="px-3 pt-4 pb-3 md:px-6 md:pt-6 md:pb-6">
@@ -660,6 +673,7 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
</div>
</CardContent>
</Card>
</div>
</PageLayout>
);
}

View File

@@ -10,12 +10,6 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai
import { vendorConfig } from './vendorConfig';
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
// 필드명 매핑
const FIELD_NAME_MAP: Record<string, string> = {
businessNumber: '사업자등록번호',
vendorName: '거래처명',
category: '거래처 유형',
};
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -29,7 +23,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
// 새 입력 컴포넌트
import { PhoneInput } from '@/components/ui/phone-input';
import { BusinessNumberInput } from '@/components/ui/business-number-input';
@@ -186,13 +179,25 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
}
setValidationErrors(errors);
if (Object.keys(errors).length > 0) {
const firstError = Object.values(errors)[0];
toast.error(firstError);
}
return Object.keys(errors).length === 0;
}, [formData.businessNumber, formData.vendorName, formData.category]);
// 필드 변경 핸들러
const handleChange = useCallback((field: string, value: string | number | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
}, []);
// 에러 클리어
if (validationErrors[field]) {
setValidationErrors(prev => {
const next = { ...prev };
delete next[field];
return next;
});
}
}, [validationErrors]);
// 파일 검증 및 추가
const validateAndAddFiles = useCallback((files: FileList | File[]) => {
@@ -265,7 +270,6 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
// 저장 핸들러 (IntegratedDetailTemplate용)
const handleSubmit = useCallback(async () => {
if (!validateForm()) {
window.scrollTo({ top: 0, behavior: 'smooth' });
return { success: false, error: '입력 내용을 확인해주세요.' };
}
@@ -326,8 +330,9 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
onChange={(e) => handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)}
placeholder={placeholder}
disabled={isViewMode || disabled}
className="bg-white"
className={`bg-white ${validationErrors[field] ? 'border-red-500' : ''}`}
/>
{validationErrors[field] && <p className="text-sm text-red-500">{validationErrors[field]}</p>}
</div>
);
};
@@ -350,7 +355,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
onValueChange={(val) => handleChange(field, val)}
disabled={isViewMode}
>
<SelectTrigger className="bg-white">
<SelectTrigger className={`bg-white ${validationErrors[field] ? 'border-red-500' : ''}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
@@ -361,6 +366,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
))}
</SelectContent>
</Select>
{validationErrors[field] && <p className="text-sm text-red-500">{validationErrors[field]}</p>}
</div>
);
};
@@ -368,35 +374,6 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
// 폼 콘텐츠 렌더링 (View/Edit 공통)
const renderFormContent = () => (
<div className="space-y-6">
{/* Validation 에러 표시 */}
{Object.keys(validationErrors).length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({Object.keys(validationErrors).length} )
</strong>
<ul className="space-y-1 text-sm">
{Object.entries(validationErrors).map(([field, message]) => {
const fieldName = FIELD_NAME_MAP[field] || field;
return (
<li key={field} className="flex items-start gap-1">
<span></span>
<span>
<strong>{fieldName}</strong>: {message}
</span>
</li>
);
})}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 기본 정보 */}
<Card>
<CardHeader>
@@ -496,6 +473,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
showValidation={!isViewMode}
error={!!validationErrors.businessNumber}
/>
{validationErrors.businessNumber && <p className="text-sm text-red-500">{validationErrors.businessNumber}</p>}
</div>
{renderField('거래처코드', 'vendorCode', formData.vendorCode, { placeholder: '자동생성', disabled: true })}
{renderField('거래처명', 'vendorName', formData.vendorName, { required: true })}