4d19c31440
직전 Phase 1 의 후속 폴리시. 신규 백엔드 - crosstenant/CrossTenantDeptController.java GET /api/admin/cross-tenant/departments?company_code=TEST02 단일 모드 GET /admin/departments 와 응답 형태 동일. company_code query param 으로 명시된 회사 DB 컨텍스트로 임시 전환해서 부서 트리 반환. 버그 수정: 메타 DB DEPT_INFO 시드 (qnc/COMPANY_7 등 다른 회사 부서) 가 TEST02 선택 시에도 dropdown 에 섞여 보이던 문제 해결. 프론트 - lib/api/user.ts — getDepartmentList(companyCode) 가 isCrossTenantMode() 면 /admin/cross-tenant/departments?company_code= 호출. cross-tenant 모드 + companyCode 미지정 → 빈 배열 반환 (회사 안 골랐는데 메타 부서 보여주는 것 방지). UserFormModal - 회사 dropdown 을 폼 가장 위로 이동 — 사용자 ID 중복확인·부서 선택이 모두 회사에 의존하므로 자연스러운 입력 순서 - SUPER_ADMIN 인데 회사 미선택 상태에선 사용자 ID input + 중복확인 버튼 disable + placeholder "회사 먼저 선택" - checkUserIdDuplicate 가드: 회사 미선택이면 "회사를 먼저 선택해주세요" (백엔드의 400 "company_code 가 비어있음" 보다 친절) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
709 lines
26 KiB
TypeScript
709 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Eye, EyeOff } from "lucide-react";
|
|
import { userAPI } from "@/lib/api/user";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
// 알림 모달 컴포넌트
|
|
interface AlertModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
message: string;
|
|
type?: "success" | "error" | "info";
|
|
}
|
|
|
|
function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertModalProps) {
|
|
const getTypeColor = () => {
|
|
switch (type) {
|
|
case "success":
|
|
return "text-emerald-600";
|
|
case "error":
|
|
return "text-destructive";
|
|
default:
|
|
return "text-primary";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className={getTypeColor()}>{title}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="py-4">
|
|
<p className="text-muted-foreground text-sm">{message}</p>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button onClick={onClose} className="w-20">
|
|
확인
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
interface UserFormModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess?: () => void;
|
|
editingUser?: any | null;
|
|
}
|
|
|
|
interface CompanyOption {
|
|
company_code: string;
|
|
company_name: string;
|
|
[key: string]: any; // 기타 필드들
|
|
}
|
|
|
|
interface DepartmentOption {
|
|
dept_code?: string;
|
|
deptCode?: string;
|
|
dept_name?: string;
|
|
deptName?: string;
|
|
parent_dept_code?: string;
|
|
parentDeptCode?: string;
|
|
masterSabun?: string;
|
|
masterUserId?: string;
|
|
location?: string;
|
|
locationName?: string;
|
|
regdate?: string;
|
|
dataType?: string;
|
|
status?: string;
|
|
salesYn?: string;
|
|
companyName?: string;
|
|
children?: DepartmentOption[];
|
|
// 기존 호환성을 위한 필드들
|
|
CODE?: string;
|
|
NAME?: string;
|
|
DEPT_CODE?: string;
|
|
DEPT_NAME?: string;
|
|
[key: string]: any; // 기타 필드들
|
|
}
|
|
|
|
export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserFormModalProps) {
|
|
// 현재 로그인한 사용자 정보
|
|
const { user: currentUser } = useAuth();
|
|
|
|
// 최고 관리자 여부 (company_code === '*' && user_type === 'SUPER_ADMIN')
|
|
const isSuperAdmin = currentUser?.company_code === "*" && currentUser?.user_type === "SUPER_ADMIN";
|
|
|
|
// 수정 모드 여부
|
|
const isEditMode = !!editingUser;
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [companies, setCompanies] = useState<CompanyOption[]>([]);
|
|
const [departments, setDepartments] = useState<DepartmentOption[]>([]);
|
|
|
|
// 알림 모달 상태
|
|
const [alertModal, setAlertModal] = useState({
|
|
isOpen: false,
|
|
title: "",
|
|
message: "",
|
|
type: "info" as "success" | "error" | "info",
|
|
});
|
|
|
|
// 알림 모달 표시 함수 (useCallback 유지 - 다른 useCallback의 의존성으로 사용됨)
|
|
const showAlert = useCallback((title: string, message: string, type: "success" | "error" | "info" = "info") => {
|
|
setAlertModal({
|
|
isOpen: true,
|
|
title,
|
|
message,
|
|
type,
|
|
});
|
|
}, []);
|
|
|
|
// 알림 모달 닫기 함수 (useCallback 제거 - 의존성이 없고 성능상 이점 없음)
|
|
const closeAlert = () => {
|
|
setAlertModal((prev) => ({ ...prev, isOpen: false }));
|
|
};
|
|
const [formData, setFormData] = useState({
|
|
user_id: "",
|
|
user_password: "",
|
|
user_name: "",
|
|
email: "",
|
|
tel: "",
|
|
cell_phone: "",
|
|
position_name: "",
|
|
company_code: "",
|
|
dept_code: "",
|
|
user_type: "USER", // 기본값: 일반 사용자
|
|
sabun: null, // 항상 null로 설정
|
|
});
|
|
|
|
// ID 중복체크 상태 관리
|
|
const [isUserIdChecked, setIsUserIdChecked] = useState(false);
|
|
const [lastCheckedUserId, setLastCheckedUserId] = useState("");
|
|
const [duplicateCheckMessage, setDuplicateCheckMessage] = useState("");
|
|
const [duplicateCheckType, setDuplicateCheckType] = useState<"success" | "error" | "">("");
|
|
|
|
// 필수 필드 검증 (실시간)
|
|
const isFormValid = useMemo(() => {
|
|
// 수정 모드에서는 비밀번호 선택 사항 (변경할 경우만 입력)
|
|
const requiredFields = isEditMode
|
|
? [formData.user_id.trim(), formData.user_name.trim(), formData.company_code]
|
|
: [
|
|
formData.user_id.trim(),
|
|
formData.user_password.trim(),
|
|
formData.user_name.trim(),
|
|
formData.company_code,
|
|
];
|
|
|
|
// 모든 필수 필드가 입력되었는지 확인
|
|
const allFieldsFilled = requiredFields.every((field) => field);
|
|
|
|
// 수정 모드: ID 중복체크 불필요 (이미 존재하는 사용자)
|
|
// 등록 모드: ID 중복체크 필수
|
|
const duplicateCheckValid = isEditMode || (isUserIdChecked && lastCheckedUserId === formData.user_id);
|
|
|
|
return allFieldsFilled && duplicateCheckValid;
|
|
}, [formData, isUserIdChecked, lastCheckedUserId, isEditMode]);
|
|
|
|
// 회사 목록 로드
|
|
const loadCompanies = useCallback(async () => {
|
|
try {
|
|
const companyList = await userAPI.getCompanyList();
|
|
setCompanies(companyList);
|
|
} catch (error) {
|
|
console.error("회사 목록 조회 오류:", error);
|
|
showAlert("오류 발생", "회사 목록을 불러오는데 실패했습니다.", "error");
|
|
}
|
|
}, [showAlert]);
|
|
|
|
// 부서 목록 로드
|
|
const loadDepartments = useCallback(
|
|
async (companyCode?: string) => {
|
|
try {
|
|
const departmentList = await userAPI.getDepartmentList(companyCode);
|
|
setDepartments(departmentList);
|
|
} catch (error) {
|
|
console.error("부서 목록 조회 오류:", error);
|
|
showAlert("오류 발생", "부서 목록을 불러오는데 실패했습니다.", "error");
|
|
}
|
|
},
|
|
[showAlert],
|
|
);
|
|
|
|
// 모달이 열릴 때 회사 목록 및 부서 목록 로드, 수정 모드면 데이터 로드
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
loadCompanies();
|
|
loadDepartments(); // 전체 부서 목록 로드
|
|
|
|
// 수정 모드: 기존 사용자 정보 로드
|
|
if (isEditMode && editingUser) {
|
|
setFormData({
|
|
user_id: editingUser.user_id || "",
|
|
user_password: "", // 수정 시 비밀번호는 비워둠 (변경 원할 경우만 입력)
|
|
user_name: editingUser.user_name || "",
|
|
email: editingUser.email || "",
|
|
tel: editingUser.tel || "",
|
|
cell_phone: editingUser.cell_phone || "",
|
|
position_name: editingUser.position_name || "",
|
|
company_code: editingUser.company_code || "",
|
|
dept_code: editingUser.dept_code || "",
|
|
user_type: editingUser.user_type || "USER",
|
|
sabun: editingUser.sabun || null,
|
|
});
|
|
// 수정 모드에서는 ID 중복체크 불필요
|
|
setIsUserIdChecked(true);
|
|
setLastCheckedUserId(editingUser.user_id);
|
|
} else {
|
|
// 등록 모드: 폼 초기화
|
|
setFormData({
|
|
user_id: "",
|
|
user_password: "",
|
|
user_name: "",
|
|
email: "",
|
|
tel: "",
|
|
cell_phone: "",
|
|
position_name: "",
|
|
company_code: "",
|
|
dept_code: "",
|
|
user_type: "USER",
|
|
sabun: null,
|
|
});
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("");
|
|
setDuplicateCheckType("");
|
|
}
|
|
}
|
|
}, [isOpen, isEditMode, editingUser, loadCompanies, loadDepartments]);
|
|
|
|
// 회사 선택 시 부서 목록 업데이트
|
|
useEffect(() => {
|
|
if (formData.company_code) {
|
|
loadDepartments(formData.company_code);
|
|
// 회사 변경 시 부서 선택 초기화
|
|
setFormData((prev) => ({ ...prev, dept_code: "" }));
|
|
}
|
|
}, [formData.company_code, loadDepartments]);
|
|
|
|
// 폼 데이터 변경 핸들러 (useCallback 제거 - 의존성이 없고 성능상 이점 없음)
|
|
const handleInputChange = (field: string, value: string) => {
|
|
// userId가 변경되면 중복체크 상태 초기화
|
|
if (field === "user_id") {
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("");
|
|
setDuplicateCheckType("");
|
|
}
|
|
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
// 사용자 ID 중복 체크
|
|
const checkUserIdDuplicate = async () => {
|
|
if (!formData.user_id.trim()) {
|
|
setDuplicateCheckMessage("사용자 ID를 입력해주세요.");
|
|
setDuplicateCheckType("error");
|
|
return;
|
|
}
|
|
|
|
// SUPER_ADMIN(메타) 은 어느 회사에서 체크할지 먼저 골라야 함.
|
|
// 회사별 USER_INFO 라 회사 미선택 상태에선 체크 의미가 없음.
|
|
if (isSuperAdmin && !formData.company_code) {
|
|
setDuplicateCheckMessage("회사를 먼저 선택해주세요.");
|
|
setDuplicateCheckType("error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// cross-tenant 모드: 회사별 USER_INFO 라 그 회사 코드와 함께 중복 체크.
|
|
// 단일 모드: 두번째 인자 무시 (백엔드가 JWT.company_code 사용).
|
|
const response = await userAPI.checkDuplicateId(formData.user_id, formData.company_code);
|
|
if (response.success && response.data) {
|
|
// 백엔드 API 응답 구조: { is_duplicate: boolean, message: string }
|
|
const isDuplicate = response.data.is_duplicate;
|
|
const message = response.data.message;
|
|
|
|
if (!isDuplicate) {
|
|
// 중복되지 않음 (사용 가능)
|
|
setIsUserIdChecked(true);
|
|
setLastCheckedUserId(formData.user_id);
|
|
setDuplicateCheckMessage(message || "사용 가능한 사용자 ID입니다.");
|
|
setDuplicateCheckType("success");
|
|
} else {
|
|
// 중복됨 (사용 불가)
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage(message || "이미 사용 중인 사용자 ID입니다.");
|
|
setDuplicateCheckType("error");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("ID 중복 체크 오류:", error);
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("ID 중복 체크 중 오류가 발생했습니다.");
|
|
setDuplicateCheckType("error");
|
|
}
|
|
};
|
|
|
|
// 유효성 검사
|
|
const validateForm = useCallback(() => {
|
|
if (!formData.user_id.trim()) {
|
|
showAlert("입력 오류", "사용자 ID를 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
// ID 중복체크 필수 검증
|
|
if (!isUserIdChecked || lastCheckedUserId !== formData.user_id) {
|
|
showAlert("중복체크 필요", "사용자 ID 중복체크를 먼저 진행해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.user_password.trim()) {
|
|
showAlert("입력 오류", "비밀번호를 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.user_name.trim()) {
|
|
showAlert("입력 오류", "사용자명을 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.company_code) {
|
|
showAlert("입력 오류", "회사를 선택해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
// 이메일 형식 검사 (입력된 경우만)
|
|
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
showAlert("입력 오류", "올바른 이메일 형식을 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}, [formData, isUserIdChecked, lastCheckedUserId, showAlert]);
|
|
|
|
// 사용자 등록
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const userDataToSend = {
|
|
user_id: formData.user_id,
|
|
user_password: formData.user_password,
|
|
user_name: formData.user_name,
|
|
email: formData.email || null,
|
|
tel: formData.tel || null,
|
|
cell_phone: formData.cell_phone || null,
|
|
position_name: formData.position_name || null,
|
|
company_code: formData.company_code,
|
|
dept_code: formData.dept_code || null,
|
|
user_type: formData.user_type, // 권한 타입 추가
|
|
sabun: null, // 항상 null (테이블 1번 컬럼)
|
|
status: "active", // 기본값 (테이블 18번 컬럼)
|
|
};
|
|
|
|
let response;
|
|
if (isEditMode) {
|
|
// 수정 모드: 비밀번호 필드 제외 (비밀번호 초기화 기능 별도 제공)
|
|
const { user_password: _pw, ...updateData } = userDataToSend;
|
|
response = await userAPI.update(updateData as any);
|
|
} else {
|
|
// 등록 모드
|
|
response = await userAPI.create(userDataToSend);
|
|
}
|
|
|
|
if (response.success) {
|
|
showAlert(
|
|
isEditMode ? "수정 완료" : "등록 완료",
|
|
isEditMode ? "사용자 정보가 성공적으로 수정되었습니다." : "사용자가 성공적으로 등록되었습니다.",
|
|
"success",
|
|
);
|
|
// 성공 시 모달을 바로 닫지 않고 사용자가 확인 후 닫도록 수정
|
|
setTimeout(() => {
|
|
onClose();
|
|
onSuccess?.();
|
|
}, 1500); // 1.5초 후 자동으로 모달 닫기
|
|
} else {
|
|
showAlert(
|
|
isEditMode ? "수정 실패" : "등록 실패",
|
|
response.message || (isEditMode ? "사용자 정보 수정에 실패했습니다." : "사용자 등록에 실패했습니다."),
|
|
"error",
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(isEditMode ? "사용자 수정 오류:" : "사용자 등록 오류:", error);
|
|
showAlert(
|
|
"오류 발생",
|
|
isEditMode ? "사용자 정보 수정 중 오류가 발생했습니다." : "사용자 등록 중 오류가 발생했습니다.",
|
|
"error",
|
|
);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [formData, validateForm, onSuccess, onClose, showAlert, isEditMode]);
|
|
|
|
// 모달 닫기
|
|
const handleClose = useCallback(() => {
|
|
setFormData({
|
|
user_id: "",
|
|
user_password: "",
|
|
user_name: "",
|
|
email: "",
|
|
tel: "",
|
|
cell_phone: "",
|
|
position_name: "",
|
|
company_code: "",
|
|
dept_code: "",
|
|
user_type: "USER",
|
|
sabun: null,
|
|
});
|
|
setShowPassword(false);
|
|
// ID 중복체크 상태 초기화
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("");
|
|
setDuplicateCheckType("");
|
|
onClose();
|
|
}, [onClose]);
|
|
|
|
// Enter 키 처리
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && !isLoading) {
|
|
handleSubmit();
|
|
}
|
|
},
|
|
[handleSubmit, isLoading],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
|
|
<DialogHeader>
|
|
<DialogTitle>{isEditMode ? "사용자 정보 수정" : "사용자 등록"}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
{/* 회사 선택 — 모든 후속 입력의 컨텍스트라 가장 위 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="companyCode" className="text-sm font-medium">
|
|
회사 <span className="text-destructive">*</span>
|
|
</Label>
|
|
{isSuperAdmin ? (
|
|
<>
|
|
<Select
|
|
value={formData.company_code}
|
|
onValueChange={(value) => handleInputChange("company_code", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="회사 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{companies.map((company) => (
|
|
<SelectItem key={company.company_code} value={company.company_code}>
|
|
{company.company_name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground text-xs">
|
|
회사를 먼저 선택해야 사용자 ID 중복확인과 부서 선택이 가능합니다.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Input
|
|
id="companyCode"
|
|
value={
|
|
companies.find((c) => c.company_code === formData.company_code)?.company_name ||
|
|
formData.company_code
|
|
}
|
|
disabled
|
|
className="bg-muted cursor-not-allowed"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">회사는 최고 관리자만 변경할 수 있습니다.</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* 기본 정보 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userId" className="text-sm font-medium">
|
|
사용자 ID <span className="text-destructive">*</span>
|
|
</Label>
|
|
{isEditMode ? (
|
|
<Input id="userId" value={formData.user_id} disabled className="bg-muted cursor-not-allowed" />
|
|
) : (
|
|
<>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="userId"
|
|
placeholder={isSuperAdmin && !formData.company_code ? "회사 먼저 선택" : "사용자 ID 입력"}
|
|
value={formData.user_id}
|
|
onChange={(e) => handleInputChange("user_id", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
disabled={isSuperAdmin && !formData.company_code}
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant={isUserIdChecked && lastCheckedUserId === formData.user_id ? "default" : "outline"}
|
|
onClick={checkUserIdDuplicate}
|
|
disabled={!formData.user_id.trim() || isLoading || (isSuperAdmin && !formData.company_code)}
|
|
className="whitespace-nowrap"
|
|
>
|
|
{isUserIdChecked && lastCheckedUserId === formData.user_id ? "확인완료" : "중복확인"}
|
|
</Button>
|
|
</div>
|
|
{/* 중복확인 결과 메시지 */}
|
|
{duplicateCheckMessage && (
|
|
<div
|
|
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-emerald-600" : "text-destructive"}`}
|
|
>
|
|
{duplicateCheckMessage}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userName" className="text-sm font-medium">
|
|
사용자명 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="userName"
|
|
placeholder="사용자명 입력"
|
|
value={formData.user_name}
|
|
onChange={(e) => handleInputChange("user_name", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 비밀번호 - 등록 모드에만 표시 */}
|
|
{!isEditMode && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userPassword" className="text-sm font-medium">
|
|
비밀번호 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="userPassword"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="비밀번호 입력"
|
|
value={formData.user_password}
|
|
onChange={(e) => handleInputChange("user_password", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="pr-10"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute top-0 right-0 h-full px-3 hover:bg-transparent"
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
<p className="text-muted-foreground text-xs">
|
|
비밀번호 변경은 별도의 비밀번호 초기화 기능을 이용하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 부서 정보 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="deptCode" className="text-sm font-medium">
|
|
부서
|
|
</Label>
|
|
<Select value={formData.dept_code} onValueChange={(value) => handleInputChange("dept_code", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="부서 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.isArray(departments) && departments.length > 0 ? (
|
|
departments
|
|
.filter((department) => {
|
|
const deptCode = department.dept_code || department.CODE || department.DEPT_CODE;
|
|
return deptCode && deptCode.trim() !== "";
|
|
})
|
|
.map((department) => {
|
|
const deptCode = department.dept_code || department.CODE || department.DEPT_CODE || "";
|
|
const deptName =
|
|
department.dept_name || department.NAME || department.DEPT_NAME || "Unknown Department";
|
|
|
|
return (
|
|
<SelectItem key={deptCode} value={deptCode}>
|
|
{deptName}
|
|
</SelectItem>
|
|
);
|
|
})
|
|
) : (
|
|
<SelectItem value="no-data" disabled>
|
|
부서 정보가 없습니다
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 연락처 정보 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email" className="text-sm font-medium">
|
|
이메일
|
|
</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="이메일 입력 (선택사항)"
|
|
value={formData.email}
|
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="tel" className="text-sm font-medium">
|
|
전화번호
|
|
</Label>
|
|
<Input
|
|
id="tel"
|
|
placeholder="전화번호 입력 (선택사항)"
|
|
value={formData.tel}
|
|
onChange={(e) => handleInputChange("tel", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 추가 정보 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cellPhone" className="text-sm font-medium">
|
|
휴대폰
|
|
</Label>
|
|
<Input
|
|
id="cellPhone"
|
|
placeholder="휴대폰 번호 입력 (선택사항)"
|
|
value={formData.cell_phone}
|
|
onChange={(e) => handleInputChange("cell_phone", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="positionName" className="text-sm font-medium">
|
|
직책
|
|
</Label>
|
|
<Input
|
|
id="positionName"
|
|
placeholder="직책명 입력 (선택사항)"
|
|
value={formData.position_name}
|
|
onChange={(e) => handleInputChange("position_name", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 버튼 영역 */}
|
|
<div className="flex justify-end gap-3 border-t pt-4">
|
|
<Button type="button" variant="outline" onClick={handleClose} disabled={isLoading}>
|
|
취소
|
|
</Button>
|
|
<Button type="button" onClick={handleSubmit} disabled={isLoading || !isFormValid} className="min-w-[80px]">
|
|
{isLoading ? "처리중..." : isEditMode ? "수정" : "등록"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 알림 모달 */}
|
|
<AlertModal
|
|
isOpen={alertModal.isOpen}
|
|
onClose={closeAlert}
|
|
title={alertModal.title}
|
|
message={alertModal.message}
|
|
type={alertModal.type}
|
|
/>
|
|
</>
|
|
);
|
|
}
|