ERP-node/frontend/components/admin/MenuCopyDialog.tsx

472 lines
17 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { menuApi, MenuCopyResult } from "@/lib/api/menu";
import { apiClient } from "@/lib/api/client";
interface MenuCopyDialogProps {
menuObjid: number | null;
menuName: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onCopyComplete?: () => void;
}
interface Company {
company_code: string;
company_name: string;
}
export function MenuCopyDialog({
menuObjid,
menuName,
open,
onOpenChange,
onCopyComplete,
}: MenuCopyDialogProps) {
const [targetCompanyCode, setTargetCompanyCode] = useState("");
const [companies, setCompanies] = useState<Company[]>([]);
const [copying, setCopying] = useState(false);
const [result, setResult] = useState<MenuCopyResult | null>(null);
const [loadingCompanies, setLoadingCompanies] = useState(false);
// 화면명 일괄 변경 설정
const [useBulkRename, setUseBulkRename] = useState(false);
const [removeText, setRemoveText] = useState("");
const [addPrefix, setAddPrefix] = useState("");
2025-12-18 10:55:26 +09:00
// 카테고리/코드 복사 옵션
const [copyCodeCategory, setCopyCodeCategory] = useState(false);
const [copyNumberingRules, setCopyNumberingRules] = useState(false);
const [copyCategoryMapping, setCopyCategoryMapping] = useState(false);
const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false);
2025-12-18 16:35:55 +09:00
const [copyCascadingRelation, setCopyCascadingRelation] = useState(false);
2025-12-18 10:55:26 +09:00
// 회사 목록 로드
useEffect(() => {
if (open) {
loadCompanies();
// 다이얼로그가 열릴 때마다 초기화
setTargetCompanyCode("");
setResult(null);
setUseBulkRename(false);
setRemoveText("");
setAddPrefix("");
2025-12-18 10:55:26 +09:00
setCopyCodeCategory(false);
setCopyNumberingRules(false);
setCopyCategoryMapping(false);
setCopyTableTypeColumns(false);
2025-12-18 16:35:55 +09:00
setCopyCascadingRelation(false);
}
}, [open]);
const loadCompanies = async () => {
try {
setLoadingCompanies(true);
const response = await apiClient.get("/admin/companies/db");
if (response.data.success && response.data.data) {
// 최고 관리자(*) 회사 제외
const filteredCompanies = response.data.data.filter(
(company: Company) => company.company_code !== "*"
);
setCompanies(filteredCompanies);
}
} catch (error) {
console.error("회사 목록 조회 실패:", error);
toast.error("회사 목록을 불러올 수 없습니다");
} finally {
setLoadingCompanies(false);
}
};
const handleCopy = async () => {
if (!menuObjid) {
toast.error("메뉴를 선택해주세요");
return;
}
if (!targetCompanyCode) {
toast.error("대상 회사를 선택해주세요");
return;
}
setCopying(true);
setResult(null);
try {
// 화면명 변환 설정 (사용 중일 때만 전달)
const screenNameConfig =
useBulkRename && (removeText.trim() || addPrefix.trim())
? {
removeText: removeText.trim() || undefined,
addPrefix: addPrefix.trim() || undefined,
}
: undefined;
2025-12-18 10:55:26 +09:00
// 추가 복사 옵션
const additionalCopyOptions = {
copyCodeCategory,
copyNumberingRules,
copyCategoryMapping,
copyTableTypeColumns,
2025-12-18 16:35:55 +09:00
copyCascadingRelation,
2025-12-18 10:55:26 +09:00
};
const response = await menuApi.copyMenu(
menuObjid,
targetCompanyCode,
2025-12-18 10:55:26 +09:00
screenNameConfig,
additionalCopyOptions
);
if (response.success && response.data) {
setResult(response.data);
toast.success("메뉴 복사 완료!");
// 경고 메시지 표시
if (response.data.warnings && response.data.warnings.length > 0) {
response.data.warnings.forEach((warning) => {
toast.warning(warning);
});
}
// 복사 완료 콜백
if (onCopyComplete) {
onCopyComplete();
}
} else {
toast.error(response.message || "메뉴 복사 실패");
}
} catch (error: any) {
console.error("메뉴 복사 오류:", error);
toast.error(error.message || "메뉴 복사 중 오류가 발생했습니다");
} finally {
setCopying(false);
}
};
const handleClose = () => {
if (!copying) {
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
"{menuName}" .
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 회사 선택 */}
{!result && (
<div>
<Label htmlFor="company" className="text-xs sm:text-sm">
*
</Label>
<Select
value={targetCompanyCode}
onValueChange={setTargetCompanyCode}
disabled={copying || loadingCompanies}
>
<SelectTrigger
id="company"
className="h-8 text-xs sm:h-10 sm:text-sm"
>
<SelectValue placeholder="회사 선택" />
</SelectTrigger>
<SelectContent>
{loadingCompanies ? (
<div className="flex items-center justify-center p-2 text-xs text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</div>
) : companies.length === 0 ? (
<div className="p-2 text-xs text-muted-foreground text-center">
</div>
) : (
companies.map((company) => (
<SelectItem
key={company.company_code}
value={company.company_code}
className="text-xs sm:text-sm"
>
{company.company_name} ({company.company_code})
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
)}
{/* 화면명 일괄 변경 설정 */}
{!result && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Checkbox
id="useBulkRename"
checked={useBulkRename}
onCheckedChange={(checked) => setUseBulkRename(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="useBulkRename"
className="text-xs sm:text-sm font-medium cursor-pointer"
>
</Label>
</div>
{useBulkRename && (
<div className="space-y-3 pl-6 border-l-2">
<div>
<Label htmlFor="removeText" className="text-xs sm:text-sm">
</Label>
<Input
id="removeText"
value={removeText}
onChange={(e) => setRemoveText(e.target.value)}
placeholder="예: 탑씰"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "탑씰 회사정보" "회사정보")
</p>
</div>
<div>
<Label htmlFor="addPrefix" className="text-xs sm:text-sm">
</Label>
<Input
id="addPrefix"
value={addPrefix}
onChange={(e) => setAddPrefix(e.target.value)}
placeholder="예: 한신"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "회사정보" "한신 회사정보")
</p>
</div>
</div>
)}
</div>
)}
2025-12-18 10:55:26 +09:00
{/* 추가 복사 옵션 */}
{!result && (
<div className="space-y-3">
<p className="text-xs font-medium"> ():</p>
<div className="space-y-2 pl-2 border-l-2">
<div className="flex items-center gap-2">
<Checkbox
id="copyCodeCategory"
checked={copyCodeCategory}
onCheckedChange={(checked) => setCopyCodeCategory(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="copyCodeCategory"
className="text-xs cursor-pointer"
>
+
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="copyNumberingRules"
checked={copyNumberingRules}
onCheckedChange={(checked) => setCopyNumberingRules(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="copyNumberingRules"
className="text-xs cursor-pointer"
>
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="copyCategoryMapping"
checked={copyCategoryMapping}
onCheckedChange={(checked) => setCopyCategoryMapping(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="copyCategoryMapping"
className="text-xs cursor-pointer"
>
+
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="copyTableTypeColumns"
checked={copyTableTypeColumns}
onCheckedChange={(checked) => setCopyTableTypeColumns(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="copyTableTypeColumns"
className="text-xs cursor-pointer"
>
</Label>
</div>
2025-12-18 16:35:55 +09:00
<div className="flex items-center gap-2">
<Checkbox
id="copyCascadingRelation"
checked={copyCascadingRelation}
onCheckedChange={(checked) => setCopyCascadingRelation(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="copyCascadingRelation"
className="text-xs cursor-pointer"
>
</Label>
</div>
2025-12-18 10:55:26 +09:00
</div>
</div>
)}
{/* 복사 항목 안내 */}
{!result && (
<div className="rounded-md border p-3 text-xs">
2025-12-18 10:55:26 +09:00
<p className="font-medium mb-2"> :</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li> ( )</li>
<li> + (, )</li>
<li> (, )</li>
</ul>
2025-12-18 10:55:26 +09:00
<p className="mt-2 text-muted-foreground">
* , , .
</p>
</div>
)}
{/* 복사 결과 */}
{result && (
<div className="rounded-md border border-success bg-success/10 p-3 text-xs space-y-2">
<p className="font-medium text-success"> !</p>
<div className="grid grid-cols-2 gap-2">
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedMenus}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedScreens}</span>
</div>
2025-12-18 10:55:26 +09:00
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedFlows}</span>
</div>
2025-12-18 10:55:26 +09:00
{(result.copiedCodeCategories ?? 0) > 0 && (
<div>
<span className="text-muted-foreground"> :</span>{" "}
<span className="font-medium">{result.copiedCodeCategories}</span>
</div>
)}
{(result.copiedCodes ?? 0) > 0 && (
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedCodes}</span>
</div>
)}
{(result.copiedNumberingRules ?? 0) > 0 && (
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedNumberingRules}</span>
</div>
)}
{(result.copiedCategoryMappings ?? 0) > 0 && (
<div>
<span className="text-muted-foreground"> :</span>{" "}
<span className="font-medium">{result.copiedCategoryMappings}</span>
</div>
)}
{(result.copiedTableTypeColumns ?? 0) > 0 && (
<div>
<span className="text-muted-foreground"> :</span>{" "}
<span className="font-medium">{result.copiedTableTypeColumns}</span>
2025-12-18 16:35:55 +09:00
</div>
)}
{(result.copiedCascadingRelations ?? 0) > 0 && (
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedCascadingRelations}</span>
2025-12-18 10:55:26 +09:00
</div>
)}
</div>
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={handleClose}
disabled={copying}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{result ? "닫기" : "취소"}
</Button>
{!result && (
<Button
onClick={handleCopy}
disabled={copying || !targetCompanyCode || loadingCompanies}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{copying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
"복사 시작"
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}