ERP-node/frontend/components/screen/CopyScreenModal.tsx

705 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle } from "lucide-react";
import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen";
import { apiClient } from "@/lib/api/client";
import { toast } from "sonner";
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;
}
interface CopyScreenModalProps {
isOpen: boolean;
onClose: () => void;
sourceScreen: ScreenDefinition | null;
onCopySuccess: () => void;
}
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]);
// 메인 화면 복사 정보
const [screenName, setScreenName] = useState("");
const [screenCode, setScreenCode] = useState("");
const [description, setDescription] = useState("");
// 대상 회사 선택 (최고 관리자 전용)
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("");
// 복사 중 상태
const [isCopying, setIsCopying] = useState(false);
// 최고 관리자인 경우 회사 목록 조회
useEffect(() => {
console.log("🔍 회사 목록 조회 체크:", { isSuperAdmin, isOpen });
if (isSuperAdmin && isOpen) {
console.log("✅ 회사 목록 조회 시작");
loadCompanies();
}
}, [isSuperAdmin, isOpen]);
// 모달이 열릴 때 초기값 설정 및 연결된 화면 감지
useEffect(() => {
console.log("🔍 모달 초기화:", { isOpen, sourceScreen, isSuperAdmin });
if (isOpen && sourceScreen) {
// 메인 화면 정보 설정
setScreenName(`${sourceScreen.screenName} (복사본)`);
setDescription(sourceScreen.description || "");
// 대상 회사 코드 설정
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(() => {
// 모달 화면들의 코드가 모두 설정되었는지 확인
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");
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);
}
};
// 화면 코드 자동 생성 (메인 + 모달 화면들) - 일괄 생성으로 중복 방지
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 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);
toast.success(
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개)`
);
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([]);
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Copy className="h-5 w-5" />
{linkedScreens.length > 0 && (
<span className="text-sm font-normal text-muted-foreground">
({linkedScreens.length} )
</span>
)}
</DialogTitle>
<DialogDescription>
{sourceScreen?.screenName} . .
</DialogDescription>
</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>
<div className="space-y-1 text-sm text-muted-foreground">
<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>
{/* 최고 관리자: 대상 회사 선택 */}
{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>
<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>
{/* 연결된 모달 화면 목록 */}
{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}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose} disabled={isCopying}>
</Button>
<Button onClick={handleCopy} disabled={isCopying || !targetCompanyCode}>
{isCopying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" />
{linkedScreens.length > 0 && ` (${linkedScreens.length + 1}개 화면)`}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}