From 642402c94811b0ed907ae9dae0a9e3e582d9abcc Mon Sep 17 00:00:00 2001 From: pray4skylark Date: Wed, 30 Sep 2026 16:36:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EC=A7=81=EC=9B=90=20=EC=88=98=EC=A0=95/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=AA=A8=EB=8B=AC=20=EB=B0=8F=20=EC=8B=A0=EA=B7=9C?= =?UTF-8?q?=20=ED=8C=8C=EA=B2=AC=20=EB=93=B1=EB=A1=9D/=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/dispatch/page.tsx | 23 +- src/app/(main)/employees/page.tsx | 48 ++- .../dispatch/CreateDispatchModal.tsx | 280 ++++++++++++++++++ .../employees/EmployeeEditModal.tsx | 278 +++++++++++++++++ 4 files changed, 624 insertions(+), 5 deletions(-) create mode 100644 src/components/dispatch/CreateDispatchModal.tsx create mode 100644 src/components/employees/EmployeeEditModal.tsx diff --git a/src/app/(main)/dispatch/page.tsx b/src/app/(main)/dispatch/page.tsx index c4207be..b1ee39b 100644 --- a/src/app/(main)/dispatch/page.tsx +++ b/src/app/(main)/dispatch/page.tsx @@ -10,6 +10,7 @@ import { useAuthStore } from '@/stores'; import employeeService, { type PortfolioEntry, type CreateDispatchRequest } from '@/services/employeeService'; import projectService from '@/services/projectService'; import type { EmployeeSearchResult, Project } from '@/types'; +import CreateDispatchModal from '@/components/dispatch/CreateDispatchModal'; function initialColor(name: string) { const colors = [ @@ -275,7 +276,27 @@ export default function DispatchPage() { 공수 {entry.contributionRate}%

- {entry.contributionRate}% +
+ {entry.contributionRate}% + {(user?.roleCode === 'MANAGER' || user?.roleCode === 'ADMIN') && !entry.endDate && ( + + )} +
diff --git a/src/app/(main)/employees/page.tsx b/src/app/(main)/employees/page.tsx index e2b690f..57119ad 100644 --- a/src/app/(main)/employees/page.tsx +++ b/src/app/(main)/employees/page.tsx @@ -8,6 +8,7 @@ import apiClient from '@/services/apiClient'; import { useCommonCodes } from '@/hooks/useCommonCodes'; import { useAuthStore } from '@/stores'; import type { Employee, RoleCode, EmpStatus, DispatchStatus } from '@/types'; +import EmployeeEditModal from '@/components/employees/EmployeeEditModal'; const statusColor: Record = { ACTIVE: 'bg-status-resolved-bg text-status-resolved-text', @@ -36,6 +37,7 @@ export default function EmployeesPage() { // Detail Modal State const [selectedEmployee, setSelectedEmployee] = useState(null); + const [editingEmployee, setEditingEmployee] = useState(null); const [detailDispatch, setDetailDispatch] = useState([]); const [detailEquipment, setDetailEquipment] = useState([]); const [detailLoading, setDetailLoading] = useState(false); @@ -580,7 +582,21 @@ export default function EmployeesPage() { {emp.email} - View details → +
+ 상세 → + {(user?.roleCode === 'ADMIN' || user?.roleCode === 'MANAGER') && ( + + )} +
))} @@ -606,9 +622,23 @@ export default function EmployeesPage() {

{roleLabel[emp.roleCode] || emp.roleCode}

- - {emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'} - +
+ + {emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'} + + {(user?.roleCode === 'ADMIN' || user?.roleCode === 'MANAGER') && ( + + )} +
Email: {emp.email}
@@ -620,6 +650,16 @@ export default function EmployeesPage() {
+ + {/* Employee Edit Modal */} + setEditingEmployee(null)} + employee={editingEmployee} + onSuccess={() => { + fetchEmployees(); + }} + /> ); } diff --git a/src/components/dispatch/CreateDispatchModal.tsx b/src/components/dispatch/CreateDispatchModal.tsx new file mode 100644 index 0000000..ebe9c2a --- /dev/null +++ b/src/components/dispatch/CreateDispatchModal.tsx @@ -0,0 +1,280 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { X, Save, AlertCircle, Search } from 'lucide-react'; +import employeeService from '@/services/employeeService'; +import projectService from '@/services/projectService'; +import type { Employee, Project, CreateDispatchRequest } from '@/types'; + +interface CreateDispatchModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +export default function CreateDispatchModal({ + isOpen, + onClose, + onSuccess, +}: CreateDispatchModalProps) { + const [employees, setEmployees] = useState([]); + const [projects, setProjects] = useState([]); + + const [employeeId, setEmployeeId] = useState(''); + const [projectId, setProjectId] = useState(''); + const [roleTitle, setRoleTitle] = useState(''); + const [contributionRate, setContributionRate] = useState(100); + const [individualTech, setIndividualTech] = useState(''); + const [performedTasks, setPerformedTasks] = useState(''); + const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]); + + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (isOpen) { + loadData(); + } + }, [isOpen]); + + const loadData = async () => { + try { + setFetching(true); + setError(null); + const [empRes, projRes] = await Promise.all([ + employeeService.getAll({ size: 200 }), + projectService.getAll(), + ]); + + if (empRes.success && empRes.data) { + setEmployees(empRes.data.content || []); + } + if (projRes.success && projRes.data) { + setProjects(projRes.data || []); + } + } catch (err) { + console.error('Failed to load dispatch form options', err); + setError('직원 및 프로젝트 목록을 불러오지 못했습니다.'); + } finally { + setFetching(false); + } + }; + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!employeeId) { + setError('파견할 직원을 선택해 주세요.'); + return; + } + if (!projectId) { + setError('파견 프로젝트를 선택해 주세요.'); + return; + } + if (!roleTitle.trim()) { + setError('담당 역할을 입력해 주세요.'); + return; + } + + try { + setLoading(true); + setError(null); + + const request: CreateDispatchRequest = { + employeeId: Number(employeeId), + projectId: Number(projectId), + roleTitle: roleTitle.trim(), + contributionRate: Number(contributionRate) || 100, + individualTech: individualTech.trim(), + performedTasks: performedTasks.trim(), + startDate, + }; + + const res = await employeeService.createDispatch(request); + if (res.success) { + onSuccess(); + onClose(); + // Reset form + setEmployeeId(''); + setProjectId(''); + setRoleTitle(''); + setContributionRate(100); + setIndividualTech(''); + setPerformedTasks(''); + } + } catch (err: any) { + setError(err.response?.data?.message || '파견 등록 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ 신규 파견(투입) 등록 +

+ +
+ + {/* Body Form */} +
+ {error && ( +
+ + {error} +
+ )} + + {fetching ? ( +
목록 로딩 중...
+ ) : ( + <> + {/* Employee Selection */} +
+ + +
+ + {/* Project Selection */} +
+ + +
+ + {/* Role Title & Contribution Rate */} +
+
+ + setRoleTitle(e.target.value)} + placeholder="예: 백엔드 개발, PL, QA" + className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" + required + /> +
+
+ + setContributionRate(Number(e.target.value))} + className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" + required + /> +
+
+ + {/* Start Date */} +
+ + setStartDate(e.target.value)} + className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" + required + /> +
+ + {/* Tech Stack */} +
+ + setIndividualTech(e.target.value)} + placeholder="예: Java 17, Spring Boot, React, PostgreSQL" + className="w-full px-3 py-2 border border-border rounded-xl bg-surface-container-lowest text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary" + /> +
+ + {/* Performed Tasks */} +
+ +