"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, Hash, Code, Table, Settings, Database } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { ScreenDefinition } from "@/types/screen"; import { screenApi, updateTabScreenReferences } 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 [copyNumberingRules, setCopyNumberingRules] = useState(false); // 추가 복사 옵션들 const [copyCodeCategory, setCopyCodeCategory] = useState(false); // 코드 카테고리 + 코드 복사 const [copyCategoryMapping, setCopyCategoryMapping] = useState(false); // 카테고리 매핑 + 값 복사 const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false); // 테이블 타입관리 입력타입 설정 복사 const [copyCascadingRelation, setCopyCascadingRelation] = useState(false); // 연쇄관계 설정 복사 // 복사 중 상태 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, target_company_code: finalCompanyCode, // 대상 회사 코드 전달 }); 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, sourceCompanyCode?: string): string => { // 루트 그룹은 사용자가 직접 입력한 이름 사용 if (isRootGroup) { return newGroupName.trim(); } // 일괄 이름 변경이 활성화된 경우 (찾기/대체 방식) if (useGroupBulkRename && groupFindText) { // 찾을 텍스트를 대체할 텍스트로 변경 return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText); } // 다른 회사로 복제하는 경우: 원본 이름 그대로 사용 (중복될 일 없음) if (sourceCompanyCode && sourceCompanyCode !== targetCompanyCode) { return originalName; } // 같은 회사 내 복제: "(복제)" 붙이기 (중복 방지) return `${originalName} (복제)`; }; // 재귀적 그룹 복제 함수 (하위 그룹 + 화면 전부 복제) const copyGroupRecursively = async ( sourceGroupData: ScreenGroup, parentGroupId: number | null, targetCompany: string, screenCodes: string[], // 미리 생성된 화면 코드 배열 codeIndex: { current: number }, // 현재 사용할 코드 인덱스 (참조로 전달) stats: { groups: number; screens: number }, totalScreenCount: number, // 전체 화면 수 (진행률 표시용) screenIdMap: { [key: number]: number } // 원본 화면 ID -> 새 화면 ID 매핑 ): Promise => { // 1. 현재 그룹 생성 (원본 display_order 유지) const timestamp = Date.now(); const randomSuffix = Math.floor(Math.random() * 1000); const newGroupCode = `${targetCompany}_GROUP_${timestamp}_${randomSuffix}`; const transformedGroupName = transformName(sourceGroupData.group_name, false, sourceGroupData.company_code); console.log(`📁 그룹 생성: ${transformedGroupName}`); const newGroupResponse = await createScreenGroup({ group_name: transformedGroupName, // 일괄 이름 변경 적용 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를 함께 매핑 // allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시) 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 screenName = typeof s === 'object' ? s.screen_name : ''; const tableName = typeof s === 'object' ? s.table_name : ''; // allScreens에서 먼저 찾고, 없으면 그룹의 screens 정보로 대체 let screenData = allScreens.find((sc) => sc.screenId === screenId); if (!screenData && screenId && screenName) { // allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성 screenData = { screenId: screenId, screenName: screenName, screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성) tableName: tableName || '', description: '', companyCode: sourceGroupData.company_code || '', } as any; } return { screenId, displayOrder, screenRole, screenData }; }).filter(item => item.screenData && item.screenId); // screenId가 유효한 것만 // 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}` }); const transformedScreenName = transformName(screen.screenName, false, sourceGroupData.company_code); console.log(` 📄 화면 복제: ${screen.screenName} → ${transformedScreenName}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: targetCompany, mainScreen: { screenName: transformedScreenName, // 일괄 이름 변경 적용 screenCode: newScreenCode, description: screen.description || "", }, modalScreens: [], }); if (result.mainScreen?.screenId) { // 원본 화면 ID -> 새 화면 ID 매핑 기록 screenIdMap[screen.screenId] = result.mainScreen.screenId; await addScreenToGroup({ group_id: newGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // 원본 정렬순서 유지 target_company_code: targetCompany, // 대상 회사 코드 전달 }); stats.screens++; console.log(` ✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`); } } 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, screenIdMap // screenIdMap 전달 ); } } }; // 그룹 내 모든 화면 수 계산 (재귀적) 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 }; const screenIdMap: { [key: number]: number } = {}; // 원본 화면 ID -> 새 화면 ID 매핑 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, false, sourceGroup.company_code) : 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를 함께 매핑 // allScreens에서 못 찾으면 그룹의 screens 정보를 직접 사용 (다른 회사 폴더 복사 시) console.log(`🔍 루트 그룹 화면 매핑 시작: ${sourceScreensInfo.length}개 화면, allScreens: ${allScreens.length}개`); 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 screenName = typeof s === 'object' ? s.screen_name : ''; const tableName = typeof s === 'object' ? s.table_name : ''; // allScreens에서 먼저 찾고, 없으면 그룹의 screens 정보로 대체 let screenData = allScreens.find((sc) => sc.screenId === screenId); const foundInAllScreens = !!screenData; if (!screenData && screenId && screenName) { // allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성 console.log(` ⚠️ allScreens에서 못 찾음, 그룹 정보 사용: ${screenId} - ${screenName}`); screenData = { screenId: screenId, screenName: screenName, screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성) tableName: tableName || '', description: '', companyCode: sourceGroup.company_code || '', } as any; } else if (screenData) { console.log(` ✅ allScreens에서 찾음: ${screenId} - ${screenData.screenName}`); } else { console.log(` ❌ 화면 정보 없음: screenId=${screenId}, screenName=${screenName}`); } return { screenId, displayOrder, screenRole, screenData }; }).filter(item => item.screenData && item.screenId); // screenId가 유효한 것만 console.log(`🔍 매핑 완료: ${screensWithOrder.length}개 화면 복사 예정`); screensWithOrder.forEach(item => console.log(` - ${item.screenId}: ${item.screenData?.screenName}`)); // 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}` }); const transformedScreenName = transformName(screen.screenName, false, sourceGroup.company_code); console.log(`📄 화면 복제: ${screen.screenName} → ${transformedScreenName}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: finalCompanyCode, mainScreen: { screenName: transformedScreenName, // 일괄 이름 변경 적용 screenCode: newScreenCode, description: screen.description || "", }, modalScreens: [], }); if (result.mainScreen?.screenId) { // 원본 화면 ID -> 새 화면 ID 매핑 기록 screenIdMap[screen.screenId] = result.mainScreen.screenId; await addScreenToGroup({ group_id: newRootGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // 원본 정렬순서 유지 target_company_code: finalCompanyCode, // 대상 회사 코드 전달 }); stats.screens++; console.log(`✅ 화면 복제 완료: ${result.mainScreen.screenName} (${screen.screenId} → ${result.mainScreen.screenId})`); } } 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, screenIdMap // screenIdMap 전달 ); } } // 6. 탭 컴포넌트의 screenId 참조 일괄 업데이트 console.log("🔍 screenIdMap 상태:", screenIdMap, "키 개수:", Object.keys(screenIdMap).length); if (Object.keys(screenIdMap).length > 0) { console.log("🔗 탭 screenId 참조 업데이트 중...", screenIdMap); setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "탭 참조 업데이트 중..." }); const targetScreenIds = Object.values(screenIdMap); try { const updateResult = await updateTabScreenReferences(targetScreenIds, screenIdMap); console.log(`✅ 탭 screenId 참조 업데이트 완료: ${updateResult.updated}개 레이아웃`); } catch (tabUpdateError) { console.warn("탭 screenId 참조 업데이트 실패 (무시):", tabUpdateError); } } // 7. 채번규칙 복제 옵션이 선택된 경우 (복제 → 메뉴 동기화 → 채번규칙 복제) if (copyNumberingRules) { try { setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "메뉴 동기화 중..." }); console.log("📋 메뉴 동기화 시작 (채번규칙 복제 준비)..."); // 7-1. 메뉴 동기화 (화면 그룹 → 메뉴) const syncResponse = await apiClient.post("/screen-groups/sync/screen-to-menu", { targetCompanyCode: finalCompanyCode, }); if (syncResponse.data?.success) { console.log("✅ 메뉴 동기화 완료:", syncResponse.data.data); // 7-2. 채번규칙 복제 setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "채번규칙 복제 중..." }); console.log("📋 채번규칙 복제 시작..."); const numberingResponse = await apiClient.post("/numbering-rules/copy-for-company", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, }); if (numberingResponse.data?.success) { console.log("✅ 채번규칙 복제 완료:", numberingResponse.data.data); toast.success(`채번규칙 ${numberingResponse.data.data?.copiedCount || 0}개가 복제되었습니다.`); } else { console.warn("채번규칙 복제 실패:", numberingResponse.data?.error); toast.warning("채번규칙 복제에 실패했습니다. 수동으로 복제해주세요."); } // 7-3. 화면-메뉴 할당 복제 (screen_menu_assignments) setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "화면-메뉴 할당 복제 중..." }); console.log("📋 화면-메뉴 할당 복제 시작..."); const menuAssignResponse = await apiClient.post("/screen-management/copy-menu-assignments", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, screenIdMap, }); if (menuAssignResponse.data?.success) { console.log("✅ 화면-메뉴 할당 복제 완료:", menuAssignResponse.data.data); toast.success(`화면-메뉴 할당 ${menuAssignResponse.data.data?.copiedCount || 0}개가 복제되었습니다.`); } else { console.warn("화면-메뉴 할당 복제 실패:", menuAssignResponse.data?.error); } } else { console.warn("메뉴 동기화 실패:", syncResponse.data?.error); toast.warning("메뉴 동기화에 실패했습니다. 채번규칙이 복제되지 않았습니다."); } } catch (numberingError) { console.error("채번규칙 복제 중 오류:", numberingError); toast.warning("채번규칙 복제 중 오류가 발생했습니다."); } } // 8. 코드 카테고리 + 코드 복제 if (copyCodeCategory) { try { setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "코드 카테고리/코드 복제 중..." }); console.log("📋 코드 카테고리/코드 복제 시작..."); const response = await apiClient.post("/screen-management/copy-code-category", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, }); if (response.data?.success) { console.log("✅ 코드 카테고리/코드 복제 완료:", response.data.data); toast.success(`코드 카테고리 ${response.data.data?.copiedCategories || 0}개, 코드 ${response.data.data?.copiedCodes || 0}개가 복제되었습니다.`); } else { console.warn("코드 카테고리/코드 복제 실패:", response.data?.error); toast.warning("코드 카테고리/코드 복제에 실패했습니다."); } } catch (error) { console.error("코드 카테고리/코드 복제 중 오류:", error); toast.warning("코드 카테고리/코드 복제 중 오류가 발생했습니다."); } } // 9. 카테고리 매핑 + 값 복제 if (copyCategoryMapping) { try { setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "카테고리 매핑/값 복제 중..." }); console.log("📋 카테고리 매핑/값 복제 시작..."); const response = await apiClient.post("/screen-management/copy-category-mapping", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, }); if (response.data?.success) { console.log("✅ 카테고리 매핑/값 복제 완료:", response.data.data); toast.success(`카테고리 매핑 ${response.data.data?.copiedMappings || 0}개, 값 ${response.data.data?.copiedValues || 0}개가 복제되었습니다.`); } else { console.warn("카테고리 매핑/값 복제 실패:", response.data?.error); toast.warning("카테고리 매핑/값 복제에 실패했습니다."); } } catch (error) { console.error("카테고리 매핑/값 복제 중 오류:", error); toast.warning("카테고리 매핑/값 복제 중 오류가 발생했습니다."); } } // 10. 테이블 타입관리 입력타입 설정 복제 if (copyTableTypeColumns) { try { setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "테이블 타입 컬럼 복제 중..." }); console.log("📋 테이블 타입 컬럼 복제 시작..."); const response = await apiClient.post("/screen-management/copy-table-type-columns", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, }); if (response.data?.success) { console.log("✅ 테이블 타입 컬럼 복제 완료:", response.data.data); toast.success(`테이블 타입 컬럼 ${response.data.data?.copiedCount || 0}개가 복제되었습니다.`); } else { console.warn("테이블 타입 컬럼 복제 실패:", response.data?.error); toast.warning("테이블 타입 컬럼 복제에 실패했습니다."); } } catch (error) { console.error("테이블 타입 컬럼 복제 중 오류:", error); toast.warning("테이블 타입 컬럼 복제 중 오류가 발생했습니다."); } } // 11. 연쇄관계 설정 복제 if (copyCascadingRelation) { try { setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "연쇄관계 설정 복제 중..." }); console.log("📋 연쇄관계 설정 복제 시작..."); const response = await apiClient.post("/screen-management/copy-cascading-relation", { sourceCompanyCode: sourceGroup.company_code, targetCompanyCode: finalCompanyCode, }); if (response.data?.success) { console.log("✅ 연쇄관계 설정 복제 완료:", response.data.data); toast.success(`연쇄관계 설정 ${response.data.data?.copiedCount || 0}개가 복제되었습니다.`); } else { console.warn("연쇄관계 설정 복제 실패:", response.data?.error); toast.warning("연쇄관계 설정 복제에 실패했습니다."); } } catch (error) { console.error("연쇄관계 설정 복제 중 오류:", error); toast.warning("연쇄관계 설정 복제 중 오류가 발생했습니다."); } } 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" && "현재 그룹의 화면만 복제하고 하위 그룹은 복제하지 않습니다"}

{/* 추가 복사 옵션 (선택사항) */}
{/* 코드 카테고리 + 코드 복사 */}
setCopyCodeCategory(checked === true)} />
{/* 채번규칙 복제 */}
setCopyNumberingRules(checked === true)} />
{/* 카테고리 매핑 + 값 복사 */}
setCopyCategoryMapping(checked === true)} />