"use client"; import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle, AlertTriangle, Check, ChevronsUpDown, FolderTree } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import { ScreenGroup, addScreenToGroup, createScreenGroup } from "@/lib/api/screenGroup"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; import { useAuth } from "@/hooks/useAuth"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { cn } from "@/lib/utils"; interface LinkedModalScreen { screenId: number; screenName: string; screenCode: string; newScreenName?: string; newScreenCode?: string; } interface CompanyInfo { companyCode: string; companyName: string; } interface CopyScreenModalProps { isOpen: boolean; onClose: () => void; sourceScreen: ScreenDefinition | null; onCopySuccess: () => void | Promise; // 트리 구조 지원용 추가 props mode?: "screen" | "group"; // 단일 화면 복제 또는 그룹 복제 sourceGroup?: ScreenGroup | null; // 그룹 복제 시 원본 그룹 groups?: ScreenGroup[]; // 대상 그룹 목록 targetGroupId?: number | null; // 초기 선택된 대상 그룹 allScreens?: ScreenDefinition[]; // 그룹 복제 시 사용할 전체 화면 목록 } export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopySuccess, mode = "screen", sourceGroup, groups = [], targetGroupId: initialTargetGroupId, allScreens = [], }: CopyScreenModalProps) { const { user } = useAuth(); // 최고 관리자 판별: userType이 "SUPER_ADMIN" 또는 companyCode가 "*" const isSuperAdmin = user?.userType === "SUPER_ADMIN" || user?.companyCode === "*"; // 디버깅: 사용자 정보 확인 useEffect(() => { console.log("🔍 CopyScreenModal - User Info:", { user, isSuperAdmin, userType: user?.userType, companyCode: user?.companyCode, 조건1: user?.userType === "SUPER_ADMIN", 조건2: user?.companyCode === "*", 최종판별: user?.userType === "SUPER_ADMIN" || user?.companyCode === "*", }); }, [user, isSuperAdmin]); // 메인 화면 복사 정보 const [screenName, setScreenName] = useState(""); const [screenCode, setScreenCode] = useState(""); const [description, setDescription] = useState(""); // 대상 그룹 선택 (트리 구조용) const [selectedTargetGroupId, setSelectedTargetGroupId] = useState(null); const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false); // 그룹 복제용 상태 const [newGroupName, setNewGroupName] = useState(""); const [groupParentId, setGroupParentId] = useState(null); const [isParentGroupSelectOpen, setIsParentGroupSelectOpen] = useState(false); const [groupDisplayOrder, setGroupDisplayOrder] = useState(0); // 그룹 일괄 이름 변경 (고급 옵션) - 찾기/대체 방식 const [useGroupBulkRename, setUseGroupBulkRename] = useState(false); const [groupFindText, setGroupFindText] = useState(""); // 찾을 텍스트 const [groupReplaceText, setGroupReplaceText] = useState(""); // 대체할 텍스트 // 대상 회사 선택 (최고 관리자 전용) const [targetCompanyCode, setTargetCompanyCode] = useState(""); const [companies, setCompanies] = useState([]); const [loadingCompanies, setLoadingCompanies] = useState(false); // 연결된 모달 화면들 const [linkedScreens, setLinkedScreens] = useState([]); const [loadingLinkedScreens, setLoadingLinkedScreens] = useState(false); // 화면명 일괄 수정 기능 const [useBulkRename, setUseBulkRename] = useState(false); const [removeText, setRemoveText] = useState(""); const [addPrefix, setAddPrefix] = useState(""); // 그룹 복제 모드: "all" (전체), "folder_only" (폴더만), "screen_only" (화면만) const [groupCopyMode, setGroupCopyMode] = useState<"all" | "folder_only" | "screen_only">("all"); // 복사 중 상태 const [isCopying, setIsCopying] = useState(false); const [copyProgress, setCopyProgress] = useState({ current: 0, total: 0, message: "" }); // 최고 관리자인 경우 회사 목록 조회 useEffect(() => { console.log("🔍 회사 목록 조회 체크:", { isSuperAdmin, isOpen }); if (isSuperAdmin && isOpen) { console.log("✅ 회사 목록 조회 시작"); loadCompanies(); } }, [isSuperAdmin, isOpen]); // 모달이 열릴 때 초기값 설정 및 연결된 화면 감지 useEffect(() => { console.log("🔍 모달 초기화:", { isOpen, sourceScreen, sourceGroup, mode, isSuperAdmin }); if (isOpen && mode === "screen" && sourceScreen) { // 단일 화면 복제 모드 setScreenName(`${sourceScreen.screenName} (복사본)`); setDescription(sourceScreen.description || ""); // 대상 회사 코드 설정 if (isSuperAdmin) { setTargetCompanyCode(sourceScreen.companyCode); } else { setTargetCompanyCode(sourceScreen.companyCode); } // 대상 그룹 초기화 (전달받은 값 또는 null) setSelectedTargetGroupId(initialTargetGroupId ?? null); // 연결된 모달 화면 감지 console.log("✅ 연결된 모달 화면 감지 시작"); detectLinkedModals(); } else if (isOpen && mode === "group" && sourceGroup) { // 그룹 복제 모드 // 1. 그룹명 중복 체크 - 같은 부모 그룹 내에 동일한 이름이 있는지 확인 const parentId = sourceGroup.parent_group_id; const siblingGroups = groups.filter(g => g.parent_group_id === parentId); const existingNames = siblingGroups.map(g => g.group_name); let newName = sourceGroup.group_name; if (existingNames.includes(newName)) { // 겹치는 이름이 있으면 "(복제)" 추가 newName = `${sourceGroup.group_name} (복제)`; // "(복제)"도 겹치면 숫자 추가 let copyNum = 2; while (existingNames.includes(newName)) { newName = `${sourceGroup.group_name} (복제 ${copyNum})`; copyNum++; } } setNewGroupName(newName); setGroupParentId(sourceGroup.parent_group_id ?? null); // 2. 상위 그룹의 회사 코드로 대상 회사 자동 설정 let autoCompanyCode = sourceGroup.company_code || ""; if (sourceGroup.parent_group_id) { const parentGroup = groups.find(g => g.id === sourceGroup.parent_group_id); if (parentGroup?.company_code) { autoCompanyCode = parentGroup.company_code; } } setTargetCompanyCode(autoCompanyCode); setGroupDisplayOrder(sourceGroup.display_order ?? 0); setUseGroupBulkRename(false); setGroupFindText(""); setGroupReplaceText(""); setGroupCopyMode("all"); } }, [isOpen, sourceScreen, sourceGroup, mode, isSuperAdmin, initialTargetGroupId]); // 일괄 변경 설정이 변경될 때 화면명 자동 업데이트 useEffect(() => { if (!sourceScreen) return; if (useBulkRename) { // 일괄 수정 사용 시: (복사본) 텍스트 제거 const newMainName = applyBulkRename(sourceScreen.screenName); setScreenName(newMainName); // 모달 화면명 업데이트 setLinkedScreens((prev) => prev.map((screen) => ({ ...screen, newScreenName: applyBulkRename(screen.screenName), })) ); } else { // 일괄 수정 미사용 시: (복사본) 텍스트 추가 setScreenName(`${sourceScreen.screenName} (복사본)`); setLinkedScreens((prev) => prev.map((screen) => ({ ...screen, newScreenName: screen.screenName, })) ); } }, [useBulkRename, removeText, addPrefix]); // 대상 회사 변경 시 기존 코드 초기화 useEffect(() => { if (targetCompanyCode) { console.log("🔄 회사 변경 → 기존 코드 초기화:", targetCompanyCode); setScreenCode(""); // 모달 화면들의 코드도 초기화 setLinkedScreens((prev) => prev.map((screen) => ({ ...screen, newScreenCode: undefined })) ); } }, [targetCompanyCode]); // linkedScreens 로딩이 완료되면 화면 코드 생성 useEffect(() => { // 모달 화면들의 코드가 모두 설정되었는지 확인 const allModalCodesSet = linkedScreens.length === 0 || linkedScreens.every(screen => screen.newScreenCode); console.log("🔍 코드 생성 조건 체크:", { targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreensCount: linkedScreens.length, allModalCodesSet, }); // 조건: 회사 코드가 있고, 로딩이 완료되고, (메인 코드가 없거나 모달 코드가 없을 때) const needsCodeGeneration = targetCompanyCode && !loadingLinkedScreens && (!screenCode || (linkedScreens.length > 0 && !allModalCodesSet)); if (needsCodeGeneration) { console.log("✅ 화면 코드 생성 시작 (linkedScreens 개수:", linkedScreens.length, ")"); generateScreenCodes(); } }, [targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreens]); // 회사 목록 조회 const loadCompanies = async () => { try { setLoadingCompanies(true); const response = await apiClient.get("/admin/companies"); console.log("📋 회사 목록 API 응답:", response.data); const data = response.data.data || response.data || []; console.log("📋 회사 목록 데이터:", data); const mappedCompanies = data.map((c: any) => ({ companyCode: c.company_code || c.companyCode, companyName: c.company_name || c.companyName, })); console.log("📋 매핑된 회사 목록:", mappedCompanies); setCompanies(mappedCompanies); } catch (error) { console.error("회사 목록 조회 실패:", error); toast.error("회사 목록을 불러오는데 실패했습니다."); } finally { setLoadingCompanies(false); } }; // 연결된 모달 화면 감지 const detectLinkedModals = async () => { if (!sourceScreen) return; try { setLoadingLinkedScreens(true); console.log("📡 API 호출: detectLinkedModals", sourceScreen.screenId); const linked = await screenApi.detectLinkedModals(sourceScreen.screenId); console.log("✅ 연결된 모달 화면 감지 결과:", linked); // 초기 newScreenName 설정 setLinkedScreens( linked.map((screen) => ({ ...screen, newScreenName: `${screen.screenName} (복사본)`, })) ); if (linked.length > 0) { toast.info(`${linked.length}개의 연결된 모달 화면을 감지했습니다.`); console.log("🎉 감지된 화면들:", linked); } else { console.log("ℹ️ 연결된 모달 화면이 없습니다."); } } catch (error) { console.error("❌ 연결된 화면 감지 실패:", error); // 에러가 나도 진행 가능하도록 무시 } finally { setLoadingLinkedScreens(false); } }; // 화면 코드 자동 생성 (메인 + 모달 화면들) - 일괄 생성으로 중복 방지 const generateScreenCodes = async () => { if (!targetCompanyCode) { console.log("❌ targetCompanyCode가 없어서 화면 코드 생성 중단"); return; } try { // 메인 화면 1개 + 연결된 모달 화면들 = 총 개수 const totalCount = 1 + linkedScreens.length; console.log(`📡 화면 코드 일괄 생성 API 호출: ${targetCompanyCode}, 개수: ${totalCount}`); // 한 번에 모든 코드 생성 (중복 방지) const generatedCodes = await screenApi.generateMultipleScreenCodes( targetCompanyCode, totalCount ); console.log("✅ 생성된 화면 코드들:", generatedCodes); // 첫 번째 코드는 메인 화면용 setScreenCode(generatedCodes[0]); console.log("✅ 메인 화면 코드:", generatedCodes[0]); // 나머지 코드들은 모달 화면들에 순서대로 할당 if (linkedScreens.length > 0) { const updatedLinkedScreens = linkedScreens.map((screen, index) => ({ ...screen, newScreenCode: generatedCodes[index + 1], // 1번째부터 시작 })); setLinkedScreens(updatedLinkedScreens); console.log("✅ 모달 화면 코드 할당 완료:", updatedLinkedScreens.map(s => ({ name: s.screenName, code: s.newScreenCode }))); } } catch (error) { console.error("❌ 화면 코드 일괄 생성 실패:", error); toast.error("화면 코드 생성에 실패했습니다."); } }; // 연결된 화면 이름 변경 const updateLinkedScreenName = (screenId: number, newName: string) => { setLinkedScreens((prev) => prev.map((screen) => screen.screenId === screenId ? { ...screen, newScreenName: newName } : screen ) ); }; // 연결된 화면 제거 (복사하지 않음) const removeLinkedScreen = (screenId: number) => { setLinkedScreens((prev) => prev.filter((screen) => screen.screenId !== screenId)); }; // 화면명 일괄 변경 적용 const applyBulkRename = (originalName: string): string => { if (!useBulkRename) return originalName; let newName = originalName; // 1. 제거할 텍스트 제거 if (removeText.trim()) { newName = newName.replace(new RegExp(removeText.trim(), "g"), ""); newName = newName.trim(); // 앞뒤 공백 제거 } // 2. 접두사 추가 if (addPrefix.trim()) { newName = addPrefix.trim() + " " + newName; } return newName; }; // 미리보기: 변경될 화면명들 const getPreviewNames = () => { if (!sourceScreen || !useBulkRename) return null; return { main: { original: sourceScreen.screenName, preview: applyBulkRename(sourceScreen.screenName), // (복사본) 없음 }, modals: linkedScreens.map((screen) => ({ original: screen.screenName, preview: applyBulkRename(screen.screenName), })), }; }; // 그룹 경로 가져오기 const getGroupPath = (groupId: number | null): string => { if (groupId === null) return ""; const group = groups.find((g) => g.id === groupId); if (!group) return ""; const path: string[] = [group.group_name]; let currentParentId = group.parent_group_id; while (currentParentId) { const parent = groups.find((g) => g.id === currentParentId); if (parent) { path.unshift(parent.group_name); currentParentId = parent.parent_group_id; } else { break; } } return path.join(" > "); }; // 그룹 레벨 가져오기 (들여쓰기용) const getGroupLevel = (groupId: number | null): number => { if (groupId === null) return 0; const group = groups.find((g) => g.id === groupId); if (!group) return 0; let level = 0; let currentParentId = group.parent_group_id; while (currentParentId) { level++; const parent = groups.find((g) => g.id === currentParentId); if (parent) { currentParentId = parent.parent_group_id; } else { break; } } return level; }; // 그룹 정렬 (계층 구조 유지) const getSortedGroups = (): ScreenGroup[] => { if (!groups || groups.length === 0) return []; const result: ScreenGroup[] = []; const addChildren = (parentId: number | null | undefined) => { const children = groups.filter((g) => (g.parent_group_id === parentId) || (parentId === null && !g.parent_group_id) ); for (const child of children) { result.push(child); addChildren(child.id); } }; addChildren(null); // 정렬 결과가 비어있으면 원본 그룹 반환 (parent_group_id가 없는 경우) return result.length > 0 ? result : groups; }; // 고유한 화면코드 생성 (중복 시 _COPY 추가) const generateUniqueScreenCode = (baseCode: string, existingCodes: Set): string => { let newCode = `${baseCode}_COPY`; while (existingCodes.has(newCode)) { newCode = `${newCode}_COPY`; } return newCode; }; // 화면 복사 실행 const handleCopy = async () => { if (!sourceScreen) return; // 입력값 검증 if (!screenName.trim()) { toast.error("화면명을 입력해주세요."); return; } if (!screenCode.trim()) { toast.error("화면 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요."); return; } // 연결된 화면들의 이름 검증 for (const linked of linkedScreens) { if (!linked.newScreenName?.trim()) { toast.error(`"${linked.screenName}" 모달 화면의 새 이름을 입력해주세요.`); return; } if (!linked.newScreenCode?.trim()) { toast.error(`"${linked.screenName}" 모달 화면의 코드가 생성되지 않았습니다.`); return; } } try { setIsCopying(true); // 화면명 중복 체크 const companyCode = targetCompanyCode || sourceScreen.companyCode; // 메인 화면명 중복 체크 const isMainDuplicate = await screenApi.checkDuplicateScreenName( companyCode, screenName.trim() ); if (isMainDuplicate) { toast.error(`"${screenName}" 화면명이 이미 존재합니다. 다른 이름을 입력해주세요.`); setIsCopying(false); return; } // 모달 화면명 중복 체크 for (const linked of linkedScreens) { const isModalDuplicate = await screenApi.checkDuplicateScreenName( companyCode, linked.newScreenName!.trim() ); if (isModalDuplicate) { toast.error( `"${linked.newScreenName}" 화면명이 이미 존재합니다. 모달 화면의 이름을 변경해주세요.` ); setIsCopying(false); return; } } // 메인 화면 + 모달 화면들 일괄 복사 const result = await screenApi.copyScreenWithModals(sourceScreen.screenId, { targetCompanyCode: targetCompanyCode || undefined, // 최고 관리자: 대상 회사 전달 mainScreen: { screenName: screenName.trim(), screenCode: screenCode.trim(), description: description.trim(), }, modalScreens: linkedScreens.map((screen) => ({ sourceScreenId: screen.screenId, screenName: screen.newScreenName!.trim(), screenCode: screen.newScreenCode!.trim(), })), }); console.log("✅ 복사 완료:", result); // 대상 그룹이 선택된 경우 복제된 메인 화면을 그룹에 추가 if (selectedTargetGroupId && result.mainScreen?.screenId) { try { await addScreenToGroup({ group_id: selectedTargetGroupId, screen_id: result.mainScreen.screenId, screen_role: "MAIN", display_order: 1, }); console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`); } catch (groupError) { console.error("그룹에 화면 추가 실패:", groupError); // 그룹 추가 실패해도 복제는 성공했으므로 계속 진행 } } toast.success( `화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개)` ); // 새로고침 완료 후 모달 닫기 await onCopySuccess(); handleClose(); } catch (error: any) { console.error("화면 복사 실패:", error); const errorMessage = error.response?.data?.message || error.message || "화면 복사에 실패했습니다."; toast.error(errorMessage); } finally { setIsCopying(false); } }; // 이름 변환 헬퍼 함수 (일괄 이름 변경 적용) const transformName = (originalName: string, isRootGroup: boolean = false): string => { // 루트 그룹은 사용자가 직접 입력한 이름 사용 if (isRootGroup) { return newGroupName.trim(); } // 일괄 이름 변경이 활성화된 경우 (찾기/대체 방식) if (useGroupBulkRename && groupFindText) { // 찾을 텍스트를 대체할 텍스트로 변경 return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText); } // 기본: "(복제)" 붙이기 return `${originalName} (복제)`; }; // 재귀적 그룹 복제 함수 (하위 그룹 + 화면 전부 복제) const copyGroupRecursively = async ( sourceGroupData: ScreenGroup, parentGroupId: number | null, targetCompany: string, screenCodes: string[], // 미리 생성된 화면 코드 배열 codeIndex: { current: number }, // 현재 사용할 코드 인덱스 (참조로 전달) stats: { groups: number; screens: number }, totalScreenCount: number // 전체 화면 수 (진행률 표시용) ): Promise => { // 1. 현재 그룹 생성 (원본 display_order 유지) const timestamp = Date.now(); const randomSuffix = Math.floor(Math.random() * 1000); const newGroupCode = `${targetCompany}_GROUP_${timestamp}_${randomSuffix}`; console.log(`📁 그룹 생성: ${sourceGroupData.group_name} (복제)`); const newGroupResponse = await createScreenGroup({ group_name: transformName(sourceGroupData.group_name), // 일괄 이름 변경 적용 group_code: newGroupCode, parent_group_id: parentGroupId, target_company_code: targetCompany, display_order: sourceGroupData.display_order, // 원본 정렬순서 유지 }); if (!newGroupResponse.success || !newGroupResponse.data) { throw new Error(newGroupResponse.error || `그룹 생성 실패: ${sourceGroupData.group_name}`); } const newGroup = newGroupResponse.data; stats.groups++; console.log(`✅ 그룹 생성 완료: ${newGroup.group_name} (id: ${newGroup.id})`); // 2. 현재 그룹의 화면들 복제 (원본 display_order 유지) - folder_only 모드가 아닌 경우만 if (groupCopyMode !== "folder_only") { const sourceScreensInfo = sourceGroupData.screens || []; // 화면 정보와 display_order를 함께 매핑 const screensWithOrder = sourceScreensInfo.map((s: any) => { const screenId = typeof s === 'object' ? s.screen_id : s; const displayOrder = typeof s === 'object' ? s.display_order : 0; const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN'; const screenData = allScreens.find((sc) => sc.screenId === screenId); return { screenId, displayOrder, screenRole, screenData }; }).filter(item => item.screenData); // 화면 데이터가 있는 것만 // display_order 순으로 정렬 screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)); for (const { screenData: screen, displayOrder, screenRole } of screensWithOrder) { try { // 미리 생성된 화면 코드 사용 const newScreenCode = screenCodes[codeIndex.current]; codeIndex.current++; // 진행률 업데이트 setCopyProgress({ current: stats.screens + 1, total: totalScreenCount, message: `화면 복제 중: ${screen.screenName}` }); console.log(` 📄 화면 복제: ${screen.screenName} → ${newScreenCode}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: targetCompany, mainScreen: { screenName: transformName(screen.screenName), // 일괄 이름 변경 적용 screenCode: newScreenCode, description: screen.description || "", }, modalScreens: [], }); if (result.mainScreen?.screenId) { await addScreenToGroup({ group_id: newGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // 원본 정렬순서 유지 }); stats.screens++; console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName}`); } } catch (screenError) { console.error(` ❌ 화면 복제 실패 (${screen.screenCode}):`, screenError); } } } // 3. 하위 그룹들 재귀 복제 (display_order 순으로 정렬) - screen_only 모드가 아닌 경우만 if (groupCopyMode !== "screen_only") { const childGroups = groups .filter(g => g.parent_group_id === sourceGroupData.id) .sort((a, b) => (a.display_order || 0) - (b.display_order || 0)); console.log(`📂 하위 그룹 ${childGroups.length}개 발견:`, childGroups.map(g => g.group_name)); for (const childGroup of childGroups) { await copyGroupRecursively( childGroup, newGroup.id, targetCompany, screenCodes, codeIndex, stats, totalScreenCount ); } } }; // 그룹 내 모든 화면 수 계산 (재귀적) const countAllScreensInGroup = (groupId: number): number => { const group = groups.find(g => g.id === groupId); if (!group) return 0; const directScreens = group.screens?.length || 0; const childGroups = groups.filter(g => g.parent_group_id === groupId); const childScreens = childGroups.reduce((sum, child) => sum + countAllScreensInGroup(child.id), 0); return directScreens + childScreens; }; // 그룹 복제 실행 const handleCopyGroup = async () => { if (!sourceGroup) return; if (!newGroupName.trim()) { toast.error("그룹명을 입력해주세요."); return; } // 최고 관리자인 경우 대상 회사 필수 if (isSuperAdmin && !targetCompanyCode) { toast.error("대상 회사를 선택해주세요."); return; } try { setIsCopying(true); setCopyProgress({ current: 0, total: 0, message: "복제 준비 중..." }); const finalCompanyCode = targetCompanyCode || sourceGroup.company_code; const stats = { groups: 0, screens: 0 }; console.log("🔄 그룹 복제 시작 (재귀적):", { sourceGroup: sourceGroup.group_name, targetCompany: finalCompanyCode, }); // 1. 복제할 전체 화면 수 계산 (folder_only 모드가 아닌 경우만) const totalScreenCount = groupCopyMode === "folder_only" ? 0 : countAllScreensInGroup(sourceGroup.id); setCopyProgress({ current: 0, total: totalScreenCount, message: "화면 코드 생성 중..." }); console.log(`📊 복제할 총 화면 수: ${totalScreenCount}개 (모드: ${groupCopyMode})`); // 2. 필요한 화면 코드들을 API로 미리 생성 (DB에서 고유 코드 보장) let screenCodes: string[] = []; if (totalScreenCount > 0) { console.log(`🔧 화면 코드 ${totalScreenCount}개 생성 중...`); screenCodes = await screenApi.generateMultipleScreenCodes(finalCompanyCode, totalScreenCount); console.log(`✅ 화면 코드 생성 완료:`, screenCodes); } const codeIndex = { current: 0 }; // 참조로 전달해서 재귀 호출간 공유 // 3. 루트 그룹 생성 (일괄 변경 활성화 시 transformName 적용) const timestamp = Date.now(); const newGroupCode = `${finalCompanyCode}_GROUP_${timestamp}`; // 일괄 이름 변경이 활성화된 경우 원본 이름에 변환 적용 const rootGroupName = useGroupBulkRename && groupFindText ? transformName(sourceGroup.group_name) : newGroupName.trim(); const newGroupResponse = await createScreenGroup({ group_name: rootGroupName, group_code: newGroupCode, parent_group_id: groupParentId, target_company_code: finalCompanyCode, display_order: groupDisplayOrder, // 사용자가 입력한 정렬 순서 }); if (!newGroupResponse.success || !newGroupResponse.data) { throw new Error(newGroupResponse.error || "그룹 생성 실패"); } const newRootGroup = newGroupResponse.data; stats.groups++; console.log("✅ 루트 그룹 생성 완료:", newRootGroup.group_name); // 4. 원본 그룹의 화면들 복제 (루트 레벨, 원본 display_order 유지) - folder_only 모드가 아닌 경우만 if (groupCopyMode !== "folder_only") { const sourceScreensInfo = sourceGroup.screens || []; // 화면 정보와 display_order를 함께 매핑 const screensWithOrder = sourceScreensInfo.map((s: any) => { const screenId = typeof s === 'object' ? s.screen_id : s; const displayOrder = typeof s === 'object' ? s.display_order : 0; const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN'; const screenData = allScreens.find((sc) => sc.screenId === screenId); return { screenId, displayOrder, screenRole, screenData }; }).filter(item => item.screenData); // display_order 순으로 정렬 screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)); for (const { screenData: screen, displayOrder, screenRole } of screensWithOrder) { try { // 미리 생성된 화면 코드 사용 const newScreenCode = screenCodes[codeIndex.current]; codeIndex.current++; // 진행률 업데이트 setCopyProgress({ current: stats.screens + 1, total: totalScreenCount, message: `화면 복제 중: ${screen.screenName}` }); console.log(`📄 화면 복제: ${screen.screenName} → ${newScreenCode}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: finalCompanyCode, mainScreen: { screenName: transformName(screen.screenName), // 일괄 이름 변경 적용 screenCode: newScreenCode, description: screen.description || "", }, modalScreens: [], }); if (result.mainScreen?.screenId) { await addScreenToGroup({ group_id: newRootGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // 원본 정렬순서 유지 }); stats.screens++; console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName}`); } } catch (screenError) { console.error(`화면 복제 실패 (${screen.screenCode}):`, screenError); } } } // 5. 하위 그룹들 재귀 복제 (display_order 순으로 정렬) - screen_only 모드가 아닌 경우만 if (groupCopyMode !== "screen_only") { const childGroups = groups .filter(g => g.parent_group_id === sourceGroup.id) .sort((a, b) => (a.display_order || 0) - (b.display_order || 0)); console.log(`📂 하위 그룹 ${childGroups.length}개 발견:`, childGroups.map(g => g.group_name)); for (const childGroup of childGroups) { await copyGroupRecursively( childGroup, newRootGroup.id, finalCompanyCode, screenCodes, codeIndex, stats, totalScreenCount ); } } toast.success( `그룹 복제가 완료되었습니다! (그룹 ${stats.groups}개 + 화면 ${stats.screens}개)` ); await onCopySuccess(); handleClose(); } catch (error: any) { console.error("그룹 복제 실패:", error); const errorMessage = error.response?.data?.message || "그룹 복제에 실패했습니다."; toast.error(errorMessage); } finally { setIsCopying(false); } }; // 모달 닫기 const handleClose = () => { setScreenName(""); setScreenCode(""); setDescription(""); setTargetCompanyCode(""); setLinkedScreens([]); setSelectedTargetGroupId(null); setNewGroupName(""); setGroupParentId(null); setGroupDisplayOrder(0); setUseGroupBulkRename(false); setGroupFindText(""); setGroupReplaceText(""); onClose(); }; // 그룹 복제 모드 렌더링 if (mode === "group") { return ( {/* 로딩 오버레이 */} {isCopying && (

{copyProgress.message}

{copyProgress.total > 0 && ( <>

{copyProgress.current} / {copyProgress.total} 화면

)}
)} 그룹 복제 "{sourceGroup?.group_name}" 그룹을 복제합니다. 그룹 내 모든 화면도 함께 복제됩니다.
{/* 대분류 경고 (최상위 그룹인 경우) */} {sourceGroup && !sourceGroup.parent_group_id && (

대분류 폴더 복제

이 폴더는 최상위(대분류) 폴더입니다. 복제 시 모든 하위 폴더와 화면이 함께 복제됩니다. 데이터 양이 많을 경우 복제에 시간이 소요될 수 있습니다.

)} {/* 원본 그룹 정보 */}

원본 그룹 정보

그룹명: {sourceGroup?.group_name}
정렬 순서: {sourceGroup?.display_order ?? 0}
직접 포함 화면: {sourceGroup?.screens?.length || 0}개
{(() => { // 하위 그룹 수 계산 const childGroupCount = groups.filter(g => g.parent_group_id === sourceGroup?.id).length; // 총 화면 수 계산 (현재 그룹 + 모든 하위 그룹) const totalScreenCount = sourceGroup ? countAllScreensInGroup(sourceGroup.id) : 0; return ( <>
하위 그룹: {childGroupCount}개
{childGroupCount > 0 && (
복제될 총 화면:{" "} {totalScreenCount}개
)} ); })()}
{/* 복제 모드 선택 */}
setGroupCopyMode(value as "all" | "folder_only" | "screen_only")} className="flex flex-wrap gap-4" >

{groupCopyMode === "all" && "하위 그룹과 모든 화면이 함께 복제됩니다"} {groupCopyMode === "folder_only" && "하위 그룹 구조만 복제하고 화면은 복제하지 않습니다"} {groupCopyMode === "screen_only" && "현재 그룹의 화면만 복제하고 하위 그룹은 복제하지 않습니다"}

{/* 새 그룹명 + 정렬 순서 */}
setNewGroupName(e.target.value)} placeholder="복제될 그룹의 이름을 입력하세요" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
{ const val = e.target.value; setGroupDisplayOrder(val === "" ? 0 : parseInt(val) || 0); }} onBlur={(e) => { // 빈 값이면 0으로 설정 if (e.target.value === "") { setGroupDisplayOrder(0); } }} className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />
{/* 상위 그룹 선택 */} {groups.length > 0 && (
그룹을 찾을 수 없습니다. { setGroupParentId(null); setIsParentGroupSelectOpen(false); // 최상위 선택 시 원본 그룹의 회사 코드 유지 if (sourceGroup?.company_code) { setTargetCompanyCode(sourceGroup.company_code); } }} className="text-xs sm:text-sm" > 최상위 그룹 (상위 없음) {getSortedGroups() .filter((g) => g.id !== sourceGroup?.id) .map((group) => ( { setGroupParentId(group.id); setIsParentGroupSelectOpen(false); // 선택한 상위 그룹의 회사 코드로 자동 설정 if (group.company_code) { setTargetCompanyCode(group.company_code); } }} className="text-xs sm:text-sm" > {group.group_name} ))}
)} {/* 대상 회사 선택 (최고 관리자 전용) */} {isSuperAdmin && companies.length > 0 && (

복제된 그룹과 화면이 이 회사에 생성됩니다

)} {/* 고급 옵션: 일괄 이름 변경 */}
고급 옵션: 이름 일괄 변경
setUseGroupBulkRename(e.target.checked)} className="h-4 w-4 rounded border-gray-300" />
{useGroupBulkRename && ( <>
setGroupFindText(e.target.value)} placeholder="예: 테스트" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />

모든 폴더/화면 이름에서 이 텍스트를 찾습니다

setGroupReplaceText(e.target.value)} placeholder="예: TEST" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" />

찾은 텍스트를 이 텍스트로 대체합니다

미리보기:
"{sourceGroup?.group_name}" → " {groupFindText ? (sourceGroup?.group_name || "").replace(new RegExp(groupFindText, "g"), groupReplaceText) : `${sourceGroup?.group_name} (복제)`} "
)}
); } // 화면 복제 모드 렌더링 return ( 화면 복제 "{sourceScreen?.screenName}" 화면을 복제합니다. {linkedScreens.length > 0 && ` (모달 ${linkedScreens.length}개 포함)`}
{/* 새 화면명 */}
setScreenName(e.target.value)} placeholder="복제될 화면 이름" className="mt-1" />
{/* 새 화면코드 (자동생성) */}
{/* 대상 그룹 선택 */} {groups.length > 0 && (
그룹 없음 { setSelectedTargetGroupId(null); setIsGroupSelectOpen(false); }} > 미분류 {getSortedGroups().map((group) => ( { setSelectedTargetGroupId(group.id); setIsGroupSelectOpen(false); }} > {group.group_name} ))}
)} {/* 최고 관리자: 대상 회사 선택 */} {isSuperAdmin && (
)} {/* 연결된 모달 화면 (있을 경우만) */} {linkedScreens.length > 0 && (
연결된 모달 ({linkedScreens.length}개)
{linkedScreens.map((linked) => (
updateLinkedScreenName(linked.screenId, e.target.value)} className="h-7 text-xs" placeholder="모달 화면명" />
))}
)} {/* 화면명 일괄 수정 (접히는 옵션) */}
고급 옵션
{ setRemoveText(e.target.value); setUseBulkRename(true); }} placeholder="예: 탑씰" className="mt-1 h-8 text-xs" />
{ setAddPrefix(e.target.value); setUseBulkRename(true); }} placeholder="예: 대진" className="mt-1 h-8 text-xs" />
{(removeText || addPrefix) && getPreviewNames() && (
미리보기: {getPreviewNames()?.main.preview}
)}
); }