2025-09-03 18:23:47 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
DialogContent,
|
|
|
|
|
|
DialogHeader,
|
2025-11-06 12:11:49 +09:00
|
|
|
|
DialogTitle,
|
|
|
|
|
|
DialogDescription,
|
|
|
|
|
|
DialogFooter,
|
|
|
|
|
|
} from "@/components/ui/dialog";
|
2025-09-03 18:23:47 +09:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
2025-11-13 12:17:10 +09:00
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
|
import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle } from "lucide-react";
|
2025-09-03 18:23:47 +09:00
|
|
|
|
import { ScreenDefinition } from "@/types/screen";
|
|
|
|
|
|
import { screenApi } from "@/lib/api/screen";
|
2025-11-13 12:17:10 +09:00
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-09-03 18:23:47 +09:00
|
|
|
|
import { toast } from "sonner";
|
2025-11-13 12:17:10 +09:00
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
|
|
|
|
|
|
|
|
|
|
interface LinkedModalScreen {
|
|
|
|
|
|
screenId: number;
|
|
|
|
|
|
screenName: string;
|
|
|
|
|
|
screenCode: string;
|
|
|
|
|
|
newScreenName?: string;
|
|
|
|
|
|
newScreenCode?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface CompanyInfo {
|
|
|
|
|
|
companyCode: string;
|
|
|
|
|
|
companyName: string;
|
|
|
|
|
|
}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
|
|
|
|
|
interface CopyScreenModalProps {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
sourceScreen: ScreenDefinition | null;
|
|
|
|
|
|
onCopySuccess: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
export default function CopyScreenModal({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
sourceScreen,
|
|
|
|
|
|
onCopySuccess,
|
|
|
|
|
|
}: 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]);
|
|
|
|
|
|
|
|
|
|
|
|
// 메인 화면 복사 정보
|
2025-09-03 18:23:47 +09:00
|
|
|
|
const [screenName, setScreenName] = useState("");
|
|
|
|
|
|
const [screenCode, setScreenCode] = useState("");
|
|
|
|
|
|
const [description, setDescription] = useState("");
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 대상 회사 선택 (최고 관리자 전용)
|
|
|
|
|
|
const [targetCompanyCode, setTargetCompanyCode] = useState<string>("");
|
|
|
|
|
|
const [companies, setCompanies] = useState<CompanyInfo[]>([]);
|
|
|
|
|
|
const [loadingCompanies, setLoadingCompanies] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 연결된 모달 화면들
|
|
|
|
|
|
const [linkedScreens, setLinkedScreens] = useState<LinkedModalScreen[]>([]);
|
|
|
|
|
|
const [loadingLinkedScreens, setLoadingLinkedScreens] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 화면명 일괄 수정 기능
|
|
|
|
|
|
const [useBulkRename, setUseBulkRename] = useState(false);
|
|
|
|
|
|
const [removeText, setRemoveText] = useState("");
|
|
|
|
|
|
const [addPrefix, setAddPrefix] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
// 복사 중 상태
|
2025-09-03 18:23:47 +09:00
|
|
|
|
const [isCopying, setIsCopying] = useState(false);
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 최고 관리자인 경우 회사 목록 조회
|
2025-09-03 18:23:47 +09:00
|
|
|
|
useEffect(() => {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.log("🔍 회사 목록 조회 체크:", { isSuperAdmin, isOpen });
|
|
|
|
|
|
if (isSuperAdmin && isOpen) {
|
|
|
|
|
|
console.log("✅ 회사 목록 조회 시작");
|
|
|
|
|
|
loadCompanies();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isSuperAdmin, isOpen]);
|
|
|
|
|
|
|
|
|
|
|
|
// 모달이 열릴 때 초기값 설정 및 연결된 화면 감지
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
console.log("🔍 모달 초기화:", { isOpen, sourceScreen, isSuperAdmin });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
if (isOpen && sourceScreen) {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 메인 화면 정보 설정
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setScreenName(`${sourceScreen.screenName} (복사본)`);
|
|
|
|
|
|
setDescription(sourceScreen.description || "");
|
2025-11-13 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
// 대상 회사 코드 설정
|
|
|
|
|
|
if (isSuperAdmin) {
|
|
|
|
|
|
setTargetCompanyCode(sourceScreen.companyCode); // 기본값: 원본과 같은 회사
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setTargetCompanyCode(sourceScreen.companyCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 연결된 모달 화면 감지
|
|
|
|
|
|
console.log("✅ 연결된 모달 화면 감지 시작");
|
|
|
|
|
|
detectLinkedModals();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isOpen, sourceScreen, isSuperAdmin]);
|
|
|
|
|
|
|
|
|
|
|
|
// 일괄 변경 설정이 변경될 때 화면명 자동 업데이트
|
|
|
|
|
|
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(() => {
|
2025-12-09 16:11:04 +09:00
|
|
|
|
// 모달 화면들의 코드가 모두 설정되었는지 확인
|
|
|
|
|
|
const allModalCodesSet = linkedScreens.length === 0 ||
|
|
|
|
|
|
linkedScreens.every(screen => screen.newScreenCode);
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.log("🔍 코드 생성 조건 체크:", {
|
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
loadingLinkedScreens,
|
|
|
|
|
|
screenCode,
|
|
|
|
|
|
linkedScreensCount: linkedScreens.length,
|
2025-12-09 16:11:04 +09:00
|
|
|
|
allModalCodesSet,
|
2025-11-13 12:17:10 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-09 16:11:04 +09:00
|
|
|
|
// 조건: 회사 코드가 있고, 로딩이 완료되고, (메인 코드가 없거나 모달 코드가 없을 때)
|
|
|
|
|
|
const needsCodeGeneration = targetCompanyCode &&
|
|
|
|
|
|
!loadingLinkedScreens &&
|
|
|
|
|
|
(!screenCode || (linkedScreens.length > 0 && !allModalCodesSet));
|
|
|
|
|
|
|
|
|
|
|
|
if (needsCodeGeneration) {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.log("✅ 화면 코드 생성 시작 (linkedScreens 개수:", linkedScreens.length, ")");
|
|
|
|
|
|
generateScreenCodes();
|
|
|
|
|
|
}
|
2025-12-09 16:11:04 +09:00
|
|
|
|
}, [targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreens]);
|
2025-11-13 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
// 회사 목록 조회
|
|
|
|
|
|
const loadCompanies = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoadingCompanies(true);
|
|
|
|
|
|
const response = await apiClient.get("/admin/companies");
|
|
|
|
|
|
const data = response.data.data || response.data || [];
|
|
|
|
|
|
setCompanies(data.map((c: any) => ({
|
|
|
|
|
|
companyCode: c.company_code || c.companyCode,
|
|
|
|
|
|
companyName: c.company_name || c.companyName,
|
|
|
|
|
|
})));
|
|
|
|
|
|
} 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);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
}
|
2025-11-13 12:17:10 +09:00
|
|
|
|
};
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 화면 코드 자동 생성 (메인 + 모달 화면들) - 일괄 생성으로 중복 방지
|
|
|
|
|
|
const generateScreenCodes = async () => {
|
|
|
|
|
|
if (!targetCompanyCode) {
|
|
|
|
|
|
console.log("❌ targetCompanyCode가 없어서 화면 코드 생성 중단");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 메인 화면 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
|
|
|
|
|
|
})));
|
|
|
|
|
|
}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
} catch (error) {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.error("❌ 화면 코드 일괄 생성 실패:", error);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
toast.error("화면 코드 생성에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 연결된 화면 이름 변경
|
|
|
|
|
|
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),
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 화면 복사 실행
|
|
|
|
|
|
const handleCopy = async () => {
|
|
|
|
|
|
if (!sourceScreen) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!screenName.trim()) {
|
|
|
|
|
|
toast.error("화면명을 입력해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenCode.trim()) {
|
|
|
|
|
|
toast.error("화면 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 연결된 화면들의 이름 검증
|
|
|
|
|
|
for (const linked of linkedScreens) {
|
|
|
|
|
|
if (!linked.newScreenName?.trim()) {
|
|
|
|
|
|
toast.error(`"${linked.screenName}" 모달 화면의 새 이름을 입력해주세요.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!linked.newScreenCode?.trim()) {
|
|
|
|
|
|
toast.error(`"${linked.screenName}" 모달 화면의 코드가 생성되지 않았습니다.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
try {
|
|
|
|
|
|
setIsCopying(true);
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
// 화면명 중복 체크
|
|
|
|
|
|
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(),
|
|
|
|
|
|
})),
|
2025-09-03 18:23:47 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.log("✅ 복사 완료:", result);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(
|
|
|
|
|
|
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개)`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
onCopySuccess();
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
} catch (error: any) {
|
2025-11-13 12:17:10 +09:00
|
|
|
|
console.error("화면 복사 실패:", error);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
const errorMessage = error.response?.data?.message || "화면 복사에 실패했습니다.";
|
|
|
|
|
|
toast.error(errorMessage);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsCopying(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 모달 닫기
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
setScreenName("");
|
|
|
|
|
|
setScreenCode("");
|
|
|
|
|
|
setDescription("");
|
2025-11-13 12:17:10 +09:00
|
|
|
|
setTargetCompanyCode("");
|
|
|
|
|
|
setLinkedScreens([]);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
onClose();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
2025-12-05 10:46:10 +09:00
|
|
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-hidden">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<DialogHeader>
|
2025-11-06 12:11:49 +09:00
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Copy className="h-5 w-5" />
|
|
|
|
|
|
화면 복사
|
2025-11-13 12:17:10 +09:00
|
|
|
|
{linkedScreens.length > 0 && (
|
|
|
|
|
|
<span className="text-sm font-normal text-muted-foreground">
|
|
|
|
|
|
({linkedScreens.length}개의 모달 화면 포함)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2025-11-06 12:11:49 +09:00
|
|
|
|
</DialogTitle>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<DialogDescription>
|
2025-11-13 12:17:10 +09:00
|
|
|
|
{sourceScreen?.screenName} 화면을 복사합니다. 화면 구성과 연결된 모달 화면도 함께 복사됩니다.
|
2025-11-06 12:11:49 +09:00
|
|
|
|
</DialogDescription>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* 원본 화면 정보 */}
|
|
|
|
|
|
<div className="rounded-md bg-gray-50 p-3">
|
|
|
|
|
|
<h4 className="mb-2 text-sm font-medium text-gray-700">원본 화면 정보</h4>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="space-y-1 text-sm text-muted-foreground">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<span className="font-medium">화면명:</span> {sourceScreen?.screenName}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="font-medium">화면코드:</span> {sourceScreen?.screenCode}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="font-medium">회사코드:</span> {sourceScreen?.companyCode}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
{/* 최고 관리자: 대상 회사 선택 */}
|
|
|
|
|
|
{isSuperAdmin && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="targetCompany">복사 대상 회사 * (최고 관리자 전용)</Label>
|
|
|
|
|
|
<Select value={targetCompanyCode} onValueChange={setTargetCompanyCode}>
|
|
|
|
|
|
<SelectTrigger id="targetCompany" className="mt-1">
|
|
|
|
|
|
<SelectValue placeholder="회사를 선택하세요" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{loadingCompanies ? (
|
|
|
|
|
|
<div className="p-2 text-center text-sm text-muted-foreground">
|
|
|
|
|
|
로딩 중...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
companies.map((company) => (
|
|
|
|
|
|
<SelectItem key={company.companyCode} value={company.companyCode}>
|
|
|
|
|
|
{company.companyName} ({company.companyCode})
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
선택한 회사로 화면이 복사됩니다. 원본과 다른 회사를 선택하면 회사 간 화면 복사가 가능합니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 화면명 일괄 수정 */}
|
|
|
|
|
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
|
|
|
|
|
<div className="mb-3 flex items-center gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
id="useBulkRename"
|
|
|
|
|
|
checked={useBulkRename}
|
|
|
|
|
|
onChange={(e) => setUseBulkRename(e.target.checked)}
|
|
|
|
|
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="useBulkRename" className="text-sm font-medium text-blue-900 cursor-pointer">
|
|
|
|
|
|
🔄 화면명 일괄 수정 (선택사항)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{useBulkRename && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="removeText" className="text-xs text-blue-900">
|
|
|
|
|
|
제거할 텍스트
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="removeText"
|
|
|
|
|
|
value={removeText}
|
|
|
|
|
|
onChange={(e) => setRemoveText(e.target.value)}
|
|
|
|
|
|
placeholder="예: 탑씰"
|
|
|
|
|
|
className="mt-1 bg-white"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="addPrefix" className="text-xs text-blue-900">
|
|
|
|
|
|
추가할 접두사
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="addPrefix"
|
|
|
|
|
|
value={addPrefix}
|
|
|
|
|
|
onChange={(e) => setAddPrefix(e.target.value)}
|
|
|
|
|
|
placeholder="예: 대진산업"
|
|
|
|
|
|
className="mt-1 bg-white"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 미리보기 */}
|
|
|
|
|
|
{(removeText || addPrefix) && getPreviewNames() && (
|
|
|
|
|
|
<div className="rounded-md border border-blue-300 bg-white p-3">
|
|
|
|
|
|
<p className="mb-2 text-xs font-medium text-blue-900">미리보기</p>
|
|
|
|
|
|
<div className="space-y-2 text-xs">
|
|
|
|
|
|
{/* 메인 화면 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
|
메인: <span className="line-through">{getPreviewNames()?.main.original}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="font-medium text-blue-700">
|
|
|
|
|
|
→ {getPreviewNames()?.main.preview}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* 모달 화면들 */}
|
|
|
|
|
|
{getPreviewNames()?.modals.map((modal, idx) => (
|
|
|
|
|
|
<div key={idx}>
|
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
|
모달: <span className="line-through">{modal.original}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="font-medium text-blue-700">→ {modal.preview}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<p className="text-xs text-blue-700">
|
|
|
|
|
|
💡 모든 화면명에서 "제거할 텍스트"를 삭제하고 "추가할 접두사"를 앞에 붙입니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 메인 화면 정보 입력 */}
|
|
|
|
|
|
<div className="space-y-3 rounded-lg border p-3">
|
|
|
|
|
|
<h4 className="text-sm font-medium">메인 화면 정보</h4>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="screenName">새 화면명 *</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="screenName"
|
|
|
|
|
|
value={screenName}
|
|
|
|
|
|
onChange={(e) => setScreenName(e.target.value)}
|
|
|
|
|
|
placeholder="복사될 화면의 이름을 입력하세요"
|
|
|
|
|
|
className="mt-1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="screenCode">새 화면코드 (자동생성)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="screenCode"
|
|
|
|
|
|
value={screenCode}
|
|
|
|
|
|
readOnly
|
|
|
|
|
|
className="mt-1 bg-gray-50"
|
|
|
|
|
|
placeholder="화면 코드가 자동으로 생성됩니다"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="description">설명</Label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
id="description"
|
|
|
|
|
|
value={description}
|
|
|
|
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
|
|
|
|
placeholder="화면 설명을 입력하세요 (선택사항)"
|
|
|
|
|
|
className="mt-1"
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-11-13 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 연결된 모달 화면 목록 */}
|
|
|
|
|
|
{loadingLinkedScreens ? (
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2 rounded-lg border p-6">
|
|
|
|
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">연결된 모달 화면 감지 중...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : linkedScreens.length > 0 ? (
|
|
|
|
|
|
<div className="space-y-3 rounded-lg border p-3">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h4 className="flex items-center gap-2 text-sm font-medium">
|
|
|
|
|
|
<LinkIcon className="h-4 w-4" />
|
|
|
|
|
|
연결된 모달 화면 ({linkedScreens.length}개)
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">함께 복사됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{linkedScreens.map((linkedScreen) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={linkedScreen.screenId}
|
|
|
|
|
|
className="space-y-2 rounded-md border bg-gray-50 p-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
|
<div className="flex-1 space-y-1">
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
원본: {linkedScreen.screenName} ({linkedScreen.screenCode})
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={linkedScreen.newScreenName || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateLinkedScreenName(linkedScreen.screenId, e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="복사될 모달 화면 이름"
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={linkedScreen.newScreenCode || ""}
|
|
|
|
|
|
readOnly
|
|
|
|
|
|
className="h-8 bg-white text-xs"
|
|
|
|
|
|
placeholder="코드 자동 생성"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
className="h-8 w-8 text-destructive hover:bg-destructive/10"
|
|
|
|
|
|
onClick={() => removeLinkedScreen(linkedScreen.screenId)}
|
|
|
|
|
|
title="이 화면은 복사하지 않음"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Alert>
|
|
|
|
|
|
<AlertCircle className="h-4 w-4" />
|
|
|
|
|
|
<AlertDescription className="text-xs">
|
|
|
|
|
|
모달 화면을 복사하지 않으려면 휴지통 아이콘을 클릭하세요. 버튼의 모달 연결이 자동으로
|
|
|
|
|
|
업데이트됩니다.
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-13 12:17:10 +09:00
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Button variant="outline" onClick={handleClose} disabled={isCopying}>
|
|
|
|
|
|
취소
|
|
|
|
|
|
</Button>
|
2025-11-13 12:17:10 +09:00
|
|
|
|
<Button onClick={handleCopy} disabled={isCopying || !targetCompanyCode}>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{isCopying ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
|
복사 중...
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
|
|
|
|
복사하기
|
2025-11-13 12:17:10 +09:00
|
|
|
|
{linkedScreens.length > 0 && ` (${linkedScreens.length + 1}개 화면)`}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
2025-11-06 12:11:49 +09:00
|
|
|
|
</DialogFooter>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|