- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
473 lines
17 KiB
TypeScript
473 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
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("");
|
|
|
|
// 카테고리/코드 복사 옵션
|
|
const [copyCodeCategory, setCopyCodeCategory] = useState(false);
|
|
const [copyNumberingRules, setCopyNumberingRules] = useState(false);
|
|
const [copyCategoryMapping, setCopyCategoryMapping] = useState(false);
|
|
const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false);
|
|
const [copyCascadingRelation, setCopyCascadingRelation] = useState(false);
|
|
|
|
// 회사 목록 로드
|
|
useEffect(() => {
|
|
if (open) {
|
|
loadCompanies();
|
|
// 다이얼로그가 열릴 때마다 초기화
|
|
setTargetCompanyCode("");
|
|
setResult(null);
|
|
setUseBulkRename(false);
|
|
setRemoveText("");
|
|
setAddPrefix("");
|
|
setCopyCodeCategory(false);
|
|
setCopyNumberingRules(false);
|
|
setCopyCategoryMapping(false);
|
|
setCopyTableTypeColumns(false);
|
|
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);
|
|
showErrorToast("회사 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." });
|
|
} 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;
|
|
|
|
// 추가 복사 옵션
|
|
const additionalCopyOptions = {
|
|
copyCodeCategory,
|
|
copyNumberingRules,
|
|
copyCategoryMapping,
|
|
copyTableTypeColumns,
|
|
copyCascadingRelation,
|
|
};
|
|
|
|
const response = await menuApi.copyMenu(
|
|
menuObjid,
|
|
targetCompanyCode,
|
|
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);
|
|
showErrorToast("메뉴 복사에 실패했습니다", error, { guidance: "복사 대상과 설정을 확인해 주세요." });
|
|
} 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>
|
|
)}
|
|
|
|
{/* 추가 복사 옵션 */}
|
|
{!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>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 복사 항목 안내 */}
|
|
{!result && (
|
|
<div className="rounded-md border p-3 text-xs">
|
|
<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>
|
|
<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>
|
|
<div>
|
|
<span className="text-muted-foreground">플로우:</span>{" "}
|
|
<span className="font-medium">{result.copiedFlows}개</span>
|
|
</div>
|
|
{(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>
|
|
</div>
|
|
)}
|
|
{(result.copiedCascadingRelations ?? 0) > 0 && (
|
|
<div>
|
|
<span className="text-muted-foreground">연쇄관계:</span>{" "}
|
|
<span className="font-medium">{result.copiedCascadingRelations}개</span>
|
|
</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>
|
|
);
|
|
}
|
|
|