561 lines
20 KiB
TypeScript
561 lines
20 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|