ERP-node/frontend/components/pop/management/PopDeployModal.tsx

561 lines
20 KiB
TypeScript
Raw Normal View History

"use client";
import { 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 { Checkbox } from "@/components/ui/checkbox";
import { Loader2, Link2, Monitor, Folder, ChevronRight } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { GroupCopyInfo } from "./PopCategoryTree";
import { getCompanyList } from "@/lib/api/company";
import { ScreenDefinition } from "@/types/screen";
import { Company } from "@/types/company";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "sonner";
interface LinkedScreenInfo {
screenId: number;
screenName: string;
screenCode: string;
references: Array<{
componentId: string;
referenceType: string;
}>;
deploy: boolean;
newScreenName: string;
newScreenCode: string;
}
interface ScreenEntry {
screenId: number;
screenName: string;
newScreenName: string;
newScreenCode: string;
included: boolean;
}
interface PopDeployModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
screen: ScreenDefinition | null;
groupScreens?: ScreenDefinition[];
groupName?: string;
groupInfo?: GroupCopyInfo;
allScreens: ScreenDefinition[];
onDeployed?: () => void;
}
export function PopDeployModal({
open,
onOpenChange,
screen,
groupScreens,
groupName,
groupInfo,
allScreens,
onDeployed,
}: PopDeployModalProps) {
const isGroupMode = !!(groupScreens && groupScreens.length > 0);
const [companies, setCompanies] = useState<Company[]>([]);
const [targetCompanyCode, setTargetCompanyCode] = useState("");
// 단일 화면 모드
const [screenName, setScreenName] = useState("");
const [screenCode, setScreenCode] = useState("");
const [linkedScreens, setLinkedScreens] = useState<LinkedScreenInfo[]>([]);
// 그룹 모드
const [groupEntries, setGroupEntries] = useState<ScreenEntry[]>([]);
const [analyzing, setAnalyzing] = useState(false);
const [deploying, setDeploying] = useState(false);
// 회사 목록 로드
useEffect(() => {
if (open) {
getCompanyList({ status: "active" })
.then((list) => {
setCompanies(list.filter((c) => c.company_code !== "*"));
})
.catch(console.error);
}
}, [open]);
// 모달 열릴 때 초기화
useEffect(() => {
if (!open) return;
setTargetCompanyCode("");
setLinkedScreens([]);
if (isGroupMode && groupScreens) {
setGroupEntries(
groupScreens.map((s) => ({
screenId: s.screenId,
screenName: s.screenName,
newScreenName: s.screenName,
newScreenCode: "",
included: true,
})),
);
setScreenName("");
setScreenCode("");
} else if (screen) {
setScreenName(screen.screenName);
setScreenCode("");
setGroupEntries([]);
analyzeLinks(screen.screenId);
}
}, [open, screen, groupScreens, isGroupMode]);
// 회사 선택 시 화면 코드 자동 생성
useEffect(() => {
if (!targetCompanyCode) return;
if (isGroupMode) {
const count = groupEntries.filter((e) => e.included).length;
if (count > 0) {
screenApi
.generateMultipleScreenCodes(targetCompanyCode, count)
.then((codes) => {
let codeIdx = 0;
setGroupEntries((prev) =>
prev.map((e) =>
e.included
? { ...e, newScreenCode: codes[codeIdx++] || "" }
: e,
),
);
})
.catch(console.error);
}
} else {
const count = 1 + linkedScreens.filter((ls) => ls.deploy).length;
screenApi
.generateMultipleScreenCodes(targetCompanyCode, count)
.then((codes) => {
setScreenCode(codes[0] || "");
setLinkedScreens((prev) =>
prev.map((ls, idx) => ({
...ls,
newScreenCode: codes[idx + 1] || "",
})),
);
})
.catch(console.error);
}
}, [targetCompanyCode]);
const analyzeLinks = async (screenId: number) => {
setAnalyzing(true);
try {
const result = await screenApi.analyzePopScreenLinks(screenId);
const linked: LinkedScreenInfo[] = result.linkedScreenIds.map(
(linkedId) => {
const linkedScreen = allScreens.find(
(s) => s.screenId === linkedId,
);
const refs = result.references.filter(
(r) => r.targetScreenId === linkedId,
);
return {
screenId: linkedId,
screenName: linkedScreen?.screenName || `화면 ${linkedId}`,
screenCode: linkedScreen?.screenCode || "",
references: refs.map((r) => ({
componentId: r.componentId,
referenceType: r.referenceType,
})),
deploy: true,
newScreenName: linkedScreen?.screenName || `화면 ${linkedId}`,
newScreenCode: "",
};
},
);
setLinkedScreens(linked);
} catch (error) {
console.error("연결 분석 실패:", error);
} finally {
setAnalyzing(false);
}
};
const handleDeploy = async () => {
if (!targetCompanyCode) return;
setDeploying(true);
try {
let screensToSend: Array<{
sourceScreenId: number;
screenName: string;
screenCode: string;
}>;
if (isGroupMode) {
screensToSend = groupEntries
.filter((e) => e.included && e.newScreenCode)
.map((e) => ({
sourceScreenId: e.screenId,
screenName: e.newScreenName,
screenCode: e.newScreenCode,
}));
} else {
if (!screen || !screenName || !screenCode) return;
screensToSend = [
{
sourceScreenId: screen.screenId,
screenName,
screenCode,
},
...linkedScreens
.filter((ls) => ls.deploy)
.map((ls) => ({
sourceScreenId: ls.screenId,
screenName: ls.newScreenName,
screenCode: ls.newScreenCode,
})),
];
}
if (screensToSend.length === 0) {
toast.error("복사할 화면이 없습니다.");
return;
}
const deployPayload: Parameters<typeof screenApi.deployPopScreens>[0] = {
screens: screensToSend,
targetCompanyCode,
};
if (isGroupMode && groupInfo) {
deployPayload.groupStructure = groupInfo;
}
const result = await screenApi.deployPopScreens(deployPayload);
const groupMsg = result.createdGroups
? ` (카테고리 ${result.createdGroups}개 생성)`
: "";
toast.success(
`POP 화면 ${result.deployedScreens.length}개가 복사되었습니다.${groupMsg}`,
);
onOpenChange(false);
onDeployed?.();
} catch (error: any) {
toast.error(error?.response?.data?.message || "복사에 실패했습니다.");
} finally {
setDeploying(false);
}
};
const totalCount = isGroupMode
? groupEntries.filter((e) => e.included).length
: 1 + linkedScreens.filter((ls) => ls.deploy).length;
const canDeploy = isGroupMode
? !deploying && targetCompanyCode && groupEntries.some((e) => e.included)
: !deploying && targetCompanyCode && screenName && screenCode;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[560px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
POP
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{isGroupMode
? `"${groupName}" 카테고리의 화면 ${groupScreens!.length}개를 다른 회사로 복사합니다.`
: screen
? `"${screen.screenName}" (ID: ${screen.screenId}) 화면을 다른 회사로 복사합니다.`
: "화면을 선택해주세요."}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 대상 회사 선택 */}
<div>
<Label className="text-xs sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Select
value={targetCompanyCode}
onValueChange={setTargetCompanyCode}
>
<SelectTrigger className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="회사를 선택하세요" />
</SelectTrigger>
<SelectContent>
{companies.map((c) => (
<SelectItem
key={c.company_code}
value={c.company_code}
className="text-xs sm:text-sm"
>
{c.company_name} ({c.company_code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* ===== 그룹 모드: 카테고리 구조 + 화면 목록 ===== */}
{isGroupMode ? (
<div>
<Label className="text-xs sm:text-sm">
({groupEntries.filter((e) => e.included).length}
{groupInfo
? ` + ${1 + (groupInfo.children?.length || 0)}개 카테고리`
: ""}
)
</Label>
<div className="mt-1 max-h-[280px] overflow-y-auto rounded-md border p-2">
{groupInfo ? (
<div className="space-y-0.5">
{/* 메인 카테고리 */}
<div className="flex items-center gap-1.5 rounded bg-muted/50 p-1.5 text-xs font-medium">
<Folder className="h-3.5 w-3.5 shrink-0 text-amber-500" />
<span>{groupInfo.groupName}</span>
<ChevronRight className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">
</span>
</div>
{/* 메인 카테고리의 직접 화면 */}
{groupEntries
.filter((e) => groupInfo.screenIds.includes(e.screenId))
.map((entry) => (
<div
key={entry.screenId}
className="flex items-center gap-2 rounded p-1.5 pl-6 text-xs hover:bg-muted/50"
>
<Checkbox
checked={entry.included}
onCheckedChange={(checked) => {
setGroupEntries((prev) =>
prev.map((e) =>
e.screenId === entry.screenId
? { ...e, included: !!checked }
: e,
),
);
}}
/>
<Monitor className="h-3.5 w-3.5 shrink-0 text-blue-500" />
<span className="flex-1 truncate">
{entry.screenName}
</span>
<span className="shrink-0 text-muted-foreground">
#{entry.screenId}
</span>
</div>
))}
{/* 하위 카테고리들 */}
{groupInfo.children?.map((child) => (
<div key={child.sourceGroupId}>
<div className="mt-1 flex items-center gap-1.5 rounded bg-muted/30 p-1.5 pl-4 text-xs font-medium">
<Folder className="h-3.5 w-3.5 shrink-0 text-amber-400" />
<span>{child.groupName}</span>
</div>
{groupEntries
.filter((e) =>
child.screenIds.includes(e.screenId),
)
.map((entry) => (
<div
key={entry.screenId}
className="flex items-center gap-2 rounded p-1.5 pl-10 text-xs hover:bg-muted/50"
>
<Checkbox
checked={entry.included}
onCheckedChange={(checked) => {
setGroupEntries((prev) =>
prev.map((e) =>
e.screenId === entry.screenId
? { ...e, included: !!checked }
: e,
),
);
}}
/>
<Monitor className="h-3.5 w-3.5 shrink-0 text-blue-500" />
<span className="flex-1 truncate">
{entry.screenName}
</span>
<span className="shrink-0 text-muted-foreground">
#{entry.screenId}
</span>
</div>
))}
</div>
))}
</div>
) : (
<div className="space-y-1">
{groupEntries.map((entry) => (
<div
key={entry.screenId}
className="flex items-center gap-2 rounded p-1.5 text-xs hover:bg-muted/50"
>
<Checkbox
checked={entry.included}
onCheckedChange={(checked) => {
setGroupEntries((prev) =>
prev.map((e) =>
e.screenId === entry.screenId
? { ...e, included: !!checked }
: e,
),
);
}}
/>
<Monitor className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">
{entry.screenName}
</span>
<span className="shrink-0 text-muted-foreground">
#{entry.screenId}
</span>
</div>
))}
</div>
)}
</div>
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
(cartScreenId )
.
</p>
</div>
) : (
<>
{/* ===== 단일 모드: 화면명 + 코드 ===== */}
<div>
<Label className="text-xs sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Input
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
value={screenName}
onChange={(e) => setScreenName(e.target.value)}
placeholder="화면 이름"
/>
</div>
<div>
<Label className="text-xs sm:text-sm">
()
</Label>
<Input
className="mt-1 h-8 bg-muted text-xs sm:h-10 sm:text-sm"
value={screenCode}
readOnly
/>
</div>
{/* 연결 화면 감지 */}
{analyzing ? (
<div className="flex items-center gap-2 rounded-md border p-3 text-xs text-muted-foreground sm:text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : linkedScreens.length > 0 ? (
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-950/30">
<div className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-amber-800 dark:text-amber-300 sm:text-sm">
<Link2 className="h-3.5 w-3.5" />
POP {linkedScreens.length}
</div>
<div className="space-y-1.5">
{linkedScreens.map((ls) => (
<div
key={ls.screenId}
className="flex items-center justify-between rounded bg-background p-2 text-xs"
>
<div className="flex-1">
<div className="font-medium">{ls.screenName}</div>
<div className="text-muted-foreground">
ID: {ls.screenId} |{" "}
{ls.references
.map((r) => r.referenceType)
.join(", ")}
</div>
</div>
<div className="flex items-center gap-1.5">
<Checkbox
checked={ls.deploy}
onCheckedChange={(checked) => {
setLinkedScreens((prev) =>
prev.map((item) =>
item.screenId === ls.screenId
? { ...item, deploy: !!checked }
: item,
),
);
}}
/>
<span className="text-xs"> </span>
</div>
</div>
))}
</div>
<p className="mt-2 text-[10px] text-amber-700 dark:text-amber-400 sm:text-xs">
(cartScreenId ) ID로
.
</p>
</div>
) : (
!analyzing && (
<div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground sm:text-sm">
POP . .
</div>
)
)}
</>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
disabled={deploying}
>
</Button>
<Button
onClick={handleDeploy}
disabled={!canDeploy}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{deploying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
`${totalCount}개 화면 복사`
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}