- Added validation to ensure that only existing SUPER_ADMIN users can grant or modify SUPER_ADMIN permissions. - Updated the user management page to reflect that both SUPER_ADMIN and COMPANY_ADMIN can access the user permissions, while COMPANY_ADMIN cannot grant SUPER_ADMIN rights. - Enhanced the user authorization modal to prevent COMPANY_ADMIN from changing SUPER_ADMIN permissions, ensuring proper access control. These changes improve the security and integrity of user role management within the application.
697 lines
25 KiB
TypeScript
697 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-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 {
|
|
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.userId.trim(),
|
|
formData.userPassword.trim(),
|
|
formData.userName.trim(),
|
|
formData.companyCode,
|
|
];
|
|
|
|
// 모든 필수 필드가 입력되었는지 확인
|
|
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.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-destructive">*</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}
|
|
autoComplete="off"
|
|
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-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.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-destructive">*</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}
|
|
autoComplete="new-password"
|
|
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-destructive">*</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">
|
|
부서
|
|
</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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|