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

@@ -12,12 +12,12 @@
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { getTodayString } from '@/lib/utils/date';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { materialInspectionCreateConfig } from './inspectionConfig';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { DatePicker } from '@/components/ui/date-picker';
import { Label } from '@/components/ui/label';
@@ -29,7 +29,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { getReceivings } from './actions';
import type { InspectionCheckItem, ReceivingItem } from './types';
import { SuccessDialog } from './SuccessDialog';
@@ -81,7 +80,7 @@ export function InspectionCreate({ id }: Props) {
const [opinion, setOpinion] = useState('');
// 유효성 검사 에러
const [validationErrors, setValidationErrors] = useState<string[]>([]);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// 성공 다이얼로그
const [showSuccess, setShowSuccess] = useState(false);
@@ -117,15 +116,22 @@ export function InspectionCreate({ id }: Props) {
// 대상 선택 핸들러
const handleTargetSelect = useCallback((targetId: string) => {
setSelectedTargetId(targetId);
setValidationErrors([]);
}, []);
// 판정 변경 핸들러
const handleJudgmentChange = useCallback((itemId: string, judgment: '적' | '부적') => {
const handleJudgmentChange = useCallback((itemId: string, index: number, judgment: '적' | '부적') => {
setInspectionItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, judgment } : item))
);
setValidationErrors([]);
// 해당 항목의 에러 클리어
setValidationErrors((prev) => {
const key = `judgment_${index}`;
if (prev[key]) {
const { [key]: _, ...rest } = prev;
return rest;
}
return prev;
});
}, []);
// 비고 변경 핸들러
@@ -137,22 +143,29 @@ export function InspectionCreate({ id }: Props) {
// 유효성 검사
const validateForm = useCallback((): boolean => {
const errors: string[] = [];
const errors: Record<string, string> = {};
// 필수 필드: 검사자
if (!inspector.trim()) {
errors.push('검사자는 필수 입력 항목입니다.');
errors.inspector = '검사자는 필수 입력 항목입니다.';
}
// 검사 항목 판정 확인
inspectionItems.forEach((item, index) => {
if (!item.judgment) {
errors.push(`${index + 1}. ${item.name}: 판정을 선택해주세요.`);
errors[`judgment_${index}`] = `${item.name}: 판정을 선택해주세요.`;
}
});
setValidationErrors(errors);
return errors.length === 0;
if (Object.keys(errors).length > 0) {
const firstError = Object.values(errors)[0];
toast.error(firstError);
return false;
}
return true;
}, [inspector, inspectionItems]);
// 검사 저장
@@ -214,30 +227,6 @@ export function InspectionCreate({ id }: Props) {
{/* 우측: 검사 정보 및 항목 */}
<div className="lg:col-span-3 space-y-6">
{/* Validation 에러 표시 */}
{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">
({validationErrors.length} )
</strong>
<ul className="space-y-1 text-sm">
{validationErrors.map((error, index) => (
<li key={index} className="flex items-start gap-1">
<span></span>
<span>{error}</span>
</li>
))}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 검사 정보 */}
<div className="space-y-4 bg-white p-4 rounded-lg border">
<h3 className="font-medium"> </h3>
@@ -257,10 +246,19 @@ export function InspectionCreate({ id }: Props) {
value={inspector}
onChange={(e) => {
setInspector(e.target.value);
setValidationErrors([]);
if (validationErrors.inspector) {
setValidationErrors((prev) => {
const { inspector: _, ...rest } = prev;
return rest;
});
}
}}
placeholder="검사자명 입력"
className={validationErrors.inspector ? 'border-red-500' : ''}
/>
{validationErrors.inspector && (
<p className="text-sm text-red-500">{validationErrors.inspector}</p>
)}
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">LOT번호 (YYMMDD-##)</Label>
@@ -284,39 +282,45 @@ export function InspectionCreate({ id }: Props) {
</tr>
</thead>
<tbody>
{inspectionItems.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">{item.name}</td>
<td className="px-3 py-2 text-muted-foreground text-xs">
{item.specification}
</td>
<td className="px-3 py-2">{item.method}</td>
<td className="px-3 py-2">
<Select
value={item.judgment || ''}
onValueChange={(value) =>
handleJudgmentChange(item.id, value as '적' | '부적')
}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="적"></SelectItem>
<SelectItem value="부적"></SelectItem>
</SelectContent>
</Select>
</td>
<td className="px-3 py-2">
<Input
value={item.remark || ''}
onChange={(e) => handleRemarkChange(item.id, e.target.value)}
placeholder="비고"
className="h-8"
/>
</td>
</tr>
))}
{inspectionItems.map((item, index) => {
const judgmentErrorKey = `judgment_${index}`;
return (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">{item.name}</td>
<td className="px-3 py-2 text-muted-foreground text-xs">
{item.specification}
</td>
<td className="px-3 py-2">{item.method}</td>
<td className="px-3 py-2">
<Select
value={item.judgment || ''}
onValueChange={(value) =>
handleJudgmentChange(item.id, index, value as '적' | '부적')
}
>
<SelectTrigger className={`h-8 ${validationErrors[judgmentErrorKey] ? 'border-red-500' : ''}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="적"></SelectItem>
<SelectItem value="부적"></SelectItem>
</SelectContent>
</Select>
{validationErrors[judgmentErrorKey] && (
<p className="text-xs text-red-500 mt-1">{validationErrors[judgmentErrorKey]}</p>
)}
</td>
<td className="px-3 py-2">
<Input
value={item.remark || ''}
onChange={(e) => handleRemarkChange(item.id, e.target.value)}
placeholder="비고"
className="h-8"
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
@@ -361,4 +365,4 @@ export function InspectionCreate({ id }: Props) {
renderForm={renderFormContent}
/>
);
}
}