701 lines
25 KiB
TypeScript
701 lines
25 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-green-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 {
|
|
deptCode?: string;
|
|
deptName?: 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 === '*' && userType === 'SUPER_ADMIN')
|
|
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "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({
|
|
userId: "",
|
|
userPassword: "",
|
|
userName: "",
|
|
email: "",
|
|
tel: "",
|
|
cellPhone: "",
|
|
positionName: "",
|
|
companyCode: "",
|
|
deptCode: "",
|
|
userType: "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.userId.trim(), formData.userName.trim(), formData.companyCode, formData.deptCode]
|
|
: [
|
|
formData.userId.trim(),
|
|
formData.userPassword.trim(),
|
|
formData.userName.trim(),
|
|
formData.companyCode,
|
|
formData.deptCode,
|
|
];
|
|
|
|
// 모든 필수 필드가 입력되었는지 확인
|
|
const allFieldsFilled = requiredFields.every((field) => field);
|
|
|
|
// 수정 모드: ID 중복체크 불필요 (이미 존재하는 사용자)
|
|
// 등록 모드: ID 중복체크 필수
|
|
const duplicateCheckValid = isEditMode || (isUserIdChecked && lastCheckedUserId === formData.userId);
|
|
|
|
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({
|
|
userId: editingUser.userId || "",
|
|
userPassword: "", // 수정 시 비밀번호는 비워둠 (변경 원할 경우만 입력)
|
|
userName: editingUser.userName || "",
|
|
email: editingUser.email || "",
|
|
tel: editingUser.tel || "",
|
|
cellPhone: editingUser.cellPhone || "",
|
|
positionName: editingUser.positionName || "",
|
|
companyCode: editingUser.companyCode || "",
|
|
deptCode: editingUser.deptCode || "",
|
|
userType: editingUser.userType || "USER",
|
|
sabun: editingUser.sabun || null,
|
|
});
|
|
// 수정 모드에서는 ID 중복체크 불필요
|
|
setIsUserIdChecked(true);
|
|
setLastCheckedUserId(editingUser.userId);
|
|
} else {
|
|
// 등록 모드: 폼 초기화
|
|
setFormData({
|
|
userId: "",
|
|
userPassword: "",
|
|
userName: "",
|
|
email: "",
|
|
tel: "",
|
|
cellPhone: "",
|
|
positionName: "",
|
|
companyCode: "",
|
|
deptCode: "",
|
|
userType: "USER",
|
|
sabun: null,
|
|
});
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("");
|
|
setDuplicateCheckType("");
|
|
}
|
|
}
|
|
}, [isOpen, isEditMode, editingUser, loadCompanies, loadDepartments]);
|
|
|
|
// 회사 선택 시 부서 목록 업데이트
|
|
useEffect(() => {
|
|
if (formData.companyCode) {
|
|
loadDepartments(formData.companyCode);
|
|
// 회사 변경 시 부서 선택 초기화
|
|
setFormData((prev) => ({ ...prev, deptCode: "" }));
|
|
}
|
|
}, [formData.companyCode, loadDepartments]);
|
|
|
|
// 폼 데이터 변경 핸들러 (useCallback 제거 - 의존성이 없고 성능상 이점 없음)
|
|
const handleInputChange = (field: string, value: string) => {
|
|
// userId가 변경되면 중복체크 상태 초기화
|
|
if (field === "userId") {
|
|
setIsUserIdChecked(false);
|
|
setLastCheckedUserId("");
|
|
setDuplicateCheckMessage("");
|
|
setDuplicateCheckType("");
|
|
}
|
|
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
// 사용자 ID 중복 체크
|
|
const checkUserIdDuplicate = async () => {
|
|
if (!formData.userId.trim()) {
|
|
setDuplicateCheckMessage("사용자 ID를 입력해주세요.");
|
|
setDuplicateCheckType("error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await userAPI.checkDuplicateId(formData.userId);
|
|
if (response.success && response.data) {
|
|
// 백엔드 API 응답 구조: { isDuplicate: boolean, message: string }
|
|
const isDuplicate = response.data.isDuplicate;
|
|
const message = response.data.message;
|
|
|
|
if (!isDuplicate) {
|
|
// 중복되지 않음 (사용 가능)
|
|
setIsUserIdChecked(true);
|
|
setLastCheckedUserId(formData.userId);
|
|
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.userId.trim()) {
|
|
showAlert("입력 오류", "사용자 ID를 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
// ID 중복체크 필수 검증
|
|
if (!isUserIdChecked || lastCheckedUserId !== formData.userId) {
|
|
showAlert("중복체크 필요", "사용자 ID 중복체크를 먼저 진행해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.userPassword.trim()) {
|
|
showAlert("입력 오류", "비밀번호를 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.userName.trim()) {
|
|
showAlert("입력 오류", "사용자명을 입력해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.companyCode) {
|
|
showAlert("입력 오류", "회사를 선택해주세요.", "error");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.deptCode) {
|
|
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 = {
|
|
userId: formData.userId,
|
|
userPassword: formData.userPassword,
|
|
userName: formData.userName,
|
|
email: formData.email || null,
|
|
tel: formData.tel || null,
|
|
cellPhone: formData.cellPhone || null,
|
|
positionName: formData.positionName || null,
|
|
companyCode: formData.companyCode,
|
|
deptCode: formData.deptCode || null,
|
|
userType: formData.userType, // 권한 타입 추가
|
|
sabun: null, // 항상 null (테이블 1번 컬럼)
|
|
status: "active", // 기본값 (테이블 18번 컬럼)
|
|
};
|
|
|
|
let response;
|
|
if (isEditMode) {
|
|
// 수정 모드: 비밀번호 필드 제외 (비밀번호 초기화 기능 별도 제공)
|
|
const updateData = { ...userDataToSend };
|
|
delete updateData.userPassword;
|
|
response = await userAPI.update(updateData);
|
|
} 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({
|
|
userId: "",
|
|
userPassword: "",
|
|
userName: "",
|
|
email: "",
|
|
tel: "",
|
|
cellPhone: "",
|
|
positionName: "",
|
|
companyCode: "",
|
|
deptCode: "",
|
|
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="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userId" className="text-sm font-medium">
|
|
사용자 ID <span className="text-red-500">*</span>
|
|
</Label>
|
|
{isEditMode ? (
|
|
<Input id="userId" value={formData.userId} disabled className="bg-muted cursor-not-allowed" />
|
|
) : (
|
|
<>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="userId"
|
|
placeholder="사용자 ID 입력"
|
|
value={formData.userId}
|
|
onChange={(e) => handleInputChange("userId", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant={isUserIdChecked && lastCheckedUserId === formData.userId ? "default" : "outline"}
|
|
onClick={checkUserIdDuplicate}
|
|
disabled={!formData.userId.trim() || isLoading}
|
|
className="whitespace-nowrap"
|
|
>
|
|
{isUserIdChecked && lastCheckedUserId === formData.userId ? "확인완료" : "중복확인"}
|
|
</Button>
|
|
</div>
|
|
{/* 중복확인 결과 메시지 */}
|
|
{duplicateCheckMessage && (
|
|
<div
|
|
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-green-600" : "text-destructive"}`}
|
|
>
|
|
{duplicateCheckMessage}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userName" className="text-sm font-medium">
|
|
사용자명 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="userName"
|
|
placeholder="사용자명 입력"
|
|
value={formData.userName}
|
|
onChange={(e) => handleInputChange("userName", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 비밀번호 - 등록 모드에만 표시 */}
|
|
{!isEditMode && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="userPassword" className="text-sm font-medium">
|
|
비밀번호 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="userPassword"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="비밀번호 입력"
|
|
value={formData.userPassword}
|
|
onChange={(e) => handleInputChange("userPassword", 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="space-y-2">
|
|
<Label htmlFor="companyCode" className="text-sm font-medium">
|
|
회사 <span className="text-red-500">*</span>
|
|
</Label>
|
|
{isSuperAdmin ? (
|
|
<>
|
|
<Select
|
|
value={formData.companyCode}
|
|
onValueChange={(value) => handleInputChange("companyCode", 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">
|
|
권한 관리는 별도의 권한 관리 페이지에서 설정할 수 있습니다.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Input
|
|
id="companyCode"
|
|
value={
|
|
companies.find((c) => c.company_code === formData.companyCode)?.company_name ||
|
|
formData.companyCode
|
|
}
|
|
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="deptCode" className="text-sm font-medium">
|
|
부서 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Select value={formData.deptCode} onValueChange={(value) => handleInputChange("deptCode", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="부서 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.isArray(departments) && departments.length > 0 ? (
|
|
departments
|
|
.filter((department) => {
|
|
const deptCode = department.deptCode || department.CODE || department.DEPT_CODE;
|
|
return deptCode && deptCode.trim() !== "";
|
|
})
|
|
.map((department) => {
|
|
const deptCode = department.deptCode || department.CODE || department.DEPT_CODE || "";
|
|
const deptName =
|
|
department.deptName || 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.cellPhone}
|
|
onChange={(e) => handleInputChange("cellPhone", 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.positionName}
|
|
onChange={(e) => handleInputChange("positionName", 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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|