775d698d06
- Introduced multiple new pages to enhance design management functionalities, allowing users to manage design requests, track their work, and oversee project and task statuses. - Implemented UI components such as tables, dialogs, and forms to facilitate user interactions and data management. - Integrated necessary API calls for fetching and manipulating design-related data, ensuring a seamless user experience across the new pages. These additions significantly expand the design management capabilities of the application, providing users with comprehensive tools for managing their design workflows.
543 lines
24 KiB
TypeScript
543 lines
24 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 부서관리 — 하드코딩 페이지
|
|
*
|
|
* 좌측: 부서 목록 (dept_info)
|
|
* 우측: 선택한 부서의 인원 목록 (user_info)
|
|
*
|
|
* 모달: 부서 등록(dept_info), 사원 추가(user_info)
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
import {
|
|
Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil,
|
|
Building2, Users, Settings2,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import * as departmentAPI from "@/lib/api/department";
|
|
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { toast } from "sonner";
|
|
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
|
import { TableSettingsModal, TableSettings, loadTableSettings } from "@/components/common/TableSettingsModal";
|
|
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
|
|
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
|
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal";
|
|
import { exportToExcel } from "@/lib/utils/excelExport";
|
|
import { FormDatePicker } from "@/components/screen/filters/FormDatePicker";
|
|
import { formatField, validateField, validateForm } from "@/lib/utils/validation";
|
|
|
|
const DEPT_TABLE = "dept_info";
|
|
const USER_TABLE = "user_info";
|
|
|
|
const LEFT_COLUMNS: DataGridColumn[] = [
|
|
{ key: "dept_code", label: "부서코드", width: "w-[120px]" },
|
|
{ key: "dept_name", label: "부서명", minWidth: "min-w-[150px]" },
|
|
{ key: "parent_dept_code", label: "상위부서", width: "w-[100px]" },
|
|
{ key: "status", label: "상태", width: "w-[70px]" },
|
|
];
|
|
|
|
const RIGHT_COLUMNS: DataGridColumn[] = [
|
|
{ key: "sabun", label: "사번", width: "w-[80px]" },
|
|
{ key: "user_name", label: "이름", width: "w-[90px]" },
|
|
{ key: "user_id", label: "사용자ID", width: "w-[100px]" },
|
|
{ key: "position_name", label: "직급", width: "w-[80px]" },
|
|
{ key: "cell_phone", label: "휴대폰", width: "w-[120px]" },
|
|
{ key: "email", label: "이메일", minWidth: "min-w-[150px]" },
|
|
];
|
|
|
|
export default function DepartmentPage() {
|
|
const { user } = useAuth();
|
|
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
|
|
|
|
// 좌측: 부서
|
|
const [depts, setDepts] = useState<any[]>([]);
|
|
const [deptLoading, setDeptLoading] = useState(false);
|
|
const [deptCount, setDeptCount] = useState(0);
|
|
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
|
const [selectedDeptId, setSelectedDeptId] = useState<string | null>(null);
|
|
const [tableSettingsOpen, setTableSettingsOpen] = useState(false);
|
|
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"] | undefined>();
|
|
|
|
// 우측: 사원
|
|
const [members, setMembers] = useState<any[]>([]);
|
|
const [memberLoading, setMemberLoading] = useState(false);
|
|
|
|
// 부서 모달
|
|
const [deptModalOpen, setDeptModalOpen] = useState(false);
|
|
const [deptEditMode, setDeptEditMode] = useState(false);
|
|
const [deptForm, setDeptForm] = useState<Record<string, any>>({});
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// 채번 시스템
|
|
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
|
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
|
|
|
// 사원 모달
|
|
const [userModalOpen, setUserModalOpen] = useState(false);
|
|
const [userEditMode, setUserEditMode] = useState(false);
|
|
const [userForm, setUserForm] = useState<Record<string, any>>({});
|
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
|
|
|
// 엑셀
|
|
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
|
|
|
|
const applyTableSettings = useCallback((settings: TableSettings) => {
|
|
setFilterConfig(settings.filters);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const saved = loadTableSettings("department");
|
|
if (saved) applyTableSettings(saved);
|
|
}, []);
|
|
|
|
// 부서 조회
|
|
const fetchDepts = useCallback(async () => {
|
|
setDeptLoading(true);
|
|
try {
|
|
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
|
|
const res = await apiClient.post(`/table-management/tables/${DEPT_TABLE}/data`, {
|
|
page: 1, size: 500,
|
|
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
|
// dept_info에 id 컬럼이 없으므로 dept_code를 id로 매핑
|
|
const data = raw.map((d: any) => ({ ...d, id: d.id || d.dept_code }));
|
|
setDepts(data);
|
|
setDeptCount(res.data?.data?.total || data.length);
|
|
} catch (err) {
|
|
toast.error("부서 목록을 불러오는데 실패했습니다.");
|
|
} finally {
|
|
setDeptLoading(false);
|
|
}
|
|
}, [searchFilters]);
|
|
|
|
useEffect(() => { fetchDepts(); }, [fetchDepts]);
|
|
|
|
// 선택된 부서
|
|
const selectedDept = depts.find((d) => d.id === selectedDeptId);
|
|
const selectedDeptCode = selectedDept?.dept_code || null;
|
|
|
|
// 우측: 사원 조회 (부서 미선택 → 전체, 선택 → 해당 부서)
|
|
const fetchMembers = useCallback(async () => {
|
|
setMemberLoading(true);
|
|
try {
|
|
const filters = selectedDeptCode
|
|
? [{ columnName: "dept_code", operator: "equals", value: selectedDeptCode }]
|
|
: [];
|
|
const res = await apiClient.post(`/table-management/tables/${USER_TABLE}/data`, {
|
|
page: 1, size: 500,
|
|
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
setMembers(res.data?.data?.data || res.data?.data?.rows || []);
|
|
} catch { setMembers([]); } finally { setMemberLoading(false); }
|
|
}, [selectedDeptCode]);
|
|
|
|
useEffect(() => { fetchMembers(); }, [fetchMembers]);
|
|
|
|
// 부서 등록
|
|
const openDeptRegister = async () => {
|
|
setDeptForm({});
|
|
setDeptEditMode(false);
|
|
setPreviewCode(null);
|
|
setNumberingRuleId(null);
|
|
setDeptModalOpen(true);
|
|
|
|
// 채번 규칙 조회 (dept_info.dept_code) — path params로 직접 호출
|
|
try {
|
|
const ruleRes = await apiClient.get(`/numbering-rules/by-column/dept_info/dept_code`);
|
|
const ruleData = ruleRes.data;
|
|
if (ruleData?.success && ruleData?.data?.ruleId) {
|
|
const ruleId = ruleData.data.ruleId;
|
|
setNumberingRuleId(ruleId);
|
|
const previewRes = await previewNumberingCode(ruleId);
|
|
if (previewRes.success && previewRes.data?.generatedCode) {
|
|
setPreviewCode(previewRes.data.generatedCode);
|
|
}
|
|
}
|
|
} catch {
|
|
// 채번 규칙 없으면 무시
|
|
}
|
|
};
|
|
|
|
const openDeptEdit = () => {
|
|
if (!selectedDept) return;
|
|
setDeptForm({ ...selectedDept });
|
|
setDeptEditMode(true);
|
|
setDeptModalOpen(true);
|
|
};
|
|
|
|
const handleDeptSave = async () => {
|
|
if (!deptForm.dept_name) { toast.error("부서명은 필수입니다."); return; }
|
|
const parentCode = (deptForm.parent_dept_code && deptForm.parent_dept_code !== "none") ? deptForm.parent_dept_code : null;
|
|
setSaving(true);
|
|
try {
|
|
if (deptEditMode && deptForm.dept_code) {
|
|
const response = await departmentAPI.updateDepartment(deptForm.dept_code, {
|
|
dept_name: deptForm.dept_name,
|
|
parent_dept_code: parentCode,
|
|
});
|
|
if (!response.success) { toast.error((response as any).error || "수정에 실패했습니다."); return; }
|
|
toast.success("수정되었습니다.");
|
|
} else {
|
|
const companyCode = user?.companyCode || "";
|
|
|
|
// 채번 규칙이 있으면 allocate로 실제 코드 할당
|
|
let allocatedCode: string | undefined;
|
|
if (numberingRuleId) {
|
|
const allocRes = await allocateNumberingCode(numberingRuleId);
|
|
if (allocRes.success && allocRes.data?.generatedCode) {
|
|
allocatedCode = allocRes.data.generatedCode;
|
|
} else {
|
|
toast.error("채번 코드 할당에 실패했습니다.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const response = await departmentAPI.createDepartment(companyCode, {
|
|
dept_name: deptForm.dept_name,
|
|
parent_dept_code: parentCode,
|
|
dept_code: allocatedCode,
|
|
});
|
|
if (!response.success) {
|
|
toast.error((response as any).error || "등록에 실패했습니다.");
|
|
return;
|
|
}
|
|
toast.success("등록되었습니다.");
|
|
}
|
|
setDeptModalOpen(false);
|
|
fetchDepts();
|
|
} catch (err: any) {
|
|
toast.error(err.response?.data?.message || "저장에 실패했습니다.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 부서 삭제
|
|
const handleDeptDelete = async () => {
|
|
if (!selectedDeptCode) return;
|
|
const ok = await confirm("부서를 삭제하시겠습니까?", {
|
|
description: "해당 부서에 소속된 사원 정보는 유지됩니다.",
|
|
variant: "destructive", confirmText: "삭제",
|
|
});
|
|
if (!ok) return;
|
|
try {
|
|
const response = await departmentAPI.deleteDepartment(selectedDeptCode);
|
|
if (!response.success) { toast.error((response as any).error || "삭제에 실패했습니다."); return; }
|
|
toast.success(response.message || "삭제되었습니다.");
|
|
setSelectedDeptId(null);
|
|
fetchDepts();
|
|
} catch { toast.error("삭제에 실패했습니다."); }
|
|
};
|
|
|
|
// 사원 추가
|
|
const openUserModal = (editData?: any) => {
|
|
if (editData) {
|
|
setUserEditMode(true);
|
|
setUserForm({ ...editData, user_password: "" });
|
|
} else {
|
|
setUserEditMode(false);
|
|
setUserForm({ dept_code: selectedDeptCode || "", user_password: "" });
|
|
}
|
|
setFormErrors({});
|
|
setUserModalOpen(true);
|
|
};
|
|
|
|
const handleUserFormChange = (field: string, value: string) => {
|
|
const formatted = formatField(field, value);
|
|
setUserForm((prev) => ({ ...prev, [field]: formatted }));
|
|
const error = validateField(field, formatted);
|
|
setFormErrors((prev) => { const n = { ...prev }; if (error) n[field] = error; else delete n[field]; return n; });
|
|
};
|
|
|
|
const handleUserSave = async () => {
|
|
if (!userForm.user_id) { toast.error("사용자 ID는 필수입니다."); return; }
|
|
if (!userForm.user_name) { toast.error("사용자 이름은 필수입니다."); return; }
|
|
if (!userForm.dept_code) { toast.error("부서는 필수입니다."); return; }
|
|
const errors = validateForm(userForm, ["cell_phone", "email"]);
|
|
setFormErrors(errors);
|
|
if (Object.keys(errors).length > 0) { toast.error("입력 형식을 확인해주세요."); return; }
|
|
|
|
setSaving(true);
|
|
try {
|
|
// 비밀번호 미입력 시 기본값 (신규만)
|
|
const password = userForm.user_password || (!userEditMode ? "qlalfqjsgh11" : undefined);
|
|
|
|
await apiClient.post("/admin/users/with-dept", {
|
|
userInfo: {
|
|
user_id: userForm.user_id,
|
|
user_name: userForm.user_name,
|
|
user_name_eng: userForm.user_name_eng || undefined,
|
|
user_password: password || undefined,
|
|
email: userEditMode ? (userForm.email || null) : (userForm.email || undefined),
|
|
tel: userForm.tel || undefined,
|
|
cell_phone: userEditMode ? (userForm.cell_phone || null) : (userForm.cell_phone || undefined),
|
|
sabun: userEditMode ? (userForm.sabun || null) : (userForm.sabun || undefined),
|
|
position_name: userForm.position_name || undefined,
|
|
dept_code: userForm.dept_code || undefined,
|
|
dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined,
|
|
status: userForm.status || "active",
|
|
},
|
|
mainDept: userForm.dept_code ? {
|
|
dept_code: userForm.dept_code,
|
|
dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name,
|
|
position_name: userForm.position_name || undefined,
|
|
} : undefined,
|
|
isUpdate: userEditMode,
|
|
});
|
|
toast.success(userEditMode ? "사원 정보가 수정되었습니다." : "사원이 추가되었습니다.");
|
|
setUserModalOpen(false);
|
|
fetchMembers();
|
|
} catch (err: any) {
|
|
toast.error(err.response?.data?.message || "저장에 실패했습니다.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 엑셀 다운로드
|
|
const handleExcelDownload = async () => {
|
|
if (depts.length === 0) return;
|
|
const data = depts.map((d) => ({
|
|
부서코드: d.dept_code, 부서명: d.dept_name, 상위부서: d.parent_dept_code, 상태: d.status,
|
|
}));
|
|
await exportToExcel(data, "부서관리.xlsx", "부서");
|
|
toast.success("다운로드 완료");
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col gap-3 p-3">
|
|
{/* 검색 */}
|
|
<DynamicSearchFilter
|
|
tableName={DEPT_TABLE}
|
|
filterId="department"
|
|
onFilterChange={setSearchFilters}
|
|
dataCount={deptCount}
|
|
externalFilterConfig={filterConfig}
|
|
extraActions={
|
|
<div className="flex gap-1.5">
|
|
<Button variant="outline" size="sm" className="h-9" onClick={() => setTableSettingsOpen(true)}>
|
|
<Settings2 className="w-4 h-4 mr-1.5" /> 테이블 설정
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-9" onClick={() => setExcelUploadOpen(true)}>
|
|
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" /> 엑셀 업로드
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="h-9" onClick={handleExcelDownload}>
|
|
<Download className="w-3.5 h-3.5 mr-1" /> 엑셀 다운로드
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* 분할 패널 */}
|
|
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
|
|
<ResizablePanelGroup direction="horizontal">
|
|
{/* 좌측: 부서 */}
|
|
<ResizablePanel defaultSize={40} minSize={25}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center justify-between p-3 border-b bg-muted/10 shrink-0">
|
|
<div className="font-semibold flex items-center gap-2 text-sm">
|
|
<Building2 className="w-4 h-4" /> 부서
|
|
<Badge variant="secondary" className="font-normal">{deptCount}건</Badge>
|
|
</div>
|
|
<div className="flex gap-1.5">
|
|
<Button size="sm" onClick={openDeptRegister}><Plus className="w-3.5 h-3.5 mr-1" /> 등록</Button>
|
|
<Button variant="outline" size="sm" disabled={!selectedDeptCode} onClick={openDeptEdit}><Pencil className="w-3.5 h-3.5 mr-1" /> 수정</Button>
|
|
<Button variant="destructive" size="sm" disabled={!selectedDeptCode} onClick={handleDeptDelete}><Trash2 className="w-3.5 h-3.5 mr-1" /> 삭제</Button>
|
|
</div>
|
|
</div>
|
|
<DataGrid
|
|
gridId="dept-left"
|
|
columns={LEFT_COLUMNS}
|
|
data={depts}
|
|
loading={deptLoading}
|
|
selectedId={selectedDeptId}
|
|
onSelect={(id) => {
|
|
setSelectedDeptId((prev) => (prev === id ? null : id));
|
|
}}
|
|
onRowDoubleClick={() => openDeptEdit()}
|
|
emptyMessage="등록된 부서가 없습니다"
|
|
/>
|
|
</div>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* 우측: 사원 */}
|
|
<ResizablePanel defaultSize={60} minSize={30}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center justify-between p-3 border-b bg-muted/10 shrink-0">
|
|
<div className="font-semibold flex items-center gap-2 text-sm">
|
|
<Users className="w-4 h-4" />
|
|
{selectedDept ? "부서 인원" : "전체 사원"}
|
|
{selectedDept && <Badge variant="outline" className="font-normal">{selectedDept.dept_name}</Badge>}
|
|
{members.length > 0 && <Badge variant="secondary" className="font-normal">{members.length}명</Badge>}
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={() => openUserModal()}>
|
|
<Plus className="w-3.5 h-3.5 mr-1" /> 사원 추가
|
|
</Button>
|
|
</div>
|
|
<DataGrid
|
|
gridId="dept-right"
|
|
columns={RIGHT_COLUMNS}
|
|
data={members}
|
|
loading={memberLoading}
|
|
showRowNumber={false}
|
|
tableName={USER_TABLE}
|
|
emptyMessage={selectedDeptCode ? "소속 사원이 없습니다" : "등록된 사원이 없습니다"}
|
|
onRowDoubleClick={(row) => openUserModal(row)}
|
|
/>
|
|
</div>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
|
|
{/* 부서 등록/수정 모달 */}
|
|
<Dialog open={deptModalOpen} onOpenChange={setDeptModalOpen}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{deptEditMode ? "부서 수정" : "부서 등록"}</DialogTitle>
|
|
<DialogDescription>{deptEditMode ? "부서 정보를 수정합니다." : "새로운 부서를 등록합니다."}</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">부서코드</Label>
|
|
<Input value={deptEditMode ? (deptForm.dept_code || "") : (previewCode || "")}
|
|
placeholder={deptEditMode ? "" : (numberingRuleId ? "채번 조회 중..." : "자동 생성됩니다")}
|
|
className="h-9" disabled readOnly />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">부서명 <span className="text-destructive">*</span></Label>
|
|
<Input value={deptForm.dept_name || ""} onChange={(e) => setDeptForm((p) => ({ ...p, dept_name: e.target.value }))}
|
|
placeholder="부서명" className="h-9" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">상위부서</Label>
|
|
<Select value={deptForm.parent_dept_code || ""} onValueChange={(v) => setDeptForm((p) => ({ ...p, parent_dept_code: v }))}>
|
|
<SelectTrigger className="h-9"><SelectValue placeholder="상위부서 선택 (선택사항)" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">없음</SelectItem>
|
|
{depts.filter((d) => d.dept_code !== deptForm.dept_code).map((d) => (
|
|
<SelectItem key={d.dept_code} value={d.dept_code}>{d.dept_name} ({d.dept_code})</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDeptModalOpen(false)}>취소</Button>
|
|
<Button onClick={handleDeptSave} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <Save className="w-4 h-4 mr-1.5" />} 저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 사원 추가 모달 */}
|
|
<Dialog open={userModalOpen} onOpenChange={setUserModalOpen}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{userEditMode ? "사원 정보 수정" : "사원 추가"}</DialogTitle>
|
|
<DialogDescription>{userEditMode ? `${userForm.user_name} (${userForm.user_id}) 사원 정보를 수정합니다.` : selectedDept ? `${selectedDept.dept_name} 부서에 사원을 추가합니다.` : "사원을 추가합니다."}</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="grid grid-cols-2 gap-4 py-4">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">사용자 ID <span className="text-destructive">*</span></Label>
|
|
<Input value={userForm.user_id || ""} onChange={(e) => setUserForm((p) => ({ ...p, user_id: e.target.value }))}
|
|
placeholder="사용자 ID" className="h-9" disabled={userEditMode} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">이름 <span className="text-destructive">*</span></Label>
|
|
<Input value={userForm.user_name || ""} onChange={(e) => setUserForm((p) => ({ ...p, user_name: e.target.value }))}
|
|
placeholder="이름" className="h-9" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">사번</Label>
|
|
<Input value={userForm.sabun || ""} onChange={(e) => setUserForm((p) => ({ ...p, sabun: e.target.value }))}
|
|
placeholder="사번" className="h-9" autoComplete="off" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">비밀번호</Label>
|
|
<Input value={userForm.user_password || ""} onChange={(e) => setUserForm((p) => ({ ...p, user_password: e.target.value }))}
|
|
placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" autoComplete="new-password" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">직급</Label>
|
|
<Input value={userForm.position_name || ""} onChange={(e) => setUserForm((p) => ({ ...p, position_name: e.target.value }))}
|
|
placeholder="직급" className="h-9" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">부서 <span className="text-destructive">*</span></Label>
|
|
<Select value={userForm.dept_code || ""} onValueChange={(v) => setUserForm((p) => ({ ...p, dept_code: v }))}>
|
|
<SelectTrigger className="h-9"><SelectValue placeholder="부서 선택" /></SelectTrigger>
|
|
<SelectContent>
|
|
{depts.map((d) => <SelectItem key={d.dept_code} value={d.dept_code}>{d.dept_name}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">휴대폰</Label>
|
|
<Input value={userForm.cell_phone || ""} onChange={(e) => handleUserFormChange("cell_phone", e.target.value)}
|
|
placeholder="010-0000-0000" className={cn("h-9", formErrors.cell_phone && "border-destructive")} />
|
|
{formErrors.cell_phone && <p className="text-xs text-destructive">{formErrors.cell_phone}</p>}
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">이메일</Label>
|
|
<Input value={userForm.email || ""} onChange={(e) => handleUserFormChange("email", e.target.value)}
|
|
placeholder="example@email.com" className={cn("h-9", formErrors.email && "border-destructive")} />
|
|
{formErrors.email && <p className="text-xs text-destructive">{formErrors.email}</p>}
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">입사일</Label>
|
|
<FormDatePicker value={userForm.regdate || ""} onChange={(v) => setUserForm((p) => ({ ...p, regdate: v }))} placeholder="입사일" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-sm">퇴사일</Label>
|
|
<FormDatePicker value={userForm.end_date || ""} onChange={(v) => setUserForm((p) => ({ ...p, end_date: v }))} placeholder="퇴사일" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setUserModalOpen(false)}>취소</Button>
|
|
<Button onClick={handleUserSave} disabled={saving}>
|
|
{saving ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <Save className="w-4 h-4 mr-1.5" />} 저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 엑셀 업로드 */}
|
|
<ExcelUploadModal
|
|
open={excelUploadOpen}
|
|
onOpenChange={setExcelUploadOpen}
|
|
tableName={DEPT_TABLE}
|
|
userId={user?.userId}
|
|
onSuccess={() => fetchDepts()}
|
|
/>
|
|
|
|
{ConfirmDialogComponent}
|
|
|
|
<TableSettingsModal
|
|
open={tableSettingsOpen}
|
|
onOpenChange={setTableSettingsOpen}
|
|
tableName={DEPT_TABLE}
|
|
settingsId="department"
|
|
onSave={applyTableSettings}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|