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 */}
+
+
+
+ );
+}
diff --git a/src/components/employees/EmployeeEditModal.tsx b/src/components/employees/EmployeeEditModal.tsx
new file mode 100644
index 0000000..9e3efbb
--- /dev/null
+++ b/src/components/employees/EmployeeEditModal.tsx
@@ -0,0 +1,278 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { X, Save, AlertCircle, Trash2, Key } from 'lucide-react';
+import employeeService from '@/services/employeeService';
+import type { Employee, RoleCode, EmpStatus } from '@/types';
+
+interface EmployeeEditModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ employee: Employee | null;
+ onSuccess: () => void;
+}
+
+export default function EmployeeEditModal({
+ isOpen,
+ onClose,
+ employee,
+ onSuccess,
+}: EmployeeEditModalProps) {
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [phone, setPhone] = useState('');
+ const [position, setPosition] = useState('');
+ const [joinedDate, setJoinedDate] = useState('');
+ const [roleCode, setRoleCode] = useState('EMPLOYEE');
+ const [empStatus, setEmpStatus] = useState('ACTIVE');
+
+ const [newPassword, setNewPassword] = useState('');
+ const [showResetPassword, setShowResetPassword] = useState(false);
+
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (employee) {
+ setName(employee.name || '');
+ setEmail(employee.email || '');
+ setPhone(employee.phone || '');
+ setPosition(employee.position || '');
+ setJoinedDate(employee.joinedDate || '');
+ setRoleCode(employee.roleCode || 'EMPLOYEE');
+ setEmpStatus(employee.empStatus || 'ACTIVE');
+ setNewPassword('');
+ setShowResetPassword(false);
+ setError(null);
+ }
+ }, [employee]);
+
+ if (!isOpen || !employee) return null;
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!name.trim()) {
+ setError('이름을 입력해주세요.');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Update employee information
+ const updateData: Partial = {
+ name,
+ email,
+ phone,
+ position,
+ joinedDate,
+ roleCode,
+ empStatus,
+ };
+
+ const res = await employeeService.update(employee.empId, updateData);
+
+ // Reset password if requested
+ if (showResetPassword && newPassword.trim()) {
+ await employeeService.resetPassword(employee.empId, newPassword.trim());
+ }
+
+ if (res.success) {
+ onSuccess();
+ onClose();
+ }
+ } catch (err: any) {
+ setError(err.response?.data?.message || '직원 정보 수정 중 오류가 발생했습니다.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!confirm(`정말로 직원 '${employee.name}' 님을 삭제(퇴사) 처리하시겠습니까?`)) {
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError(null);
+ const res = await employeeService.delete(employee.empId);
+ if (res.success) {
+ onSuccess();
+ onClose();
+ }
+ } catch (err: any) {
+ setError(err.response?.data?.message || '직원 삭제에 실패했습니다.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ 직원 정보 수정 — {employee.name}
+
+
+
+
+ {/* Form Body */}
+
+
+
+ );
+}