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:
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
@@ -22,14 +22,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { createWorkOrder, getProcessOptions, searchItemsForWorkOrder, type ProcessOption, type ManualItemOption } from './actions';
|
||||
import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types';
|
||||
import { type SalesOrder } from './types';
|
||||
import { workOrderCreateConfig } from './workOrderConfig';
|
||||
|
||||
import { useDevFill } from '@/components/dev';
|
||||
@@ -44,20 +43,6 @@ interface ManualItem {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
// Validation 에러 타입
|
||||
interface ValidationErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
selectedOrder: '수주',
|
||||
client: '발주처',
|
||||
projectName: '현장명',
|
||||
processId: '공정',
|
||||
shipmentDate: '출고예정일',
|
||||
};
|
||||
|
||||
type RegistrationMode = 'linked' | 'manual';
|
||||
|
||||
interface FormData {
|
||||
@@ -102,7 +87,7 @@ export function WorkOrderCreate() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false);
|
||||
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
|
||||
const [isLoadingProcesses, setIsLoadingProcesses] = useState(true);
|
||||
@@ -114,6 +99,17 @@ export function WorkOrderCreate() {
|
||||
const [isSearchingItems, setIsSearchingItems] = useState(false);
|
||||
const [showItemSearch, setShowItemSearch] = useState(false);
|
||||
|
||||
// 필드 에러 클리어 헬퍼
|
||||
const clearFieldError = useCallback((field: string) => {
|
||||
if (validationErrors[field]) {
|
||||
setValidationErrors(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [validationErrors]);
|
||||
|
||||
// 공정 옵션 로드
|
||||
useEffect(() => {
|
||||
async function loadProcessOptions() {
|
||||
@@ -173,6 +169,7 @@ export function WorkOrderCreate() {
|
||||
orderNo: order.orderNo,
|
||||
itemCount: order.itemCount,
|
||||
});
|
||||
clearFieldError('selectedOrder');
|
||||
};
|
||||
|
||||
// 수주 해제
|
||||
@@ -217,6 +214,7 @@ export function WorkOrderCreate() {
|
||||
setShowItemSearch(false);
|
||||
setItemSearchQuery('');
|
||||
setItemSearchResults([]);
|
||||
clearFieldError('items');
|
||||
};
|
||||
|
||||
// 품목 수량 변경
|
||||
@@ -232,7 +230,7 @@ export function WorkOrderCreate() {
|
||||
// 폼 제출
|
||||
const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => {
|
||||
// Validation 체크
|
||||
const errors: ValidationErrors = {};
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (mode === 'linked') {
|
||||
if (!formData.selectedOrder) {
|
||||
@@ -261,8 +259,8 @@ export function WorkOrderCreate() {
|
||||
// 에러가 있으면 상태 업데이트 후 리턴
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
// 페이지 상단으로 스크롤
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const firstError = Object.values(errors)[0];
|
||||
toast.error(firstError);
|
||||
return { success: false, error: '' };
|
||||
}
|
||||
|
||||
@@ -318,35 +316,6 @@ export function WorkOrderCreate() {
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 등록 방식 */}
|
||||
<section className="bg-white border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">등록 방식</h3>
|
||||
@@ -381,7 +350,7 @@ export function WorkOrderCreate() {
|
||||
<section className="bg-muted/30 border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">수주 정보</h3>
|
||||
{!formData.selectedOrder ? (
|
||||
<div className="flex items-center justify-between p-4 bg-white border rounded-lg">
|
||||
<div className={`flex items-center justify-between p-4 bg-white border rounded-lg ${validationErrors.selectedOrder ? 'border-red-500' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
@@ -448,6 +417,7 @@ export function WorkOrderCreate() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{validationErrors.selectedOrder && <p className="text-sm text-red-500 mt-1">{validationErrors.selectedOrder}</p>}
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -459,21 +429,29 @@ export function WorkOrderCreate() {
|
||||
<Label>발주처 *</Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
onChange={(e) => setFormData({ ...formData, client: e.target.value })}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, client: e.target.value });
|
||||
clearFieldError('client');
|
||||
}}
|
||||
placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '발주처 입력'}
|
||||
disabled={mode === 'linked'}
|
||||
className="bg-white"
|
||||
className={`bg-white ${validationErrors.client ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
{validationErrors.client && <p className="text-sm text-red-500">{validationErrors.client}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>현장명 *</Label>
|
||||
<Input
|
||||
value={formData.projectName}
|
||||
onChange={(e) => setFormData({ ...formData, projectName: e.target.value })}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, projectName: e.target.value });
|
||||
clearFieldError('projectName');
|
||||
}}
|
||||
placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '현장명 입력'}
|
||||
disabled={mode === 'linked'}
|
||||
className="bg-white"
|
||||
className={`bg-white ${validationErrors.projectName ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
{validationErrors.projectName && <p className="text-sm text-red-500">{validationErrors.projectName}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주번호</Label>
|
||||
@@ -506,10 +484,13 @@ export function WorkOrderCreate() {
|
||||
<Label>공정구분 *</Label>
|
||||
<Select
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
onValueChange={(value) => {
|
||||
setFormData({ ...formData, processId: parseInt(value) });
|
||||
clearFieldError('processId');
|
||||
}}
|
||||
disabled={isLoadingProcesses}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectTrigger className={`bg-white ${validationErrors.processId ? 'border-red-500' : ''}`}>
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정을 선택하세요'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -520,6 +501,7 @@ export function WorkOrderCreate() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{validationErrors.processId && <p className="text-sm text-red-500">{validationErrors.processId}</p>}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
공정코드: {getSelectedProcessCode()}
|
||||
</p>
|
||||
@@ -529,8 +511,13 @@ export function WorkOrderCreate() {
|
||||
<Label>출고예정일 *</Label>
|
||||
<DatePicker
|
||||
value={formData.shipmentDate}
|
||||
onChange={(date) => setFormData({ ...formData, shipmentDate: date })}
|
||||
onChange={(date) => {
|
||||
setFormData({ ...formData, shipmentDate: date });
|
||||
clearFieldError('shipmentDate');
|
||||
}}
|
||||
className={validationErrors.shipmentDate ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.shipmentDate && <p className="text-sm text-red-500">{validationErrors.shipmentDate}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -717,7 +704,7 @@ export function WorkOrderCreate() {
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
), [mode, formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode, manualItems, showItemSearch, itemSearchQuery, itemSearchResults, isSearchingItems]);
|
||||
), [mode, formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode, manualItems, showItemSearch, itemSearchQuery, itemSearchResults, isSearchingItems, clearFieldError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -751,4 +738,4 @@ export function WorkOrderCreate() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user