383 lines
14 KiB
TypeScript
383 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
|
import {
|
|
ResizableDialog,
|
|
ResizableDialogContent,
|
|
ResizableDialogHeader,
|
|
ResizableDialogTitle,
|
|
ResizableDialogDescription,
|
|
ResizableDialogFooter,
|
|
} from "@/components/ui/resizable-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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { roleAPI, RoleGroup } from "@/lib/api/role";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { AlertCircle, Check, ChevronsUpDown } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { companyAPI } from "@/lib/api/company";
|
|
|
|
interface RoleFormModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess?: () => void;
|
|
editingRole?: RoleGroup | null;
|
|
}
|
|
|
|
/**
|
|
* 권한 그룹 생성/수정 모달
|
|
*
|
|
* 기능:
|
|
* - 권한 그룹 생성 (authName, authCode, companyCode)
|
|
* - 권한 그룹 수정 (authName, authCode, status)
|
|
* - 유효성 검사
|
|
*
|
|
* shadcn/ui 표준 모달 디자인 적용
|
|
*/
|
|
export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleFormModalProps) {
|
|
const { user: currentUser } = useAuth();
|
|
const isEditMode = !!editingRole;
|
|
|
|
// 최고 관리자 여부
|
|
const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
|
|
|
|
// 폼 데이터
|
|
const [formData, setFormData] = useState({
|
|
authName: "",
|
|
authCode: "",
|
|
companyCode: currentUser?.companyCode || "",
|
|
status: "active",
|
|
});
|
|
|
|
// 상태 관리
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showAlert, setShowAlert] = useState(false);
|
|
const [alertMessage, setAlertMessage] = useState("");
|
|
const [alertType, setAlertType] = useState<"success" | "error" | "info">("info");
|
|
|
|
// 회사 목록 (최고 관리자용)
|
|
const [companies, setCompanies] = useState<Array<{ company_code: string; company_name: string }>>([]);
|
|
const [isLoadingCompanies, setIsLoadingCompanies] = useState(false);
|
|
const [companyComboOpen, setCompanyComboOpen] = useState(false);
|
|
|
|
// 폼 유효성 검사
|
|
const isFormValid = useMemo(() => {
|
|
return formData.authName.trim() !== "" && formData.authCode.trim() !== "" && formData.companyCode.trim() !== "";
|
|
}, [formData]);
|
|
|
|
// 알림 표시
|
|
const displayAlert = useCallback((message: string, type: "success" | "error" | "info") => {
|
|
setAlertMessage(message);
|
|
setAlertType(type);
|
|
setShowAlert(true);
|
|
setTimeout(() => setShowAlert(false), 3000);
|
|
}, []);
|
|
|
|
// 회사 목록 로드 (최고 관리자만)
|
|
const loadCompanies = useCallback(async () => {
|
|
if (!isSuperAdmin) return;
|
|
|
|
setIsLoadingCompanies(true);
|
|
try {
|
|
// companyAPI.getList()는 Promise<Company[]>를 반환하므로 직접 사용
|
|
const companies = await companyAPI.getList();
|
|
console.log("📋 회사 목록 로드 성공:", companies);
|
|
setCompanies(companies);
|
|
} catch (error) {
|
|
console.error("❌ 회사 목록 로드 오류:", error);
|
|
displayAlert("회사 목록을 불러오는데 실패했습니다.", "error");
|
|
} finally {
|
|
setIsLoadingCompanies(false);
|
|
}
|
|
}, [isSuperAdmin, displayAlert]);
|
|
|
|
// 초기화
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
// 최고 관리자이고 생성 모드일 때만 회사 목록 로드
|
|
if (isSuperAdmin && !isEditMode) {
|
|
loadCompanies();
|
|
}
|
|
|
|
if (isEditMode && editingRole) {
|
|
// 수정 모드: 기존 데이터 로드
|
|
setFormData({
|
|
authName: editingRole.authName || "",
|
|
authCode: editingRole.authCode || "",
|
|
companyCode: editingRole.companyCode || "",
|
|
status: editingRole.status || "active",
|
|
});
|
|
} else {
|
|
// 생성 모드: 초기화
|
|
setFormData({
|
|
authName: "",
|
|
authCode: "",
|
|
companyCode: currentUser?.companyCode || "",
|
|
status: "active",
|
|
});
|
|
}
|
|
setShowAlert(false);
|
|
}
|
|
}, [isOpen, isEditMode, editingRole, currentUser?.companyCode, isSuperAdmin, loadCompanies]);
|
|
|
|
// 입력 핸들러
|
|
const handleInputChange = useCallback((field: string, value: string) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
}, []);
|
|
|
|
// 제출 핸들러
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!isFormValid) {
|
|
displayAlert("모든 필수 항목을 입력해주세요.", "error");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
let response;
|
|
|
|
if (isEditMode && editingRole) {
|
|
// 수정
|
|
response = await roleAPI.update(editingRole.objid, {
|
|
authName: formData.authName,
|
|
authCode: formData.authCode,
|
|
status: formData.status,
|
|
});
|
|
} else {
|
|
// 생성
|
|
response = await roleAPI.create({
|
|
authName: formData.authName,
|
|
authCode: formData.authCode,
|
|
companyCode: formData.companyCode,
|
|
});
|
|
}
|
|
|
|
if (response.success) {
|
|
displayAlert(isEditMode ? "권한 그룹이 수정되었습니다." : "권한 그룹이 생성되었습니다.", "success");
|
|
setTimeout(() => {
|
|
onClose();
|
|
onSuccess?.();
|
|
}, 1500);
|
|
} else {
|
|
displayAlert(response.message || "작업에 실패했습니다.", "error");
|
|
}
|
|
} catch (error) {
|
|
console.error("권한 그룹 저장 오류:", error);
|
|
displayAlert("권한 그룹 저장 중 오류가 발생했습니다.", "error");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [isFormValid, isEditMode, editingRole, formData, onClose, onSuccess, displayAlert]);
|
|
|
|
// Enter 키 핸들러
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && isFormValid && !isLoading) {
|
|
handleSubmit();
|
|
}
|
|
},
|
|
[isFormValid, isLoading, handleSubmit],
|
|
);
|
|
|
|
return (
|
|
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
|
<ResizableDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
|
<ResizableDialogHeader>
|
|
<ResizableDialogTitle className="text-base sm:text-lg">{isEditMode ? "권한 그룹 수정" : "권한 그룹 생성"}</ResizableDialogTitle>
|
|
</ResizableDialogHeader>
|
|
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{/* 권한 그룹명 */}
|
|
<div>
|
|
<Label htmlFor="authName" className="text-xs sm:text-sm">
|
|
권한 그룹명 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="authName"
|
|
value={formData.authName}
|
|
onChange={(e) => handleInputChange("authName", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="예: 영업팀 권한"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
{/* 권한 코드 */}
|
|
<div>
|
|
<Label htmlFor="authCode" className="text-xs sm:text-sm">
|
|
권한 코드 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="authCode"
|
|
value={formData.authCode}
|
|
onChange={(e) => handleInputChange("authCode", e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="예: SALES_TEAM (영문/숫자/언더스코어만)"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoading}
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
|
시스템 내부에서 사용되는 고유 코드입니다. 영문 대문자, 숫자, 언더스코어만 사용하세요.
|
|
</p>
|
|
</div>
|
|
|
|
{/* 회사 (수정 모드에서는 비활성화) */}
|
|
{isEditMode ? (
|
|
<div>
|
|
<Label htmlFor="companyCode" className="text-xs sm:text-sm">
|
|
회사
|
|
</Label>
|
|
<Input
|
|
id="companyCode"
|
|
value={formData.companyCode}
|
|
disabled
|
|
className="bg-muted h-8 cursor-not-allowed text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">회사 코드는 수정할 수 없습니다.</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<Label htmlFor="companyCode" className="text-xs sm:text-sm">
|
|
회사 <span className="text-red-500">*</span>
|
|
</Label>
|
|
{isSuperAdmin ? (
|
|
<>
|
|
<Popover open={companyComboOpen} onOpenChange={setCompanyComboOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={companyComboOpen}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoading || isLoadingCompanies}
|
|
>
|
|
{formData.companyCode
|
|
? companies.find((company) => company.company_code === formData.companyCode)?.company_name ||
|
|
formData.companyCode
|
|
: "회사 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="p-0"
|
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="회사 검색..." className="text-xs sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">
|
|
{isLoadingCompanies ? "로딩 중..." : "회사를 찾을 수 없습니다."}
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{companies.map((company) => (
|
|
<CommandItem
|
|
key={company.company_code}
|
|
value={`${company.company_code} ${company.company_name}`}
|
|
onSelect={() => {
|
|
handleInputChange("companyCode", company.company_code);
|
|
setCompanyComboOpen(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
formData.companyCode === company.company_code ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{company.company_name}</span>
|
|
<span className="text-muted-foreground text-[10px]">{company.company_code}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
|
모든 회사에 권한 그룹을 생성할 수 있습니다.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Input
|
|
id="companyCode"
|
|
value={formData.companyCode}
|
|
disabled
|
|
className="bg-muted h-8 cursor-not-allowed text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
|
자신의 회사에만 권한 그룹을 생성할 수 있습니다.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 상태 (수정 모드에서만 표시) */}
|
|
{isEditMode && (
|
|
<div>
|
|
<Label htmlFor="status" className="text-xs sm:text-sm">
|
|
상태
|
|
</Label>
|
|
<Select value={formData.status} onValueChange={(value) => handleInputChange("status", value)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="active">활성</SelectItem>
|
|
<SelectItem value="inactive">비활성</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
{/* 알림 메시지 */}
|
|
{showAlert && (
|
|
<div
|
|
className={`rounded-lg border p-3 text-sm ${
|
|
alertType === "success"
|
|
? "border-green-300 bg-green-50 text-green-800"
|
|
: alertType === "error"
|
|
? "border-destructive/50 bg-destructive/10 text-destructive"
|
|
: "border-blue-300 bg-blue-50 text-blue-800"
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
{alertType === "error" && <AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />}
|
|
<span className="text-xs sm:text-sm">{alertMessage}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={isLoading || !isFormValid}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{isLoading ? "처리중..." : isEditMode ? "수정" : "생성"}
|
|
</Button>
|
|
</ResizableDialogFooter>
|
|
</ResizableDialogContent>
|
|
</ResizableDialog>
|
|
);
|
|
}
|