diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index ba23b2da..e39c592f 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -42,8 +42,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { Calendar } from '@/components/ui/calendar'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { DatePicker } from '@/components/ui/date-picker'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, @@ -142,7 +141,7 @@ export function ExpectedExpenseManagement({ // 예상 지급일 변경 다이얼로그 const [showDateChangeDialog, setShowDateChangeDialog] = useState(false); - const [newExpectedDate, setNewExpectedDate] = useState(undefined); + const [newExpectedDate, setNewExpectedDate] = useState(''); // 삭제 다이얼로그 const [showDeleteDialog, setShowDeleteDialog] = useState(false); @@ -166,7 +165,6 @@ export function ExpectedExpenseManagement({ paymentStatus: 'pending', note: '', }); - const [formExpectedDate, setFormExpectedDate] = useState(new Date()); // 거래처/계좌 옵션 const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([]); @@ -207,7 +205,6 @@ export function ExpectedExpenseManagement({ paymentStatus: 'pending', note: '', }); - setFormExpectedDate(new Date()); setEditingItem(null); }, []); @@ -232,7 +229,6 @@ export function ExpectedExpenseManagement({ paymentStatus: item.paymentStatus, note: item.note, }); - setFormExpectedDate(item.expectedPaymentDate ? new Date(item.expectedPaymentDate) : new Date()); setShowFormDialog(true); }, []); @@ -520,22 +516,21 @@ export function ExpectedExpenseManagement({ // ===== 예상 지급일 변경 핸들러 ===== const handleOpenDateChangeDialog = useCallback(() => { if (selectedItems.size === 0) return; - setNewExpectedDate(undefined); + setNewExpectedDate(''); setShowDateChangeDialog(true); }, [selectedItems.size]); const handleConfirmDateChange = useCallback(async () => { if (!newExpectedDate || selectedItems.size === 0) return; - const newDateStr = format(newExpectedDate, 'yyyy-MM-dd'); const selectedIds = Array.from(selectedItems); startTransition(async () => { - const result = await updateExpectedPaymentDate(selectedIds, newDateStr); + const result = await updateExpectedPaymentDate(selectedIds, newExpectedDate); if (result.success) { setData(prev => prev.map(item => selectedItems.has(item.id) - ? { ...item, expectedPaymentDate: newDateStr } + ? { ...item, expectedPaymentDate: newExpectedDate } : item )); toast.success(`${result.updatedCount || selectedIds.length}건의 예상 지급일이 변경되었습니다.`); @@ -544,7 +539,7 @@ export function ExpectedExpenseManagement({ toast.error(result.error || '예상 지급일 변경에 실패했습니다.'); } setShowDateChangeDialog(false); - setNewExpectedDate(undefined); + setNewExpectedDate(''); }); }, [newExpectedDate, selectedItems]); @@ -1063,24 +1058,11 @@ export function ExpectedExpenseManagement({ {/* 예상 지급일 선택 */}
- - - - - - - - +
@@ -1113,29 +1095,11 @@ export function ExpectedExpenseManagement({
- - - - - - { - setFormExpectedDate(date); - if (date) { - setFormData(prev => ({ ...prev, expectedPaymentDate: format(date, 'yyyy-MM-dd') })); - } - }} - /> - - + setFormData(prev => ({ ...prev, expectedPaymentDate: date }))} + className="w-full" + />
{/* 거래유형 */} diff --git a/src/components/board/BoardForm/index.tsx b/src/components/board/BoardForm/index.tsx index e9f4e580..71ae3be3 100644 --- a/src/components/board/BoardForm/index.tsx +++ b/src/components/board/BoardForm/index.tsx @@ -166,8 +166,8 @@ export function BoardForm({ mode, initialData }: BoardFormProps) { }, [boardCode, title, content]); // ===== 저장 핸들러 ===== - const handleSubmit = useCallback(async () => { - if (!validate()) return; + const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { + if (!validate()) return { success: false, error: '' }; setIsSubmitting(true); try { @@ -186,20 +186,19 @@ export function BoardForm({ mode, initialData }: BoardFormProps) { result = await updatePost(boardCode, initialData.id, postData); } - if (result?.success) { - toast.success(mode === 'create' ? '게시글이 등록되었습니다.' : '게시글이 수정되었습니다.'); - router.push('/ko/board'); - } else { - toast.error(result?.error || '게시글 저장에 실패했습니다.'); + if (!result?.success) { + return { success: false, error: result?.error || '게시글 저장에 실패했습니다.' }; } + return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('게시글 저장 오류:', error); - toast.error('게시글 저장에 실패했습니다.'); + const errorMessage = error instanceof Error ? error.message : '게시글 저장에 실패했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSubmitting(false); } - }, [boardCode, title, content, isPinned, mode, initialData, router, validate]); + }, [boardCode, title, content, isPinned, mode, initialData, validate]); // ===== 취소 핸들러 ===== const handleCancel = useCallback(() => { @@ -418,9 +417,8 @@ export function BoardForm({ mode, initialData }: BoardFormProps) { mode={mode} isLoading={isBoardsLoading} onCancel={handleCancel} - onSubmit={async (_data: Record) => { - await handleSubmit(); - return { success: true }; + onSubmit={async () => { + return await handleSubmit(); }} renderForm={renderFormContent} /> diff --git a/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx b/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx index 412d9202..527e98bb 100644 --- a/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx +++ b/src/components/hr/AttendanceManagement/AttendanceInfoDialog.tsx @@ -17,16 +17,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Calendar } from '@/components/ui/calendar'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { CalendarIcon } from 'lucide-react'; +import { DatePicker } from '@/components/ui/date-picker'; import { format } from 'date-fns'; -import { ko } from 'date-fns/locale'; -import { cn } from '@/lib/utils'; import type { AttendanceInfoDialogProps, AttendanceFormData, @@ -59,7 +51,6 @@ export function AttendanceInfoDialog({ onSave, }: AttendanceInfoDialogProps) { const [formData, setFormData] = useState(initialFormData); - const [selectedDate, setSelectedDate] = useState(new Date()); // 모드별 타이틀 const title = mode === 'create' ? '근태 정보' : '근태 정보'; @@ -82,10 +73,8 @@ export function AttendanceInfoDialog({ weekendOvertimeHours: '0', weekendOvertimeMinutes: '0', }); - setSelectedDate(new Date(attendance.baseDate)); } else if (open && mode === 'create') { setFormData(initialFormData); - setSelectedDate(new Date()); } }, [open, attendance, mode]); @@ -94,14 +83,6 @@ export function AttendanceInfoDialog({ setFormData(prev => ({ ...prev, [field]: value })); }; - // 날짜 변경 핸들러 - const handleDateChange = (date: Date | undefined) => { - setSelectedDate(date); - if (date) { - setFormData(prev => ({ ...prev, baseDate: format(date, 'yyyy-MM-dd') })); - } - }; - // 저장 const handleSubmit = () => { onSave(formData); @@ -142,29 +123,12 @@ export function AttendanceInfoDialog({ {/* 기준일 */}
- - - - - - - - + handleChange('baseDate', date)} + className="w-[200px]" + align="end" + />
{/* 출근 시간 */} diff --git a/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx b/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx index b67de2a8..3d1dfed5 100644 --- a/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx +++ b/src/components/hr/AttendanceManagement/ReasonInfoDialog.tsx @@ -17,16 +17,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Calendar } from '@/components/ui/calendar'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { CalendarIcon } from 'lucide-react'; +import { DatePicker } from '@/components/ui/date-picker'; import { format } from 'date-fns'; -import { ko } from 'date-fns/locale'; -import { cn } from '@/lib/utils'; import type { ReasonInfoDialogProps, ReasonFormData, @@ -47,13 +39,11 @@ export function ReasonInfoDialog({ onSubmit, }: ReasonInfoDialogProps) { const [formData, setFormData] = useState(initialFormData); - const [selectedDate, setSelectedDate] = useState(new Date()); // 데이터 초기화 useEffect(() => { if (open) { setFormData(initialFormData); - setSelectedDate(new Date()); } }, [open]); @@ -62,14 +52,6 @@ export function ReasonInfoDialog({ setFormData(prev => ({ ...prev, [field]: value })); }; - // 날짜 변경 핸들러 - const handleDateChange = (date: Date | undefined) => { - setSelectedDate(date); - if (date) { - setFormData(prev => ({ ...prev, baseDate: format(date, 'yyyy-MM-dd') })); - } - }; - // 등록 (문서 작성 화면으로 이동) const handleSubmit = () => { onSubmit(formData); @@ -107,29 +89,12 @@ export function ReasonInfoDialog({ {/* 기준일 */}
- - - - - - - - + handleChange('baseDate', date)} + className="w-[200px]" + align="end" + />
{/* 유형 선택 */} diff --git a/src/components/material/ReceivingManagement/ReceivingDetail.tsx b/src/components/material/ReceivingManagement/ReceivingDetail.tsx index 2f81b709..e57782a9 100644 --- a/src/components/material/ReceivingManagement/ReceivingDetail.tsx +++ b/src/components/material/ReceivingManagement/ReceivingDetail.tsx @@ -227,31 +227,41 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { })); }; - // 저장 핸들러 - const handleSave = async () => { + // 저장 핸들러 - IntegratedDetailTemplate의 onSubmit에서 호출 + // 반환값으로 성공/실패를 전달하여 템플릿이 toast/navigation 처리 + const handleSave = async (): Promise<{ success: boolean; error?: string }> => { + // 클라이언트 사이드 필수 필드 검증 + const errors: string[] = []; + if (!formData.itemCode) errors.push('품목코드'); + if (!formData.supplier) errors.push('발주처'); + if (!formData.receivingQty) errors.push('입고수량'); + if (!formData.receivingDate) errors.push('입고일'); + + if (errors.length > 0) { + return { success: false, error: `필수 항목을 입력해주세요: ${errors.join(', ')}` }; + } + setIsSaving(true); try { if (isNewMode) { const result = await createReceiving(formData); - if (result.success) { - toast.success('입고가 등록되었습니다.'); - router.push('/ko/material/receiving-management'); - } else { - toast.error(result.error || '등록에 실패했습니다.'); + if (!result.success) { + return { success: false, error: result.error || '등록에 실패했습니다.' }; } + return { success: true }; } else if (isEditMode) { const result = await updateReceiving(id, formData); - if (result.success) { - toast.success('입고 정보가 수정되었습니다.'); - router.push(`/ko/material/receiving-management/${id}?mode=view`); - } else { - toast.error(result.error || '수정에 실패했습니다.'); + if (!result.success) { + return { success: false, error: result.error || '수정에 실패했습니다.' }; } + return { success: true }; } + return { success: false, error: '알 수 없는 모드입니다.' }; } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ReceivingDetail] handleSave error:', err); - toast.error('저장 중 오류가 발생했습니다.'); + const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSaving(false); } @@ -754,8 +764,7 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) { renderView={() => renderViewContent()} renderForm={() => renderFormContent()} onSubmit={async () => { - await handleSave(); - return { success: true }; + return await handleSave(); }} onCancel={handleCancel} /> diff --git a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx index 48f679ea..de59c71c 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx @@ -296,25 +296,25 @@ export function ShipmentCreate() { return errors.length === 0; }; - const handleSubmit = useCallback(async () => { - if (!validateForm()) return; + const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { + if (!validateForm()) return { success: false, error: '' }; setIsSubmitting(true); try { const result = await createShipment(formData); - if (result.success) { - router.push('/ko/outbound/shipments'); - } else { - setValidationErrors([result.error || '출고 등록에 실패했습니다.']); + if (!result.success) { + return { success: false, error: result.error || '출고 등록에 실패했습니다.' }; } + return { success: true }; } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentCreate] handleSubmit error:', err); - setValidationErrors(['저장 중 오류가 발생했습니다.']); + const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSubmitting(false); } - }, [formData, router]); + }, [formData]); // 제품 부품 테이블 렌더링 const renderPartsTable = (parts: ProductPart[]) => ( @@ -762,9 +762,8 @@ export function ShipmentCreate() { mode="create" isLoading={isLoading} onCancel={handleCancel} - onSubmit={async (_data: Record) => { - await handleSubmit(); - return { success: true }; + onSubmit={async () => { + return await handleSubmit(); }} renderForm={renderFormContent} /> diff --git a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx index 950b40bd..27c7e806 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx @@ -295,25 +295,25 @@ export function ShipmentEdit({ id }: ShipmentEditProps) { return errors.length === 0; }; - const handleSubmit = useCallback(async () => { - if (!validateForm()) return; + const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { + if (!validateForm()) return { success: false, error: '' }; setIsSubmitting(true); try { const result = await updateShipment(id, formData); - if (result.success) { - router.push(`/ko/outbound/shipments/${id}?mode=view`); - } else { - setValidationErrors([result.error || '출고 수정에 실패했습니다.']); + if (!result.success) { + return { success: false, error: result.error || '출고 수정에 실패했습니다.' }; } + return { success: true }; } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentEdit] handleSubmit error:', err); - setValidationErrors(['저장 중 오류가 발생했습니다.']); + const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSubmitting(false); } - }, [id, formData, router]); + }, [id, formData]); // 제품 부품 테이블 렌더링 const renderPartsTable = (parts: ProductPart[]) => ( @@ -781,9 +781,8 @@ export function ShipmentEdit({ id }: ShipmentEditProps) { mode="edit" isLoading={isLoading} onCancel={handleCancel} - onSubmit={async (_data: Record) => { - await handleSubmit(); - return { success: true }; + onSubmit={async () => { + return await handleSubmit(); }} renderForm={renderFormContent} /> diff --git a/src/components/outbound/VehicleDispatchManagement/VehicleDispatchEdit.tsx b/src/components/outbound/VehicleDispatchManagement/VehicleDispatchEdit.tsx index 5e139c71..88520fb6 100644 --- a/src/components/outbound/VehicleDispatchManagement/VehicleDispatchEdit.tsx +++ b/src/components/outbound/VehicleDispatchManagement/VehicleDispatchEdit.tsx @@ -21,6 +21,7 @@ import { } from '@/components/ui/select'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { vehicleDispatchEditConfig } from './vehicleDispatchConfig'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getVehicleDispatchById, updateVehicleDispatch } from './actions'; import { VEHICLE_DISPATCH_STATUS_LABELS, @@ -135,22 +136,23 @@ export function VehicleDispatchEdit({ id }: VehicleDispatchEditProps) { router.push(`/ko/outbound/vehicle-dispatches/${id}?mode=view`); }, [router, id]); - const handleSubmit = useCallback(async () => { + const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { setIsSubmitting(true); try { const result = await updateVehicleDispatch(id, formData); - if (result.success) { - router.push(`/ko/outbound/vehicle-dispatches/${id}?mode=view`); - } else { - setValidationErrors([result.error || '배차차량 수정에 실패했습니다.']); + if (!result.success) { + return { success: false, error: result.error || '배차차량 수정에 실패했습니다.' }; } + return { success: true }; } catch (err) { + if (isNextRedirectError(err)) throw err; console.error('[VehicleDispatchEdit] handleSubmit error:', err); - setValidationErrors(['저장 중 오류가 발생했습니다.']); + const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSubmitting(false); } - }, [id, formData, router]); + }, [id, formData]); // 동적 config const dynamicConfig = { @@ -387,9 +389,8 @@ export function VehicleDispatchEdit({ id }: VehicleDispatchEditProps) { mode="edit" isLoading={isLoading} onCancel={handleCancel} - onSubmit={async (_data: Record) => { - await handleSubmit(); - return { success: true }; + onSubmit={async () => { + return await handleSubmit(); }} renderForm={renderFormContent} /> diff --git a/src/components/production/WorkOrders/WorkOrderCreate.tsx b/src/components/production/WorkOrders/WorkOrderCreate.tsx index 1af4ea99..48db5fba 100644 --- a/src/components/production/WorkOrders/WorkOrderCreate.tsx +++ b/src/components/production/WorkOrders/WorkOrderCreate.tsx @@ -170,7 +170,7 @@ export function WorkOrderCreate() { }; // 폼 제출 - const handleSubmit = async () => { + const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { // Validation 체크 const errors: ValidationErrors = {}; @@ -200,7 +200,7 @@ export function WorkOrderCreate() { setValidationErrors(errors); // 페이지 상단으로 스크롤 window.scrollTo({ top: 0, behavior: 'smooth' }); - return; + return { success: false, error: '' }; } // 에러 초기화 @@ -218,16 +218,15 @@ export function WorkOrderCreate() { note: formData.note || undefined, }); - if (result.success) { - toast.success('작업지시가 등록되었습니다.'); - router.push('/production/work-orders'); - } else { - toast.error(result.error || '작업지시 등록에 실패했습니다.'); + if (!result.success) { + return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' }; } + return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkOrderCreate] handleSubmit error:', error); - toast.error('작업지시 등록 중 오류가 발생했습니다.'); + const errorMessage = error instanceof Error ? error.message : '작업지시 등록 중 오류가 발생했습니다.'; + return { success: false, error: errorMessage }; } finally { setIsSubmitting(false); } @@ -518,9 +517,8 @@ export function WorkOrderCreate() { mode="create" isLoading={isLoadingProcesses} onCancel={handleCancel} - onSubmit={async (_data: Record) => { - await handleSubmit(); - return { success: true }; + onSubmit={async () => { + return await handleSubmit(); }} renderForm={renderFormContent} />