- Updated the ButtonConfigPanel to fetch a larger set of screens by modifying the API call to include a size parameter. - Enhanced the V2ButtonConfigPanel with new state variables and effects for managing data transfer field mappings, including loading available tables and their columns. - Implemented multi-table mapping logic to support complex data transfer actions, improving the flexibility and usability of the component. - Added a dedicated section for field mapping in the UI, allowing users to configure data transfer settings more effectively. These updates aim to enhance the functionality and user experience of the button configuration panels within the ERP system, enabling better data management and transfer capabilities. Made-with: Cursor
692 lines
30 KiB
TypeScript
692 lines
30 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { ComponentData } from "@/types/screen";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
|
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
|
|
|
interface ButtonConfigPanelProps {
|
|
component: ComponentData;
|
|
onUpdateProperty: (path: string, value: any) => void;
|
|
}
|
|
|
|
interface ScreenOption {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
}
|
|
|
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
|
// 🔧 항상 최신 component에서 직접 참조
|
|
const config = component.componentConfig || {};
|
|
const currentAction = component.componentConfig?.action || {}; // 🔧 최신 action 참조
|
|
|
|
// 로컬 상태 관리 (실시간 입력 반영)
|
|
const [localInputs, setLocalInputs] = useState({
|
|
text: config.text !== undefined ? config.text : "버튼", // 🔧 빈 문자열 허용
|
|
modalTitle: config.action?.modalTitle || "",
|
|
editModalTitle: config.action?.editModalTitle || "",
|
|
editModalDescription: config.action?.editModalDescription || "",
|
|
targetUrl: config.action?.targetUrl || "",
|
|
});
|
|
|
|
const [localSelects, setLocalSelects] = useState({
|
|
variant: config.variant || "default",
|
|
size: config.size || "md", // 🔧 기본값을 "md"로 변경
|
|
actionType: config.action?.type, // 🔧 기본값 완전 제거 (undefined)
|
|
modalSize: config.action?.modalSize || "md",
|
|
editMode: config.action?.editMode || "modal",
|
|
});
|
|
|
|
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
|
const [screensLoading, setScreensLoading] = useState(false);
|
|
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
|
const [navScreenOpen, setNavScreenOpen] = useState(false);
|
|
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
|
const [navSearchTerm, setNavSearchTerm] = useState("");
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
useEffect(() => {
|
|
console.log("🔄 ButtonConfigPanel useEffect 실행:", {
|
|
componentId: component.id,
|
|
"config.action?.type": config.action?.type,
|
|
"localSelects.actionType (before)": localSelects.actionType,
|
|
fullAction: config.action,
|
|
"component.componentConfig.action": component.componentConfig?.action,
|
|
});
|
|
|
|
setLocalInputs({
|
|
text: config.text !== undefined ? config.text : "버튼", // 🔧 빈 문자열 허용
|
|
modalTitle: config.action?.modalTitle || "",
|
|
editModalTitle: config.action?.editModalTitle || "",
|
|
editModalDescription: config.action?.editModalDescription || "",
|
|
targetUrl: config.action?.targetUrl || "",
|
|
});
|
|
|
|
setLocalSelects((prev) => {
|
|
const newSelects = {
|
|
variant: config.variant || "default",
|
|
size: config.size || "md", // 🔧 기본값을 "md"로 변경
|
|
actionType: config.action?.type, // 🔧 기본값 완전 제거 (undefined)
|
|
modalSize: config.action?.modalSize || "md",
|
|
editMode: config.action?.editMode || "modal",
|
|
};
|
|
|
|
console.log("📝 setLocalSelects 호출:", {
|
|
"prev.actionType": prev.actionType,
|
|
"new.actionType": newSelects.actionType,
|
|
"config.action?.type": config.action?.type,
|
|
});
|
|
|
|
return newSelects;
|
|
});
|
|
}, [
|
|
component.id, // 🔧 컴포넌트 ID (다른 컴포넌트로 전환 시)
|
|
component.componentConfig?.action?.type, // 🔧 액션 타입 (액션 변경 시 즉시 반영)
|
|
component.componentConfig?.text, // 🔧 버튼 텍스트
|
|
component.componentConfig?.variant, // 🔧 버튼 스타일
|
|
component.componentConfig?.size, // 🔧 버튼 크기
|
|
]);
|
|
|
|
// 화면 목록 가져오기
|
|
useEffect(() => {
|
|
const fetchScreens = async () => {
|
|
try {
|
|
setScreensLoading(true);
|
|
const response = await apiClient.get("/screen-management/screens?size=1000");
|
|
|
|
if (response.data.success && Array.isArray(response.data.data)) {
|
|
const screenList = response.data.data.map((screen: any) => ({
|
|
id: screen.screenId,
|
|
name: screen.screenName,
|
|
description: screen.description,
|
|
}));
|
|
setScreens(screenList);
|
|
}
|
|
} catch (error) {
|
|
// console.error("❌ 화면 목록 로딩 실패:", error);
|
|
} finally {
|
|
setScreensLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchScreens();
|
|
}, []);
|
|
|
|
// 검색 필터링 함수
|
|
const filterScreens = (searchTerm: string) => {
|
|
if (!searchTerm.trim()) return screens;
|
|
return screens.filter(
|
|
(screen) =>
|
|
screen.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
);
|
|
};
|
|
|
|
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
|
component,
|
|
config,
|
|
action: config.action,
|
|
actionType: config.action?.type,
|
|
screensCount: screens.length,
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="button-text">버튼 텍스트</Label>
|
|
<Input
|
|
id="button-text"
|
|
value={localInputs.text}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, text: newValue }));
|
|
onUpdateProperty("componentConfig.text", newValue);
|
|
}}
|
|
placeholder="버튼 텍스트를 입력하세요"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="button-variant">버튼 스타일</Label>
|
|
<Select
|
|
value={localSelects.variant}
|
|
onValueChange={(value) => {
|
|
setLocalSelects((prev) => ({ ...prev, variant: value }));
|
|
onUpdateProperty("componentConfig.variant", value);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="버튼 스타일 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="primary">기본 (Primary)</SelectItem>
|
|
<SelectItem value="secondary">보조 (Secondary)</SelectItem>
|
|
<SelectItem value="danger">위험 (Danger)</SelectItem>
|
|
<SelectItem value="success">성공 (Success)</SelectItem>
|
|
<SelectItem value="outline">외곽선 (Outline)</SelectItem>
|
|
<SelectItem value="ghost">고스트 (Ghost)</SelectItem>
|
|
<SelectItem value="link">링크 (Link)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="button-size">버튼 글씨 크기</Label>
|
|
<Select
|
|
value={localSelects.size}
|
|
onValueChange={(value) => {
|
|
setLocalSelects((prev) => ({ ...prev, size: value }));
|
|
onUpdateProperty("componentConfig.size", value);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="버튼 글씨 크기 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
|
<SelectItem value="md">기본 (Default)</SelectItem>
|
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="button-action">버튼 액션</Label>
|
|
<Select
|
|
value={localSelects.actionType || undefined}
|
|
onValueChange={(value) => {
|
|
console.log("🔵 버튼 액션 변경 시작:", {
|
|
oldValue: localSelects.actionType,
|
|
newValue: value,
|
|
componentId: component.id,
|
|
"현재 component.componentConfig.action": component.componentConfig?.action,
|
|
});
|
|
|
|
// 로컬 상태 업데이트
|
|
setLocalSelects((prev) => {
|
|
console.log("📝 setLocalSelects (액션 변경):", {
|
|
"prev.actionType": prev.actionType,
|
|
"new.actionType": value,
|
|
});
|
|
return { ...prev, actionType: value };
|
|
});
|
|
|
|
// 🔧 개별 속성만 업데이트
|
|
onUpdateProperty("componentConfig.action.type", value);
|
|
|
|
// 액션에 따른 라벨 색상 자동 설정 (별도 호출)
|
|
if (value === "delete") {
|
|
onUpdateProperty("style.labelColor", "#ef4444");
|
|
} else {
|
|
onUpdateProperty("style.labelColor", "#212121");
|
|
}
|
|
|
|
console.log("✅ 버튼 액션 변경 완료");
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="버튼 액션 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="save">저장</SelectItem>
|
|
<SelectItem value="cancel">취소</SelectItem>
|
|
<SelectItem value="delete">삭제</SelectItem>
|
|
<SelectItem value="edit">수정</SelectItem>
|
|
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
|
<SelectItem value="add">추가</SelectItem>
|
|
<SelectItem value="search">검색</SelectItem>
|
|
<SelectItem value="reset">초기화</SelectItem>
|
|
<SelectItem value="submit">제출</SelectItem>
|
|
<SelectItem value="close">닫기</SelectItem>
|
|
<SelectItem value="modal">모달 열기</SelectItem>
|
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
|
<SelectItem value="control">제어 (조건 체크만)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 모달 열기 액션 설정 */}
|
|
{localSelects.actionType === "modal" && (
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-muted p-4">
|
|
<h4 className="text-sm font-medium text-foreground">모달 설정</h4>
|
|
|
|
<div>
|
|
<Label htmlFor="modal-title">모달 제목</Label>
|
|
<Input
|
|
id="modal-title"
|
|
placeholder="모달 제목을 입력하세요"
|
|
value={localInputs.modalTitle}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
|
onUpdateProperty("componentConfig.action.modalTitle", newValue);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="modal-size">모달 크기</Label>
|
|
<Select
|
|
value={localSelects.modalSize}
|
|
onValueChange={(value) => {
|
|
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="모달 크기 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
|
<SelectItem value="md">보통 (Medium)</SelectItem>
|
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
|
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="target-screen-modal">대상 화면 선택</Label>
|
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={modalScreenOpen}
|
|
className="h-10 w-full justify-between"
|
|
disabled={screensLoading}
|
|
>
|
|
{config.action?.targetScreenId
|
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
|
"화면을 선택하세요..."
|
|
: "화면을 선택하세요..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center border-b px-3 py-2">
|
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
<Input
|
|
placeholder="화면 검색..."
|
|
value={modalSearchTerm}
|
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
|
className="border-0 p-0 focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
<div className="max-h-[200px] overflow-auto">
|
|
{(() => {
|
|
const filteredScreens = filterScreens(modalSearchTerm);
|
|
if (screensLoading) {
|
|
return <div className="p-3 text-sm text-muted-foreground">화면 목록을 불러오는 중...</div>;
|
|
}
|
|
if (filteredScreens.length === 0) {
|
|
return <div className="p-3 text-sm text-muted-foreground">검색 결과가 없습니다.</div>;
|
|
}
|
|
return filteredScreens.map((screen, index) => (
|
|
<div
|
|
key={`modal-screen-${screen.id}-${index}`}
|
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
|
onClick={() => {
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
|
setModalScreenOpen(false);
|
|
setModalSearchTerm("");
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.name}</span>
|
|
{screen.description && <span className="text-xs text-muted-foreground">{screen.description}</span>}
|
|
</div>
|
|
</div>
|
|
));
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 수정 액션 설정 */}
|
|
{localSelects.actionType === "edit" && (
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-emerald-50 p-4">
|
|
<h4 className="text-sm font-medium text-foreground">수정 설정</h4>
|
|
|
|
<div>
|
|
<Label htmlFor="edit-screen">수정 폼 화면 선택</Label>
|
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={modalScreenOpen}
|
|
className="h-10 w-full justify-between"
|
|
disabled={screensLoading}
|
|
>
|
|
{config.action?.targetScreenId
|
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
|
"수정 폼 화면을 선택하세요..."
|
|
: "수정 폼 화면을 선택하세요..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center border-b px-3 py-2">
|
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
<Input
|
|
placeholder="화면 검색..."
|
|
value={modalSearchTerm}
|
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
|
className="border-0 p-0 focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
<div className="max-h-[200px] overflow-auto">
|
|
{(() => {
|
|
const filteredScreens = filterScreens(modalSearchTerm);
|
|
if (screensLoading) {
|
|
return <div className="p-3 text-sm text-muted-foreground">화면 목록을 불러오는 중...</div>;
|
|
}
|
|
if (filteredScreens.length === 0) {
|
|
return <div className="p-3 text-sm text-muted-foreground">검색 결과가 없습니다.</div>;
|
|
}
|
|
return filteredScreens.map((screen, index) => (
|
|
<div
|
|
key={`edit-screen-${screen.id}-${index}`}
|
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
|
onClick={() => {
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
|
setModalScreenOpen(false);
|
|
setModalSearchTerm("");
|
|
}}
|
|
>
|
|
<span className="text-sm">{screen.name}</span>
|
|
</div>
|
|
));
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 복사 액션 설정 */}
|
|
{localSelects.actionType === "copy" && (
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-primary/10 p-4">
|
|
<h4 className="text-sm font-medium text-foreground">복사 설정 (품목코드 자동 초기화)</h4>
|
|
|
|
<div>
|
|
<Label htmlFor="copy-screen">복사 폼 화면 선택</Label>
|
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={modalScreenOpen}
|
|
className="h-10 w-full justify-between"
|
|
disabled={screensLoading}
|
|
>
|
|
{config.action?.targetScreenId
|
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
|
"복사 폼 화면을 선택하세요..."
|
|
: "복사 폼 화면을 선택하세요..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center border-b px-3 py-2">
|
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
<Input
|
|
placeholder="화면 검색..."
|
|
value={modalSearchTerm}
|
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
|
className="border-0 p-0 focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
<div className="max-h-[200px] overflow-auto">
|
|
{(() => {
|
|
const filteredScreens = filterScreens(modalSearchTerm);
|
|
if (screensLoading) {
|
|
return <div className="p-3 text-sm text-muted-foreground">화면 목록을 불러오는 중...</div>;
|
|
}
|
|
if (filteredScreens.length === 0) {
|
|
return <div className="p-3 text-sm text-muted-foreground">검색 결과가 없습니다.</div>;
|
|
}
|
|
return filteredScreens.map((screen, index) => (
|
|
<div
|
|
key={`edit-screen-${screen.id}-${index}`}
|
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
|
onClick={() => {
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
|
setModalScreenOpen(false);
|
|
setModalSearchTerm("");
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.name}</span>
|
|
{screen.description && <span className="text-xs text-muted-foreground">{screen.description}</span>}
|
|
</div>
|
|
</div>
|
|
));
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
선택된 데이터가 복사되며, 품목코드는 자동으로 초기화됩니다
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="copy-mode">복사 모드</Label>
|
|
<Select
|
|
value={localSelects.editMode}
|
|
onValueChange={(value) => {
|
|
setLocalSelects((prev) => ({ ...prev, editMode: value }));
|
|
onUpdateProperty("componentConfig.action.editMode", value);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="수정 모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="modal">모달로 열기</SelectItem>
|
|
<SelectItem value="navigate">새 페이지로 이동</SelectItem>
|
|
<SelectItem value="inline">현재 화면에서 수정</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{localSelects.editMode === "modal" && (
|
|
<>
|
|
<div>
|
|
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
|
<Input
|
|
id="edit-modal-title"
|
|
placeholder="모달 제목을 입력하세요 (예: 데이터 수정)"
|
|
value={localInputs.editModalTitle}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
|
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
|
|
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
|
}}
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">비워두면 기본 제목이 표시됩니다</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="edit-modal-description">모달 설명</Label>
|
|
<Input
|
|
id="edit-modal-description"
|
|
placeholder="모달 설명을 입력하세요 (예: 선택한 데이터를 수정합니다)"
|
|
value={localInputs.editModalDescription}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
|
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
|
|
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
|
}}
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">비워두면 설명이 표시되지 않습니다</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
|
<Select
|
|
value={localSelects.modalSize}
|
|
onValueChange={(value) => {
|
|
setLocalSelects((prev) => ({ ...prev, modalSize: value }));
|
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="모달 크기 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
|
<SelectItem value="md">보통 (Medium)</SelectItem>
|
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
|
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
|
<SelectItem value="full">전체 화면 (Full)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 페이지 이동 액션 설정 */}
|
|
{localSelects.actionType === "navigate" && (
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-muted p-4">
|
|
<h4 className="text-sm font-medium text-foreground">페이지 이동 설정</h4>
|
|
|
|
<div>
|
|
<Label htmlFor="target-screen-nav">이동할 화면 선택</Label>
|
|
<Popover open={navScreenOpen} onOpenChange={setNavScreenOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={navScreenOpen}
|
|
className="h-10 w-full justify-between"
|
|
disabled={screensLoading}
|
|
>
|
|
{config.action?.targetScreenId
|
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
|
"화면을 선택하세요..."
|
|
: "화면을 선택하세요..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center border-b px-3 py-2">
|
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
<Input
|
|
placeholder="화면 검색..."
|
|
value={navSearchTerm}
|
|
onChange={(e) => setNavSearchTerm(e.target.value)}
|
|
className="border-0 p-0 focus-visible:ring-0"
|
|
/>
|
|
</div>
|
|
<div className="max-h-[200px] overflow-auto">
|
|
{(() => {
|
|
const filteredScreens = filterScreens(navSearchTerm);
|
|
if (screensLoading) {
|
|
return <div className="p-3 text-sm text-muted-foreground">화면 목록을 불러오는 중...</div>;
|
|
}
|
|
if (filteredScreens.length === 0) {
|
|
return <div className="p-3 text-sm text-muted-foreground">검색 결과가 없습니다.</div>;
|
|
}
|
|
return filteredScreens.map((screen, index) => (
|
|
<div
|
|
key={`navigate-screen-${screen.id}-${index}`}
|
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
|
onClick={() => {
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
|
setNavScreenOpen(false);
|
|
setNavSearchTerm("");
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.name}</span>
|
|
{screen.description && <span className="text-xs text-muted-foreground">{screen.description}</span>}
|
|
</div>
|
|
</div>
|
|
));
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
선택한 화면으로 /screens/{"{"}화면ID{"}"} 형태로 이동합니다
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="target-url">또는 직접 URL 입력 (고급)</Label>
|
|
<Input
|
|
id="target-url"
|
|
placeholder="예: /admin/users 또는 https://example.com"
|
|
value={localInputs.targetUrl}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
|
}}
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 🔥 NEW: 제어관리 기능 섹션 */}
|
|
<div className="mt-8 border-t border-border pt-6">
|
|
<div className="mb-4">
|
|
<h3 className="text-lg font-medium text-foreground">🔧 고급 기능</h3>
|
|
<p className="text-muted-foreground mt-1 text-sm">버튼 액션과 함께 실행될 추가 기능을 설정합니다</p>
|
|
</div>
|
|
|
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|