2025-09-09 14:29:04 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
import React, { useState, useEffect, useMemo } from "react";
|
2025-09-10 14:09:32 +09:00
|
|
|
|
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";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
2025-10-27 11:11:08 +09:00
|
|
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-09-10 14:09:32 +09:00
|
|
|
|
import { ComponentData } from "@/types/screen";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-09-18 10:05:50 +09:00
|
|
|
|
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
|
2025-09-29 12:17:10 +09:00
|
|
|
|
import { ImprovedButtonControlConfigPanel } from "./ImprovedButtonControlConfigPanel";
|
2025-10-23 18:23:01 +09:00
|
|
|
|
import { FlowVisibilityConfigPanel } from "./FlowVisibilityConfigPanel";
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
2025-09-10 14:09:32 +09:00
|
|
|
|
interface ButtonConfigPanelProps {
|
|
|
|
|
|
component: ComponentData;
|
|
|
|
|
|
onUpdateProperty: (path: string, value: any) => void;
|
2025-10-23 18:23:01 +09:00
|
|
|
|
allComponents?: ComponentData[]; // 🆕 플로우 위젯 감지용
|
2025-10-27 11:11:08 +09:00
|
|
|
|
currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
|
2025-09-10 14:09:32 +09:00
|
|
|
|
}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
interface ScreenOption {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 11:11:08 +09:00
|
|
|
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|
|
|
|
|
component,
|
2025-10-23 18:23:01 +09:00
|
|
|
|
onUpdateProperty,
|
|
|
|
|
|
allComponents = [], // 🆕 기본값 빈 배열
|
2025-10-27 11:11:08 +09:00
|
|
|
|
currentTableName, // 현재 화면의 테이블명
|
2025-10-23 18:23:01 +09:00
|
|
|
|
}) => {
|
2025-10-27 11:11:08 +09:00
|
|
|
|
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
|
|
|
|
|
const config = component.componentConfig || {};
|
|
|
|
|
|
const currentAction = component.componentConfig?.action || {};
|
|
|
|
|
|
|
2025-10-21 15:11:15 +09:00
|
|
|
|
// 로컬 상태 관리 (실시간 입력 반영)
|
|
|
|
|
|
const [localInputs, setLocalInputs] = useState({
|
2025-10-21 17:32:54 +09:00
|
|
|
|
text: config.text !== undefined ? config.text : "버튼",
|
2025-10-21 15:11:15 +09:00
|
|
|
|
modalTitle: config.action?.modalTitle || "",
|
|
|
|
|
|
editModalTitle: config.action?.editModalTitle || "",
|
|
|
|
|
|
editModalDescription: config.action?.editModalDescription || "",
|
|
|
|
|
|
targetUrl: config.action?.targetUrl || "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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("");
|
|
|
|
|
|
|
2025-10-27 11:11:08 +09:00
|
|
|
|
// 테이블 컬럼 목록 상태
|
|
|
|
|
|
const [tableColumns, setTableColumns] = useState<string[]>([]);
|
|
|
|
|
|
const [columnsLoading, setColumnsLoading] = useState(false);
|
|
|
|
|
|
const [displayColumnOpen, setDisplayColumnOpen] = useState(false);
|
|
|
|
|
|
const [displayColumnSearch, setDisplayColumnSearch] = useState("");
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 컴포넌트 prop 변경 시 로컬 상태 동기화 (Input만)
|
2025-10-21 15:11:15 +09:00
|
|
|
|
useEffect(() => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
const latestConfig = component.componentConfig || {};
|
|
|
|
|
|
const latestAction = latestConfig.action || {};
|
2025-10-21 15:11:15 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
setLocalInputs({
|
|
|
|
|
|
text: latestConfig.text !== undefined ? latestConfig.text : "버튼",
|
|
|
|
|
|
modalTitle: latestAction.modalTitle || "",
|
|
|
|
|
|
editModalTitle: latestAction.editModalTitle || "",
|
|
|
|
|
|
editModalDescription: latestAction.editModalDescription || "",
|
|
|
|
|
|
targetUrl: latestAction.targetUrl || "",
|
2025-10-21 15:11:15 +09:00
|
|
|
|
});
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [component.id]);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
|
// 화면 목록 가져오기 (전체 목록)
|
2025-09-12 14:24:25 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchScreens = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setScreensLoading(true);
|
2025-10-23 13:15:52 +09:00
|
|
|
|
// 전체 목록을 가져오기 위해 size를 큰 값으로 설정
|
|
|
|
|
|
const response = await apiClient.get("/screen-management/screens", {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 9999, // 매우 큰 값으로 설정하여 전체 목록 가져오기
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 화면 목록 로딩 실패:", error);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
} finally {
|
|
|
|
|
|
setScreensLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchScreens();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-10-27 11:11:08 +09:00
|
|
|
|
// 테이블 컬럼 목록 가져오기 (테이블 이력 보기 액션일 때)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchTableColumns = async () => {
|
|
|
|
|
|
// 테이블 이력 보기 액션이 아니면 스킵
|
|
|
|
|
|
if (config.action?.type !== "view_table_history") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 수동 입력된 테이블명 우선
|
|
|
|
|
|
// 2. 없으면 현재 화면의 테이블명 사용
|
|
|
|
|
|
const tableName = config.action?.historyTableName || currentTableName;
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블명이 없으면 스킵
|
|
|
|
|
|
if (!tableName) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
setColumnsLoading(true);
|
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`, {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 9999, // 전체 컬럼 가져오기
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// API 응답 구조: { success, data: { columns: [...], total, page, totalPages } }
|
|
|
|
|
|
const columnData = response.data.data?.columns;
|
|
|
|
|
|
|
|
|
|
|
|
if (!columnData || !Array.isArray(columnData)) {
|
|
|
|
|
|
console.error("❌ 컬럼 데이터가 배열이 아닙니다:", columnData);
|
|
|
|
|
|
setTableColumns([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data.success) {
|
|
|
|
|
|
// ID 컬럼과 날짜 관련 컬럼 제외
|
|
|
|
|
|
const filteredColumns = columnData
|
|
|
|
|
|
.filter((col: any) => {
|
|
|
|
|
|
const colName = col.columnName.toLowerCase();
|
|
|
|
|
|
const dataType = col.dataType?.toLowerCase() || "";
|
|
|
|
|
|
|
|
|
|
|
|
// ID 컬럼 제외 (id, _id로 끝나는 컬럼)
|
|
|
|
|
|
if (colName === "id" || colName.endsWith("_id")) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 날짜/시간 타입 제외 (데이터 타입 기준)
|
|
|
|
|
|
if (dataType.includes("date") || dataType.includes("time") || dataType.includes("timestamp")) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 날짜/시간 관련 컬럼명 제외 (컬럼명에 date, time, at 포함)
|
|
|
|
|
|
if (
|
|
|
|
|
|
colName.includes("date") ||
|
|
|
|
|
|
colName.includes("time") ||
|
|
|
|
|
|
colName.endsWith("_at") ||
|
|
|
|
|
|
colName.startsWith("created") ||
|
|
|
|
|
|
colName.startsWith("updated")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((col: any) => col.columnName);
|
|
|
|
|
|
|
|
|
|
|
|
setTableColumns(filteredColumns);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ 테이블 컬럼 로딩 실패:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setColumnsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchTableColumns();
|
|
|
|
|
|
}, [config.action?.type, config.action?.historyTableName, currentTableName]);
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 검색 필터링 함수
|
|
|
|
|
|
const filterScreens = (searchTerm: string) => {
|
|
|
|
|
|
if (!searchTerm.trim()) return screens;
|
|
|
|
|
|
return screens.filter(
|
|
|
|
|
|
(screen) =>
|
|
|
|
|
|
screen.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
2025-09-18 10:05:50 +09:00
|
|
|
|
(screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
2025-09-12 14:24:25 +09:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
|
|
|
|
|
// component,
|
|
|
|
|
|
// config,
|
|
|
|
|
|
// action: config.action,
|
|
|
|
|
|
// actionType: config.action?.type,
|
|
|
|
|
|
// screensCount: screens.length,
|
|
|
|
|
|
// });
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
2025-09-10 14:09:32 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="button-text">버튼 텍스트</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="button-text"
|
2025-10-21 15:11:15 +09:00
|
|
|
|
value={localInputs.text}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalInputs((prev) => ({ ...prev, text: newValue }));
|
|
|
|
|
|
onUpdateProperty("componentConfig.text", newValue);
|
|
|
|
|
|
}}
|
2025-09-10 14:09:32 +09:00
|
|
|
|
placeholder="버튼 텍스트를 입력하세요"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
2025-09-10 14:09:32 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="button-action">버튼 액션</Label>
|
|
|
|
|
|
<Select
|
2025-10-21 17:32:54 +09:00
|
|
|
|
key={`action-${component.id}-${component.componentConfig?.action?.type || "save"}`}
|
|
|
|
|
|
value={component.componentConfig?.action?.type || "save"}
|
2025-09-24 18:07:36 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-10-27 11:11:08 +09:00
|
|
|
|
// 🔥 action.type 업데이트
|
|
|
|
|
|
onUpdateProperty("componentConfig.action.type", value);
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 색상 업데이트는 충분히 지연 (React 리렌더링 완료 후)
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const newColor = value === "delete" ? "#ef4444" : "#212121";
|
|
|
|
|
|
onUpdateProperty("style.labelColor", newColor);
|
|
|
|
|
|
}, 100); // 0 → 100ms로 증가
|
2025-09-24 18:07:36 +09:00
|
|
|
|
}}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
>
|
2025-09-10 14:09:32 +09:00
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="버튼 액션 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="save">저장</SelectItem>
|
|
|
|
|
|
<SelectItem value="delete">삭제</SelectItem>
|
2025-10-23 13:15:52 +09:00
|
|
|
|
<SelectItem value="edit">편집</SelectItem>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
2025-10-23 13:15:52 +09:00
|
|
|
|
<SelectItem value="modal">모달 열기</SelectItem>
|
|
|
|
|
|
<SelectItem value="control">제어 흐름</SelectItem>
|
2025-10-27 11:11:08 +09:00
|
|
|
|
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
2025-09-10 14:09:32 +09:00
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
</div>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 모달 열기 액션 설정 */}
|
2025-10-21 17:32:54 +09:00
|
|
|
|
{(component.componentConfig?.action?.type || "save") === "modal" && (
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="modal-title">모달 제목</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="modal-title"
|
|
|
|
|
|
placeholder="모달 제목을 입력하세요"
|
2025-10-21 15:11:15 +09:00
|
|
|
|
value={localInputs.modalTitle}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.modalTitle", newValue);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
}}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="modal-size">모달 크기</Label>
|
|
|
|
|
|
<Select
|
2025-10-21 17:32:54 +09:00
|
|
|
|
value={component.componentConfig?.action?.modalSize || "md"}
|
2025-10-21 15:11:15 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
}}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<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}
|
2025-09-18 10:05:50 +09:00
|
|
|
|
className="h-10 w-full justify-between"
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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>
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<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-gray-500">화면 목록을 불러오는 중...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (filteredScreens.length === 0) {
|
|
|
|
|
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</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-gray-100"
|
|
|
|
|
|
onClick={() => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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-gray-500">{screen.description}</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
));
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
|
{/* 수정 액션 설정 */}
|
2025-10-21 17:32:54 +09:00
|
|
|
|
{(component.componentConfig?.action?.type || "save") === "edit" && (
|
2025-09-18 18:49:30 +09:00
|
|
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-700">수정 설정</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-gray-500">화면 목록을 불러오는 중...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (filteredScreens.length === 0) {
|
|
|
|
|
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</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-gray-100"
|
|
|
|
|
|
onClick={() => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
2025-09-18 18:49:30 +09:00
|
|
|
|
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-gray-500">{screen.description}</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
));
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
|
|
|
|
선택된 데이터가 이 폼 화면에 자동으로 로드되어 수정할 수 있습니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="edit-mode">수정 모드</Label>
|
|
|
|
|
|
<Select
|
2025-10-21 17:32:54 +09:00
|
|
|
|
value={component.componentConfig?.action?.editMode || "modal"}
|
2025-10-21 15:11:15 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.editMode", value);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
}}
|
2025-09-18 18:49:30 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="수정 모드 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="modal">모달로 열기</SelectItem>
|
|
|
|
|
|
<SelectItem value="navigate">새 페이지로 이동</SelectItem>
|
|
|
|
|
|
<SelectItem value="inline">현재 화면에서 수정</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
{(component.componentConfig?.action?.editMode || "modal") === "modal" && (
|
2025-10-01 17:41:30 +09:00
|
|
|
|
<>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="edit-modal-title">모달 제목</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-modal-title"
|
|
|
|
|
|
placeholder="모달 제목을 입력하세요 (예: 데이터 수정)"
|
2025-10-21 15:11:15 +09:00
|
|
|
|
value={localInputs.editModalTitle}
|
2025-10-01 17:45:29 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
2025-10-21 15:11:15 +09:00
|
|
|
|
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
|
2025-10-01 17:45:29 +09:00
|
|
|
|
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
|
|
|
|
|
|
}}
|
2025-10-01 17:41:30 +09:00
|
|
|
|
/>
|
|
|
|
|
|
<p className="mt-1 text-xs text-gray-500">비워두면 기본 제목이 표시됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="edit-modal-description">모달 설명</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-modal-description"
|
|
|
|
|
|
placeholder="모달 설명을 입력하세요 (예: 선택한 데이터를 수정합니다)"
|
2025-10-21 15:11:15 +09:00
|
|
|
|
value={localInputs.editModalDescription}
|
2025-10-01 17:45:29 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
2025-10-21 15:11:15 +09:00
|
|
|
|
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
|
2025-10-01 17:45:29 +09:00
|
|
|
|
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
|
|
|
|
|
|
}}
|
2025-10-01 17:41:30 +09:00
|
|
|
|
/>
|
|
|
|
|
|
<p className="mt-1 text-xs text-gray-500">비워두면 설명이 표시되지 않습니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="edit-modal-size">모달 크기</Label>
|
|
|
|
|
|
<Select
|
2025-10-21 17:32:54 +09:00
|
|
|
|
value={component.componentConfig?.action?.modalSize || "md"}
|
2025-10-21 15:11:15 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.modalSize", value);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
}}
|
2025-10-01 17:41:30 +09:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</>
|
2025-09-18 18:49:30 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-27 11:11:08 +09:00
|
|
|
|
{/* 테이블 이력 보기 액션 설정 */}
|
|
|
|
|
|
{(component.componentConfig?.action?.type || "save") === "view_table_history" && (
|
2025-10-27 16:40:59 +09:00
|
|
|
|
<div className="mt-4 space-y-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium">📜 테이블 이력 보기 설정</h4>
|
2025-10-27 11:11:08 +09:00
|
|
|
|
|
|
|
|
|
|
<div>
|
2025-10-27 16:40:59 +09:00
|
|
|
|
<Label>
|
2025-10-27 11:11:08 +09:00
|
|
|
|
전체 이력 표시 컬럼 (필수) <span className="text-red-600">*</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
|
|
|
|
|
|
{!config.action?.historyTableName && !currentTableName ? (
|
|
|
|
|
|
<div className="mt-2 rounded-md border border-yellow-300 bg-yellow-50 p-3">
|
|
|
|
|
|
<p className="text-xs text-yellow-800">
|
|
|
|
|
|
⚠️ 먼저 <strong>테이블명</strong>을 입력하거나, 현재 화면에 테이블을 연결해주세요.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{!config.action?.historyTableName && currentTableName && (
|
|
|
|
|
|
<div className="mt-2 rounded-md border border-green-300 bg-green-50 p-2">
|
|
|
|
|
|
<p className="text-xs text-green-800">
|
|
|
|
|
|
✓ 현재 화면의 테이블 <strong>{currentTableName}</strong>을(를) 자동으로 사용합니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Popover open={displayColumnOpen} onOpenChange={setDisplayColumnOpen}>
|
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
role="combobox"
|
|
|
|
|
|
aria-expanded={displayColumnOpen}
|
|
|
|
|
|
className="mt-2 h-10 w-full justify-between text-sm"
|
|
|
|
|
|
disabled={columnsLoading || tableColumns.length === 0}
|
|
|
|
|
|
>
|
|
|
|
|
|
{columnsLoading
|
|
|
|
|
|
? "로딩 중..."
|
|
|
|
|
|
: config.action?.historyDisplayColumn
|
|
|
|
|
|
? config.action.historyDisplayColumn
|
|
|
|
|
|
: tableColumns.length === 0
|
|
|
|
|
|
? "사용 가능한 컬럼이 없습니다"
|
|
|
|
|
|
: "컬럼을 선택하세요"}
|
|
|
|
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
|
|
|
|
|
<Command>
|
|
|
|
|
|
<CommandInput placeholder="컬럼 검색..." className="text-sm" />
|
|
|
|
|
|
<CommandList>
|
|
|
|
|
|
<CommandEmpty className="text-sm">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
|
|
|
|
<CommandGroup>
|
|
|
|
|
|
{tableColumns.map((column) => (
|
|
|
|
|
|
<CommandItem
|
|
|
|
|
|
key={column}
|
|
|
|
|
|
value={column}
|
|
|
|
|
|
onSelect={(currentValue) => {
|
|
|
|
|
|
onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue);
|
|
|
|
|
|
setDisplayColumnOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Check
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"mr-2 h-4 w-4",
|
|
|
|
|
|
config.action?.historyDisplayColumn === column ? "opacity-100" : "opacity-0",
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</CommandItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
|
</CommandList>
|
|
|
|
|
|
</Command>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
|
|
<p className="mt-2 text-xs text-gray-700">
|
|
|
|
|
|
<strong>전체 테이블 이력</strong>에서 레코드를 구분하기 위한 컬럼입니다.
|
|
|
|
|
|
<br />
|
2025-10-27 11:41:30 +09:00
|
|
|
|
예: <code className="rounded bg-white px-1">device_code</code>를 설정하면 이력에 "DTG-001"로
|
|
|
|
|
|
표시됩니다.
|
2025-10-27 11:11:08 +09:00
|
|
|
|
<br />이 컬럼으로 검색도 가능합니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{tableColumns.length === 0 && !columnsLoading && (
|
|
|
|
|
|
<p className="mt-2 text-xs text-red-600">
|
|
|
|
|
|
⚠️ ID 및 날짜 타입 컬럼을 제외한 사용 가능한 컬럼이 없습니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
{/* 페이지 이동 액션 설정 */}
|
2025-10-21 17:32:54 +09:00
|
|
|
|
{(component.componentConfig?.action?.type || "save") === "navigate" && (
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="target-screen-nav">이동할 화면 선택</Label>
|
|
|
|
|
|
<Popover open={navScreenOpen} onOpenChange={setNavScreenOpen}>
|
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
role="combobox"
|
|
|
|
|
|
aria-expanded={navScreenOpen}
|
2025-09-18 10:05:50 +09:00
|
|
|
|
className="h-10 w-full justify-between"
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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>
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<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-gray-500">화면 목록을 불러오는 중...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (filteredScreens.length === 0) {
|
|
|
|
|
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</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-gray-100"
|
|
|
|
|
|
onClick={() => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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-gray-500">{screen.description}</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
));
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
|
|
|
|
선택한 화면으로 /screens/{"{"}화면ID{"}"} 형태로 이동합니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="target-url">또는 직접 URL 입력 (고급)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="target-url"
|
|
|
|
|
|
placeholder="예: /admin/users 또는 https://example.com"
|
2025-10-21 15:11:15 +09:00
|
|
|
|
value={localInputs.targetUrl}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalInputs((prev) => ({ ...prev, targetUrl: newValue }));
|
2025-10-21 17:32:54 +09:00
|
|
|
|
onUpdateProperty("componentConfig.action.targetUrl", newValue);
|
2025-10-21 15:11:15 +09:00
|
|
|
|
}}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
/>
|
|
|
|
|
|
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-27 16:40:59 +09:00
|
|
|
|
{/* 제어 기능 섹션 */}
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<div className="mt-8 border-t border-gray-200 pt-6">
|
2025-09-29 12:17:10 +09:00
|
|
|
|
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
2025-09-18 10:05:50 +09:00
|
|
|
|
</div>
|
2025-10-23 18:23:01 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 🆕 플로우 단계별 표시 제어 섹션 */}
|
|
|
|
|
|
<div className="mt-8 border-t border-gray-200 pt-6">
|
2025-10-27 11:11:08 +09:00
|
|
|
|
<FlowVisibilityConfigPanel
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
|
onUpdateProperty={onUpdateProperty}
|
2025-10-23 18:23:01 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|