feat(WEB): IntegratedDetailTemplate 통합 템플릿 구현 및 Phase 1~8 마이그레이션
- Phase 1: 기안함(DocumentCreate) 마이그레이션 - Phase 2: 작업지시(WorkOrderCreate/Edit) 마이그레이션 - Phase 3: 출하(ShipmentCreate/Edit) 마이그레이션 - Phase 4: 사원(EmployeeForm) 마이그레이션 - Phase 5: 게시판(BoardForm) 마이그레이션 - Phase 6: 1:1문의(InquiryForm) 마이그레이션 - Phase 7: 공정(ProcessForm) 마이그레이션 - Phase 8: 수입검사/품질검사(InspectionCreate) 마이그레이션 - DetailActions에 showSave 옵션 추가 - 각 도메인별 config 파일 생성 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
/**
|
||||
* 사원 등록/수정/상세 폼 컴포넌트
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { employeeCreateConfig, employeeEditConfig, employeeConfig } from './employeeConfig';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -17,7 +22,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera, Edit } from 'lucide-react';
|
||||
import { Plus, Trash2, Settings, Camera } from 'lucide-react';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { FieldSettingsDialog } from './FieldSettingsDialog';
|
||||
import type {
|
||||
@@ -357,16 +362,12 @@ export function EmployeeForm({
|
||||
router.push(`/${locale}/hr/employee-management`);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 + 버튼 영역 */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<PageHeader
|
||||
title={title}
|
||||
description={description}
|
||||
icon={Users}
|
||||
/>
|
||||
{!isViewMode && (
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
{/* 항목 설정 버튼 */}
|
||||
{!isViewMode && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -375,10 +376,10 @@ export function EmployeeForm({
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
항목 설정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-6">
|
||||
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -932,31 +933,7 @@ export function EmployeeForm({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
목록
|
||||
</Button>
|
||||
{isViewMode ? (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" onClick={onEdit}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" onClick={onDelete}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="submit">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{mode === 'create' ? '등록' : '저장'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 항목 설정 모달 */}
|
||||
<FieldSettingsDialog
|
||||
@@ -965,6 +942,33 @@ export function EmployeeForm({
|
||||
settings={fieldSettings}
|
||||
onSave={handleSaveFieldSettings}
|
||||
/>
|
||||
</PageLayout>
|
||||
</>
|
||||
), [
|
||||
formData, errors, isViewMode, mode, fieldSettings, showFieldSettings,
|
||||
ranks, titles, departments, handleChange, handleSaveFieldSettings,
|
||||
handleAddDepartmentPosition, handleRemoveDepartmentPosition,
|
||||
handleDepartmentSelect, handlePositionSelect, openPostcode,
|
||||
]);
|
||||
|
||||
// Config 선택 (create/edit/view)
|
||||
const getConfig = () => {
|
||||
if (mode === 'view') return employeeConfig;
|
||||
if (mode === 'edit') return employeeEditConfig;
|
||||
return employeeCreateConfig;
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={getConfig()}
|
||||
mode={mode}
|
||||
isLoading={false}
|
||||
isSubmitting={false}
|
||||
onBack={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { Users } from 'lucide-react';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
/**
|
||||
* 사원 등록 페이지 Config
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
export const employeeCreateConfig: DetailConfig = {
|
||||
title: '사원 등록',
|
||||
description: '새로운 사원을 등록합니다',
|
||||
icon: Users,
|
||||
basePath: '/hr/employee-management',
|
||||
fields: [],
|
||||
actions: {
|
||||
showBack: true,
|
||||
showEdit: false,
|
||||
showDelete: false,
|
||||
showSave: true,
|
||||
submitLabel: '등록',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 사원 수정 페이지 Config
|
||||
*/
|
||||
export const employeeEditConfig: DetailConfig = {
|
||||
...employeeCreateConfig,
|
||||
title: '사원 수정',
|
||||
description: '사원 정보를 수정합니다',
|
||||
actions: {
|
||||
...employeeCreateConfig.actions,
|
||||
submitLabel: '저장',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 사원 상세 페이지 Config
|
||||
*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format, differenceInDays } from 'date-fns';
|
||||
import { CalendarIcon, Loader2 } from 'lucide-react';
|
||||
import { differenceInDays, parseISO } from 'date-fns';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,13 +11,8 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -25,7 +20,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { VacationRequestFormData, LeaveType } from './types';
|
||||
import { LEAVE_TYPE_LABELS } from './types';
|
||||
import { getActiveEmployees, type EmployeeOption } from './actions';
|
||||
@@ -48,8 +42,6 @@ export function VacationRequestDialog({
|
||||
endDate: '',
|
||||
vacationDays: 1,
|
||||
});
|
||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
||||
const [endDate, setEndDate] = useState<Date | undefined>();
|
||||
const [employees, setEmployees] = useState<EmployeeOption[]>([]);
|
||||
const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
|
||||
|
||||
@@ -76,33 +68,31 @@ export function VacationRequestDialog({
|
||||
endDate: '',
|
||||
vacationDays: 1,
|
||||
});
|
||||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 날짜 변경 시 휴가 일수 자동 계산
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) {
|
||||
const days = differenceInDays(endDate, startDate) + 1;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
startDate: format(startDate, 'yyyy-MM-dd'),
|
||||
endDate: format(endDate, 'yyyy-MM-dd'),
|
||||
vacationDays: days > 0 ? days : 1,
|
||||
}));
|
||||
if (formData.startDate && formData.endDate) {
|
||||
const start = parseISO(formData.startDate);
|
||||
const end = parseISO(formData.endDate);
|
||||
const days = differenceInDays(end, start) + 1;
|
||||
if (days > 0 && days !== formData.vacationDays) {
|
||||
setFormData(prev => ({ ...prev, vacationDays: days }));
|
||||
}
|
||||
}
|
||||
}, [startDate, endDate]);
|
||||
}, [formData.startDate, formData.endDate, formData.vacationDays]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.employeeId) {
|
||||
alert('사원을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!startDate || !endDate) {
|
||||
if (!formData.startDate || !formData.endDate) {
|
||||
alert('휴가 기간을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (endDate < startDate) {
|
||||
if (formData.endDate < formData.startDate) {
|
||||
alert('종료일은 시작일 이후여야 합니다.');
|
||||
return;
|
||||
}
|
||||
@@ -174,61 +164,29 @@ export function VacationRequestDialog({
|
||||
|
||||
{/* 시작일 */}
|
||||
<div className="grid gap-2">
|
||||
<Label>시작일</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!startDate && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{startDate ? format(startDate, 'yyyy-MM-dd') : '시작일 선택'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={setStartDate}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Label htmlFor="startDate">시작일</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
value={formData.startDate}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종료일 */}
|
||||
<div className="grid gap-2">
|
||||
<Label>종료일</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!endDate && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{endDate ? format(endDate, 'yyyy-MM-dd') : '종료일 선택'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={setEndDate}
|
||||
disabled={(date) => startDate ? date < startDate : false}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Label htmlFor="endDate">종료일</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
type="date"
|
||||
value={formData.endDate}
|
||||
min={formData.startDate || undefined}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, endDate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 휴가 일수 (자동 계산) */}
|
||||
{startDate && endDate && (
|
||||
{formData.startDate && formData.endDate && (
|
||||
<div className="grid gap-2">
|
||||
<Label>휴가 일수</Label>
|
||||
<div className="p-3 bg-muted rounded-md text-center font-medium">
|
||||
|
||||
Reference in New Issue
Block a user