From 2a968ab3cfecc97ea2b34715db9d9b68aa377d9c Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Oct 2025 14:55:41 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=20=EA=B2=80=EC=83=89=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/authController.ts | 44 +++ .../components/screen/widgets/FlowWidget.tsx | 290 +++++++++++++++++- frontend/hooks/useLogin.ts | 14 +- frontend/types/auth.ts | 6 +- 4 files changed, 339 insertions(+), 15 deletions(-) diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index ba9dcdc1..374015ee 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -59,12 +59,56 @@ export class AuthController { logger.info(`- userName: ${userInfo.userName}`); logger.info(`- companyCode: ${userInfo.companyCode}`); + // 사용자의 첫 번째 접근 가능한 메뉴 조회 + let firstMenuPath: string | null = null; + try { + const { AdminService } = await import("../services/adminService"); + const paramMap = { + userId: loginResult.userInfo.userId, + userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN", + userType: loginResult.userInfo.userType, + userLang: "ko", + }; + + const menuList = await AdminService.getUserMenuList(paramMap); + logger.info(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`); + + // 접근 가능한 첫 번째 메뉴 찾기 + // 조건: + // 1. LEV (레벨)이 2 이상 (최상위 폴더 제외) + // 2. MENU_URL이 있고 비어있지 않음 + // 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴 + const firstMenu = menuList.find((menu: any) => { + const level = menu.lev || menu.level; + const url = menu.menu_url || menu.url; + + return level >= 2 && url && url.trim() !== "" && url !== "#"; + }); + + if (firstMenu) { + firstMenuPath = firstMenu.menu_url || firstMenu.url; + logger.info(`✅ 첫 번째 접근 가능한 메뉴 발견:`, { + name: firstMenu.menu_name_kor || firstMenu.translated_name, + url: firstMenuPath, + level: firstMenu.lev || firstMenu.level, + seq: firstMenu.seq, + }); + } else { + logger.info( + "⚠️ 접근 가능한 메뉴가 없습니다. 메인 페이지로 이동합니다." + ); + } + } catch (menuError) { + logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError); + } + res.status(200).json({ success: true, message: "로그인 성공", data: { userInfo, token: loginResult.token, + firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가 }, }); } else { diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 7b10e071..a97c72e1 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -1,10 +1,10 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FlowComponent } from "@/types/screen-management"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { AlertCircle, Loader2, ChevronUp } from "lucide-react"; +import { AlertCircle, Loader2, ChevronUp, Filter, X } from "lucide-react"; import { getFlowById, getAllStepCounts, @@ -27,6 +27,16 @@ import { PaginationPrevious, } from "@/components/ui/pagination"; import { useFlowStepStore } from "@/stores/flowStepStore"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; interface FlowWidgetProps { component: FlowComponent; @@ -62,6 +72,13 @@ export function FlowWidget({ const [selectedRows, setSelectedRows] = useState>(new Set()); const [columnLabels, setColumnLabels] = useState>({}); // 컬럼명 -> 라벨 매핑 + // 🆕 검색 필터 관련 상태 + const [searchFilterColumns, setSearchFilterColumns] = useState>(new Set()); // 검색 필터로 사용할 컬럼 + const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그 + const [searchValues, setSearchValues] = useState>({}); // 검색 값 + const [allAvailableColumns, setAllAvailableColumns] = useState([]); // 전체 컬럼 목록 + const [filteredData, setFilteredData] = useState([]); // 필터링된 데이터 + /** * 🆕 컬럼 표시 결정 함수 * 1순위: 플로우 스텝 기본 설정 (displayConfig) @@ -97,6 +114,113 @@ export function FlowWidget({ // 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용) const flowComponentId = component.id; + // 🆕 localStorage 키 생성 + const filterSettingKey = useMemo(() => { + if (!flowId || selectedStepId === null) return null; + return `flowWidget_searchFilters_${flowId}_${selectedStepId}`; + }, [flowId, selectedStepId]); + + // 🆕 저장된 필터 설정 불러오기 + useEffect(() => { + if (!filterSettingKey || allAvailableColumns.length === 0) return; + + try { + const saved = localStorage.getItem(filterSettingKey); + if (saved) { + const savedFilters = JSON.parse(saved); + setSearchFilterColumns(new Set(savedFilters)); + } else { + // 초기값: 빈 필터 (사용자가 선택해야 함) + setSearchFilterColumns(new Set()); + } + } catch (error) { + console.error("필터 설정 불러오기 실패:", error); + setSearchFilterColumns(new Set()); + } + }, [filterSettingKey, allAvailableColumns]); + + // 🆕 필터 설정 저장 + const saveFilterSettings = useCallback(() => { + if (!filterSettingKey) return; + + try { + localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(searchFilterColumns))); + setIsFilterSettingOpen(false); + toast.success("검색 필터 설정이 저장되었습니다"); + + // 검색 값 초기화 + setSearchValues({}); + } catch (error) { + console.error("필터 설정 저장 실패:", error); + toast.error("설정 저장에 실패했습니다"); + } + }, [filterSettingKey, searchFilterColumns]); + + // 🆕 필터 컬럼 토글 + const toggleFilterColumn = useCallback((columnName: string) => { + setSearchFilterColumns((prev) => { + const newSet = new Set(prev); + if (newSet.has(columnName)) { + newSet.delete(columnName); + } else { + newSet.add(columnName); + } + return newSet; + }); + }, []); + + // 🆕 전체 선택/해제 + const toggleAllFilters = useCallback(() => { + if (searchFilterColumns.size === allAvailableColumns.length) { + // 전체 해제 + setSearchFilterColumns(new Set()); + } else { + // 전체 선택 + setSearchFilterColumns(new Set(allAvailableColumns)); + } + }, [searchFilterColumns, allAvailableColumns]); + + // 🆕 검색 실행 + const handleSearch = useCallback(() => { + if (!stepData || stepData.length === 0) return; + + const filtered = stepData.filter((row) => { + // 모든 검색 조건을 만족하는지 확인 + return Array.from(searchFilterColumns).every((col) => { + const searchValue = searchValues[col]; + if (!searchValue || searchValue.trim() === "") return true; // 빈 값은 필터링하지 않음 + + const cellValue = row[col]; + if (cellValue === null || cellValue === undefined) return false; + + // 문자열로 변환하여 대소문자 무시 검색 + return String(cellValue).toLowerCase().includes(searchValue.toLowerCase()); + }); + }); + + setFilteredData(filtered); + console.log("🔍 검색 실행:", { + totalRows: stepData.length, + filteredRows: filtered.length, + searchValues, + }); + }, [stepData, searchFilterColumns, searchValues]); + + // 🆕 검색 초기화 + const handleClearSearch = useCallback(() => { + setSearchValues({}); + setFilteredData([]); + }, []); + + // 검색 값이 변경될 때마다 자동 검색 + useEffect(() => { + if (Object.keys(searchValues).length > 0) { + handleSearch(); + } else { + setFilteredData([]); + } + }, [searchValues, handleSearch]); + // 선택된 스텝의 데이터를 다시 로드하는 함수 const refreshStepData = async () => { if (!flowId) return; @@ -149,14 +273,18 @@ export function FlowWidget({ // 🆕 컬럼 추출 및 우선순위 적용 if (rows.length > 0) { const allColumns = Object.keys(rows[0]); + setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 const visibleColumns = getVisibleColumns(selectedStepId, allColumns); setStepDataColumns(visibleColumns); } else { + setAllAvailableColumns([]); setStepDataColumns([]); } // 선택 초기화 setSelectedRows(new Set()); + setSearchValues({}); // 검색 값도 초기화 + setFilteredData([]); // 필터링된 데이터 초기화 onSelectedDataChange?.([], selectedStepId); } } catch (err: any) { @@ -242,6 +370,7 @@ export function FlowWidget({ setStepData(rows); if (rows.length > 0) { const allColumns = Object.keys(rows[0]); + setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 // sortedSteps를 직접 전달하여 타이밍 이슈 해결 const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps); setStepDataColumns(visibleColumns); @@ -335,9 +464,11 @@ export function FlowWidget({ // 🆕 컬럼 추출 및 우선순위 적용 if (rows.length > 0) { const allColumns = Object.keys(rows[0]); + setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 const visibleColumns = getVisibleColumns(stepId, allColumns); setStepDataColumns(visibleColumns); } else { + setAllAvailableColumns([]); setStepDataColumns([]); } } catch (err: any) { @@ -385,9 +516,12 @@ export function FlowWidget({ onSelectedDataChange?.(selectedData, selectedStepId); }; + // 🆕 표시할 데이터 결정 (필터링된 데이터 또는 전체 데이터) + const displayData = filteredData.length > 0 ? filteredData : stepData; + // 🆕 페이지네이션된 스텝 데이터 - const paginatedStepData = stepData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize); - const totalStepDataPages = Math.ceil(stepData.length / stepDataPageSize); + const paginatedStepData = displayData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize); + const totalStepDataPages = Math.ceil(displayData.length / stepDataPageSize); if (loading) { return ( @@ -513,15 +647,77 @@ export function FlowWidget({
{/* 헤더 - 자동 높이 */}
-

- {steps.find((s) => s.id === selectedStepId)?.stepName} -

-

- 총 {stepData.length}건의 데이터 - {selectedRows.size > 0 && ( - ({selectedRows.size}건 선택됨) +

+
+

+ {steps.find((s) => s.id === selectedStepId)?.stepName} +

+

+ 총 {stepData.length}건의 데이터 + {filteredData.length > 0 && ( + (필터링: {filteredData.length}건) + )} + {selectedRows.size > 0 && ( + ({selectedRows.size}건 선택됨) + )} +

+
+ + {/* 🆕 필터 설정 버튼 */} + {allAvailableColumns.length > 0 && ( + )} -

+
+ + {/* 🆕 검색 필터 입력 영역 */} + {searchFilterColumns.size > 0 && ( +
+
+
검색 필터
+ {Object.keys(searchValues).length > 0 && ( + + )} +
+ +
+ {Array.from(searchFilterColumns).map((col) => ( +
+ + + setSearchValues((prev) => ({ + ...prev, + [col]: e.target.value, + })) + } + placeholder={`${columnLabels[col] || col} 검색...`} + className="h-8 text-xs" + /> +
+ ))} +
+
+ )}
{/* 데이터 영역 - 고정 높이 + 스크롤 */} @@ -746,6 +942,76 @@ export function FlowWidget({ )}
)} + + {/* 🆕 검색 필터 설정 다이얼로그 */} + + + + 검색 필터 설정 + + 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다. + + + +
+ {/* 전체 선택/해제 */} +
+ 0} + onCheckedChange={toggleAllFilters} + /> + + + {searchFilterColumns.size} / {allAvailableColumns.length}개 + +
+ + {/* 컬럼 목록 */} +
+ {allAvailableColumns.map((col) => ( +
+ toggleFilterColumn(col)} + /> + +
+ ))} +
+ + {/* 선택된 컬럼 개수 안내 */} +
+ {searchFilterColumns.size === 0 ? ( + 검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요 + ) : ( + + 총 {searchFilterColumns.size}개의 검색 필터가 + 표시됩니다 + + )} +
+
+ + + + + +
+
); } diff --git a/frontend/hooks/useLogin.ts b/frontend/hooks/useLogin.ts index 1a7513e9..09c32d5f 100644 --- a/frontend/hooks/useLogin.ts +++ b/frontend/hooks/useLogin.ts @@ -141,8 +141,18 @@ export const useLogin = () => { // 쿠키에도 저장 (미들웨어에서 사용) document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`; - // 로그인 성공 - router.push(AUTH_CONFIG.ROUTES.MAIN); + // 로그인 성공 - 첫 번째 접근 가능한 메뉴로 리다이렉트 + const firstMenuPath = result.data?.firstMenuPath; + + if (firstMenuPath) { + // 접근 가능한 메뉴가 있으면 해당 메뉴로 이동 + console.log("첫 번째 접근 가능한 메뉴로 이동:", firstMenuPath); + router.push(firstMenuPath); + } else { + // 접근 가능한 메뉴가 없으면 메인 페이지로 이동 + console.log("접근 가능한 메뉴가 없어 메인 페이지로 이동"); + router.push(AUTH_CONFIG.ROUTES.MAIN); + } } else { // 로그인 실패 setError(result.message || FORM_VALIDATION.MESSAGES.LOGIN_FAILED); diff --git a/frontend/types/auth.ts b/frontend/types/auth.ts index f1d5bbd8..cd8e65b6 100644 --- a/frontend/types/auth.ts +++ b/frontend/types/auth.ts @@ -10,7 +10,11 @@ export interface LoginFormData { export interface LoginResponse { success: boolean; message?: string; - data?: any; + data?: { + token?: string; + userInfo?: any; + firstMenuPath?: string | null; + }; errorCode?: string; } -- 2.43.0 From 53a0fa5c6aa2cdb45f9a0fa906d09a7bac734b99 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Oct 2025 15:00:08 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/screen/widgets/FlowWidget.tsx | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index a97c72e1..cd0c052f 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -180,21 +180,39 @@ export function FlowWidget({ } }, [searchFilterColumns, allAvailableColumns]); - // 🆕 검색 실행 - const handleSearch = useCallback(() => { - if (!stepData || stepData.length === 0) return; + // 🆕 검색 초기화 + const handleClearSearch = useCallback(() => { + setSearchValues({}); + setFilteredData([]); + }, []); + // 🆕 검색 값이 변경될 때마다 자동 검색 (useEffect로 직접 처리) + useEffect(() => { + if (!stepData || stepData.length === 0) { + setFilteredData([]); + return; + } + + // 검색 값이 하나라도 있는지 확인 + const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== ""); + + if (!hasSearchValue) { + // 검색 값이 없으면 필터링 해제 + setFilteredData([]); + return; + } + + // 필터링 실행 const filtered = stepData.filter((row) => { // 모든 검색 조건을 만족하는지 확인 - return Array.from(searchFilterColumns).every((col) => { - const searchValue = searchValues[col]; - if (!searchValue || searchValue.trim() === "") return true; // 빈 값은 필터링하지 않음 + return Object.entries(searchValues).every(([col, searchValue]) => { + if (!searchValue || String(searchValue).trim() === "") return true; // 빈 값은 필터링하지 않음 const cellValue = row[col]; if (cellValue === null || cellValue === undefined) return false; // 문자열로 변환하여 대소문자 무시 검색 - return String(cellValue).toLowerCase().includes(searchValue.toLowerCase()); + return String(cellValue).toLowerCase().includes(String(searchValue).toLowerCase()); }); }); @@ -203,23 +221,9 @@ export function FlowWidget({ totalRows: stepData.length, filteredRows: filtered.length, searchValues, + hasSearchValue, }); - }, [stepData, searchFilterColumns, searchValues]); - - // 🆕 검색 초기화 - const handleClearSearch = useCallback(() => { - setSearchValues({}); - setFilteredData([]); - }, []); - - // 검색 값이 변경될 때마다 자동 검색 - useEffect(() => { - if (Object.keys(searchValues).length > 0) { - handleSearch(); - } else { - setFilteredData([]); - } - }, [searchValues, handleSearch]); + }, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행 // 선택된 스텝의 데이터를 다시 로드하는 함수 const refreshStepData = async () => { @@ -516,8 +520,11 @@ export function FlowWidget({ onSelectedDataChange?.(selectedData, selectedStepId); }; - // 🆕 표시할 데이터 결정 (필터링된 데이터 또는 전체 데이터) - const displayData = filteredData.length > 0 ? filteredData : stepData; + // 🆕 표시할 데이터 결정 + // - 검색 값이 있으면 → filteredData 사용 (결과가 0건이어도 filteredData 사용) + // - 검색 값이 없으면 → stepData 사용 (전체 데이터) + const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== ""); + const displayData = hasSearchValue ? filteredData : stepData; // 🆕 페이지네이션된 스텝 데이터 const paginatedStepData = displayData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize); -- 2.43.0 From 775fbf8903ceed8647320cc53f4afc738eca77be Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Oct 2025 15:39:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=80=EC=A7=80=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 572 +++---- .../screen/InteractiveDataTable.tsx | 63 +- .../screen/InteractiveScreenViewer.tsx | 18 + .../screen/InteractiveScreenViewerDynamic.tsx | 63 +- frontend/components/screen/ScreenDesigner.tsx | 1426 +++++++++-------- frontend/components/screen/ScreenList.tsx | 4 +- .../components/screen/widgets/FlowWidget.tsx | 112 +- frontend/contexts/ScreenPreviewContext.tsx | 24 + .../button-primary/ButtonPrimaryComponent.tsx | 8 + 9 files changed, 1253 insertions(+), 1037 deletions(-) create mode 100644 frontend/contexts/ScreenPreviewContext.tsx diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index d365ebbd..a4f6ace4 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -15,6 +15,7 @@ import { FlowButtonGroup } from "@/components/screen/widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; export default function ScreenViewPage() { const params = useParams(); @@ -211,302 +212,305 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
- {/* 절대 위치 기반 렌더링 */} - {layout && layout.components.length > 0 ? ( -
- {/* 최상위 컴포넌트들 렌더링 */} - {(() => { - // 🆕 플로우 버튼 그룹 감지 및 처리 - const topLevelComponents = layout.components.filter((component) => !component.parentId); + +
+ {/* 절대 위치 기반 렌더링 */} + {layout && layout.components.length > 0 ? ( +
+ {/* 최상위 컴포넌트들 렌더링 */} + {(() => { + // 🆕 플로우 버튼 그룹 감지 및 처리 + const topLevelComponents = layout.components.filter((component) => !component.parentId); - const buttonGroups: Record = {}; - const processedButtonIds = new Set(); + const buttonGroups: Record = {}; + const processedButtonIds = new Set(); - topLevelComponents.forEach((component) => { - const isButton = - component.type === "button" || - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)); + topLevelComponents.forEach((component) => { + const isButton = + component.type === "button" || + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)); - if (isButton) { - const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as - | FlowVisibilityConfig - | undefined; + if (isButton) { + const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as + | FlowVisibilityConfig + | undefined; - if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { - if (!buttonGroups[flowConfig.groupId]) { - buttonGroups[flowConfig.groupId] = []; + if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { + if (!buttonGroups[flowConfig.groupId]) { + buttonGroups[flowConfig.groupId] = []; + } + buttonGroups[flowConfig.groupId].push(component); + processedButtonIds.add(component.id); } - buttonGroups[flowConfig.groupId].push(component); - processedButtonIds.add(component.id); } - } - }); + }); - const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); - return ( - <> - {/* 일반 컴포넌트들 */} - {regularComponents.map((component) => ( - {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("🔍 화면에서 선택된 행 데이터:", selectedData); - setSelectedRowsData(selectedData); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", { - dataCount: selectedData.length, - selectedData, - stepId, - }); - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); - console.log("🔍 [page.tsx] 상태 업데이트 완료"); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // 선택 해제 - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - console.log("🔄 플로우 새로고침 요청됨"); - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); // 선택 해제 - setFlowSelectedStepId(null); - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - console.log("📝 폼 데이터 변경:", fieldName, "=", value); - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - > - {/* 자식 컴포넌트들 */} - {(component.type === "group" || component.type === "container" || component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 - const relativeChildComponent = { - ...child, - position: { - x: child.position.x - component.position.x, - y: child.position.y - component.position.y, - z: child.position.z || 1, - }, - }; - - return ( - {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); - setSelectedRowsData(selectedData); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨 (자식)"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // 선택 해제 - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value); - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - /> - ); - })} - - ))} - - {/* 🆕 플로우 버튼 그룹들 */} - {Object.entries(buttonGroups).map(([groupId, buttons]) => { - if (buttons.length === 0) return null; - - const firstButton = buttons[0]; - const groupConfig = (firstButton as any).webTypeConfig?.flowVisibilityConfig as FlowVisibilityConfig; - - // 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용 - const groupPosition = buttons.reduce( - (min, button) => ({ - x: Math.min(min.x, button.position.x), - y: Math.min(min.y, button.position.y), - z: min.z, - }), - { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, - ); - - // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 - const direction = groupConfig.groupDirection || "horizontal"; - const gap = groupConfig.groupGap ?? 8; - - let groupWidth = 0; - let groupHeight = 0; - - if (direction === "horizontal") { - groupWidth = buttons.reduce((total, button, index) => { - const buttonWidth = button.size?.width || 100; - const gapWidth = index < buttons.length - 1 ? gap : 0; - return total + buttonWidth + gapWidth; - }, 0); - groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); - } else { - groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); - groupHeight = buttons.reduce((total, button, index) => { - const buttonHeight = button.size?.height || 40; - const gapHeight = index < buttons.length - 1 ? gap : 0; - return total + buttonHeight + gapHeight; - }, 0); - } - - return ( -
+ {/* 일반 컴포넌트들 */} + {regularComponents.map((component) => ( + {}} + screenId={screenId} + tableName={screen?.tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터:", selectedData); + setSelectedRowsData(selectedData); + }} + flowSelectedData={flowSelectedData} + flowSelectedStepId={flowSelectedStepId} + onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { + console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", { + dataCount: selectedData.length, + selectedData, + stepId, + }); + setFlowSelectedData(selectedData); + setFlowSelectedStepId(stepId); + console.log("🔍 [page.tsx] 상태 업데이트 완료"); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} + flowRefreshKey={flowRefreshKey} + onFlowRefresh={() => { + console.log("🔄 플로우 새로고침 요청됨"); + setFlowRefreshKey((prev) => prev + 1); + setFlowSelectedData([]); // 선택 해제 + setFlowSelectedStepId(null); + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경:", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); }} > - { - const relativeButton = { - ...button, - position: { x: 0, y: 0, z: button.position.z || 1 }, - }; + {/* 자식 컴포넌트들 */} + {(component.type === "group" || component.type === "container" || component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...child, + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + z: child.position.z || 1, + }, + }; - return ( -
-
- {}} - screenId={screenId} - tableName={screen?.tableName} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - setSelectedRowsData(selectedData); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); - setFlowSelectedStepId(null); - }} - onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - /> -
-
- ); + return ( + {}} + screenId={screenId} + tableName={screen?.tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); + setSelectedRowsData(selectedData); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("🔄 테이블 새로고침 요청됨 (자식)"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // 선택 해제 + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + /> + ); + })} + + ))} + + {/* 🆕 플로우 버튼 그룹들 */} + {Object.entries(buttonGroups).map(([groupId, buttons]) => { + if (buttons.length === 0) return null; + + const firstButton = buttons[0]; + const groupConfig = (firstButton as any).webTypeConfig + ?.flowVisibilityConfig as FlowVisibilityConfig; + + // 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용 + const groupPosition = buttons.reduce( + (min, button) => ({ + x: Math.min(min.x, button.position.x), + y: Math.min(min.y, button.position.y), + z: min.z, + }), + { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, + ); + + // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 + const direction = groupConfig.groupDirection || "horizontal"; + const gap = groupConfig.groupGap ?? 8; + + let groupWidth = 0; + let groupHeight = 0; + + if (direction === "horizontal") { + groupWidth = buttons.reduce((total, button, index) => { + const buttonWidth = button.size?.width || 100; + const gapWidth = index < buttons.length - 1 ? gap : 0; + return total + buttonWidth + gapWidth; + }, 0); + groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); + } else { + groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); + groupHeight = buttons.reduce((total, button, index) => { + const buttonHeight = button.size?.height || 40; + const gapHeight = index < buttons.length - 1 ? gap : 0; + return total + buttonHeight + gapHeight; + }, 0); + } + + return ( +
-
- ); - })} - - ); - })()} -
- ) : ( - // 빈 화면일 때 -
-
-
- 📄 -
-

화면이 비어있습니다

-

이 화면에는 아직 설계된 컴포넌트가 없습니다.

-
-
- )} + > + { + const relativeButton = { + ...button, + position: { x: 0, y: 0, z: button.position.z || 1 }, + }; - {/* 편집 모달 */} - { - setEditModalOpen(false); - setEditModalConfig({}); - }} - screenId={editModalConfig.screenId} - modalSize={editModalConfig.modalSize} - editData={editModalConfig.editData} - onSave={editModalConfig.onSave} - modalTitle={editModalConfig.modalTitle} - modalDescription={editModalConfig.modalDescription} - onDataChange={(changedFormData) => { - console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData); - // 변경된 데이터를 메인 폼에 반영 - setFormData((prev) => { - const updatedFormData = { - ...prev, - ...changedFormData, // 변경된 필드들만 업데이트 - }; - console.log("📊 메인 폼 데이터 업데이트:", updatedFormData); - return updatedFormData; - }); - }} - /> -
+ return ( +
+
+ {}} + screenId={screenId} + tableName={screen?.tableName} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + setSelectedRowsData(selectedData); + }} + flowSelectedData={flowSelectedData} + flowSelectedStepId={flowSelectedStepId} + onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { + setFlowSelectedData(selectedData); + setFlowSelectedStepId(stepId); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); + }} + flowRefreshKey={flowRefreshKey} + onFlowRefresh={() => { + setFlowRefreshKey((prev) => prev + 1); + setFlowSelectedData([]); + setFlowSelectedStepId(null); + }} + onFormDataChange={(fieldName, value) => { + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + /> +
+
+ ); + }} + /> +
+ ); + })} + + ); + })()} +
+ ) : ( + // 빈 화면일 때 +
+
+
+ 📄 +
+

화면이 비어있습니다

+

이 화면에는 아직 설계된 컴포넌트가 없습니다.

+
+
+ )} + + {/* 편집 모달 */} + { + setEditModalOpen(false); + setEditModalConfig({}); + }} + screenId={editModalConfig.screenId} + modalSize={editModalConfig.modalSize} + editData={editModalConfig.editData} + onSave={editModalConfig.onSave} + modalTitle={editModalConfig.modalTitle} + modalDescription={editModalConfig.modalDescription} + onDataChange={(changedFormData) => { + console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData); + // 변경된 데이터를 메인 폼에 반영 + setFormData((prev) => { + const updatedFormData = { + ...prev, + ...changedFormData, // 변경된 필드들만 업데이트 + }; + console.log("📊 메인 폼 데이터 업데이트:", updatedFormData); + return updatedFormData; + }); + }} + /> +
+ ); } diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 1a14c2d9..b54df6ad 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -49,6 +49,7 @@ import { toast } from "sonner"; import { FileUpload } from "@/components/screen/widgets/FileUpload"; import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters"; import { SaveModal } from "./SaveModal"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; // 파일 데이터 타입 정의 (AttachedFileInfo와 호환) interface FileInfo { @@ -97,6 +98,7 @@ export const InteractiveDataTable: React.FC = ({ style = {}, onRefresh, }) => { + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); @@ -411,6 +413,29 @@ export const InteractiveDataTable: React.FC = ({ async (page: number = 1, searchParams: Record = {}) => { if (!component.tableName) return; + // 프리뷰 모드에서는 샘플 데이터만 표시 + if (isPreviewMode) { + const sampleData = Array.from({ length: 3 }, (_, i) => { + const sample: Record = { id: i + 1 }; + component.columns.forEach((col) => { + if (col.type === "number") { + sample[col.key] = Math.floor(Math.random() * 1000); + } else if (col.type === "boolean") { + sample[col.key] = i % 2 === 0 ? "Y" : "N"; + } else { + sample[col.key] = `샘플 ${col.label} ${i + 1}`; + } + }); + return sample; + }); + setData(sampleData); + setTotal(3); + setTotalPages(1); + setCurrentPage(1); + setLoading(false); + return; + } + setLoading(true); try { const result = await tableTypeApi.getTableData(component.tableName, { @@ -1792,21 +1817,53 @@ export const InteractiveDataTable: React.FC = ({ {/* CRUD 버튼들 */} {component.enableAdd && ( - )} {component.enableEdit && selectedRows.size === 1 && ( - )} {component.enableDelete && selectedRows.size > 0 && ( - diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index cafd611a..a4e47c1b 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -45,6 +45,7 @@ import { UnifiedColumnInfo as ColumnInfo } from "@/types"; import { isFileComponent } from "@/lib/utils/componentTypeUtils"; import { buildGridClasses } from "@/lib/constants/columnSpans"; import { cn } from "@/lib/utils"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; interface InteractiveScreenViewerProps { component: ComponentData; @@ -86,6 +87,7 @@ export const InteractiveScreenViewer: React.FC = ( return
; } + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기 const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -211,6 +213,11 @@ export const InteractiveScreenViewer: React.FC = ( // 폼 데이터 업데이트 const updateFormData = (fieldName: string, value: any) => { + // 프리뷰 모드에서는 데이터 업데이트 하지 않음 + if (isPreviewMode) { + return; + } + // console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`); // 항상 로컬 상태도 업데이트 @@ -837,6 +844,12 @@ export const InteractiveScreenViewer: React.FC = ( }); const handleFileChange = async (e: React.ChangeEvent) => { + // 프리뷰 모드에서는 파일 업로드 차단 + if (isPreviewMode) { + e.target.value = ""; // 파일 선택 취소 + return; + } + const files = e.target.files; const fieldName = widget.columnName || widget.id; @@ -1155,6 +1168,11 @@ export const InteractiveScreenViewer: React.FC = ( const config = widget.webTypeConfig as ButtonTypeConfig | undefined; const handleButtonClick = async () => { + // 프리뷰 모드에서는 버튼 동작 차단 + if (isPreviewMode) { + return; + } + const actionType = config?.actionType || "save"; try { diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index bca6ca7a..c616e940 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -17,6 +17,7 @@ import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/ import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 import "@/lib/registry/components/ButtonRenderer"; @@ -47,6 +48,7 @@ export const InteractiveScreenViewerDynamic: React.FC { + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -405,7 +407,7 @@ export const InteractiveScreenViewerDynamic: React.FC { // console.log("📝 실제 화면 파일 업로드 완료:", data); @@ -486,50 +489,54 @@ export const InteractiveScreenViewerDynamic: React.FC { // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)"); - window.dispatchEvent(new CustomEvent('globalFileStateChanged', { - detail: { ...eventDetail, delayed: true } - })); + window.dispatchEvent( + new CustomEvent("globalFileStateChanged", { + detail: { ...eventDetail, delayed: true }, + }), + ); }, 100); - + setTimeout(() => { // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)"); - window.dispatchEvent(new CustomEvent('globalFileStateChanged', { - detail: { ...eventDetail, delayed: true, attempt: 2 } - })); + window.dispatchEvent( + new CustomEvent("globalFileStateChanged", { + detail: { ...eventDetail, delayed: true, attempt: 2 }, + }), + ); }, 500); } }} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 7a0c6004..c4eaa89d 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -70,6 +70,7 @@ import { findAllButtonGroups, } from "@/lib/utils/flowButtonGroupUtils"; import { FlowButtonGroupDialog } from "./dialogs/FlowButtonGroupDialog"; +import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; // 새로운 통합 UI 컴포넌트 import { LeftUnifiedToolbar, defaultToolbarButtons } from "./toolbar/LeftUnifiedToolbar"; @@ -4102,786 +4103,789 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } return ( -
- {/* 상단 슬림 툴바 */} - - {/* 메인 컨테이너 (좌측 툴바 + 패널들 + 캔버스) */} -
- {/* 좌측 통합 툴바 */} - + +
+ {/* 상단 슬림 툴바 */} + + {/* 메인 컨테이너 (좌측 툴바 + 패널들 + 캔버스) */} +
+ {/* 좌측 통합 툴바 */} + - {/* 열린 패널들 (좌측에서 우측으로 누적) */} - {panelStates.components?.isOpen && ( -
-
-

컴포넌트

- -
-
- { - const dragData = { - type: column ? "column" : "table", - table, - column, - }; - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - selectedTableName={selectedScreen.tableName} - placedColumns={placedColumns} - /> -
-
- )} - - {panelStates.properties?.isOpen && ( -
-
-

속성

- -
-
- 0 ? tables[0] : undefined} - currentTableName={selectedScreen?.tableName} - dragState={dragState} - onStyleChange={(style) => { - if (selectedComponent) { - updateComponentProperty(selectedComponent.id, "style", style); - } - }} - currentResolution={screenResolution} - onResolutionChange={handleResolutionChange} - allComponents={layout.components} // 🆕 플로우 위젯 감지용 - /> -
-
- )} - - {/* 스타일과 해상도 패널은 속성 패널의 탭으로 통합됨 */} - - {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} -
- {/* Pan 모드 안내 - 제거됨 */} - {/* 줌 레벨 표시 */} -
- 🔍 {Math.round(zoomLevel * 100)}% -
- {/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */} - {(() => { - // 선택된 컴포넌트들 - const selectedComps = layout.components.filter((c) => groupState.selectedComponents.includes(c.id)); - - // 버튼 컴포넌트만 필터링 - const selectedButtons = selectedComps.filter((comp) => areAllButtons([comp])); - - // 플로우 그룹에 속한 버튼이 있는지 확인 - const hasFlowGroupButton = selectedButtons.some((btn) => { - const flowConfig = (btn as any).webTypeConfig?.flowVisibilityConfig; - return flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId; - }); - - // 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시 - const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton); - - if (!shouldShow) return null; - - return ( -
-
-
- - - - - - {selectedButtons.length}개 버튼 선택됨 -
- - {/* 그룹 생성 버튼 (2개 이상 선택 시) */} - {selectedButtons.length >= 2 && ( - - )} - - {/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */} - {hasFlowGroupButton && ( - - )} - - {/* 상태 표시 */} - {hasFlowGroupButton &&

✓ 플로우 그룹 버튼

} -
+ {/* 열린 패널들 (좌측에서 우측으로 누적) */} + {panelStates.components?.isOpen && ( +
+
+

컴포넌트

+
- ); - })()} - {/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */} -
- {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */} +
+ { + const dragData = { + type: column ? "column" : "table", + table, + column, + }; + e.dataTransfer.setData("application/json", JSON.stringify(dragData)); + }} + selectedTableName={selectedScreen.tableName} + placedColumns={placedColumns} + /> +
+
+ )} + + {panelStates.properties?.isOpen && ( +
+
+

속성

+ +
+
+ 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + dragState={dragState} + onStyleChange={(style) => { + if (selectedComponent) { + updateComponentProperty(selectedComponent.id, "style", style); + } + }} + currentResolution={screenResolution} + onResolutionChange={handleResolutionChange} + allComponents={layout.components} // 🆕 플로우 위젯 감지용 + /> +
+
+ )} + + {/* 스타일과 해상도 패널은 속성 패널의 탭으로 통합됨 */} + + {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} +
+ {/* Pan 모드 안내 - 제거됨 */} + {/* 줌 레벨 표시 */} +
+ 🔍 {Math.round(zoomLevel * 100)}% +
+ {/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */} + {(() => { + // 선택된 컴포넌트들 + const selectedComps = layout.components.filter((c) => groupState.selectedComponents.includes(c.id)); + + // 버튼 컴포넌트만 필터링 + const selectedButtons = selectedComps.filter((comp) => areAllButtons([comp])); + + // 플로우 그룹에 속한 버튼이 있는지 확인 + const hasFlowGroupButton = selectedButtons.some((btn) => { + const flowConfig = (btn as any).webTypeConfig?.flowVisibilityConfig; + return flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId; + }); + + // 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시 + const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton); + + if (!shouldShow) return null; + + return ( +
+
+
+ + + + + + {selectedButtons.length}개 버튼 선택됨 +
+ + {/* 그룹 생성 버튼 (2개 이상 선택 시) */} + {selectedButtons.length >= 2 && ( + + )} + + {/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */} + {hasFlowGroupButton && ( + + )} + + {/* 상태 표시 */} + {hasFlowGroupButton &&

✓ 플로우 그룹 버튼

} +
+
+ ); + })()} + {/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */}
+ {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
{ - if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { - setSelectedComponent(null); - setGroupState((prev) => ({ ...prev, selectedComponents: [] })); - } - }} - onMouseDown={(e) => { - // Pan 모드가 아닐 때만 다중 선택 시작 - if (e.target === e.currentTarget && !isPanMode) { - startSelectionDrag(e); - } - }} - onDragOver={(e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - }} - onDrop={(e) => { - e.preventDefault(); - // console.log("🎯 캔버스 드롭 이벤트 발생"); - handleDrop(e); + className="bg-background border-border border shadow-lg" + style={{ + width: `${screenResolution.width}px`, + height: `${screenResolution.height}px`, + minWidth: `${screenResolution.width}px`, + maxWidth: `${screenResolution.width}px`, + minHeight: `${screenResolution.height}px`, + flexShrink: 0, + transform: `scale(${zoomLevel})`, + transformOrigin: "top center", }} > - {/* 격자 라인 */} - {gridLines.map((line, index) => ( -
- ))} - - {/* 컴포넌트들 */} - {(() => { - // 🆕 플로우 버튼 그룹 감지 및 처리 - const topLevelComponents = layout.components.filter((component) => !component.parentId); - - // auto-compact 모드의 버튼들을 그룹별로 묶기 - const buttonGroups: Record = {}; - const processedButtonIds = new Set(); - - topLevelComponents.forEach((component) => { - const isButton = - component.type === "button" || - (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)); - - if (isButton) { - const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as - | FlowVisibilityConfig - | undefined; - - if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { - if (!buttonGroups[flowConfig.groupId]) { - buttonGroups[flowConfig.groupId] = []; - } - buttonGroups[flowConfig.groupId].push(component); - processedButtonIds.add(component.id); - } +
{ + if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) { + setSelectedComponent(null); + setGroupState((prev) => ({ ...prev, selectedComponents: [] })); } - }); + }} + onMouseDown={(e) => { + // Pan 모드가 아닐 때만 다중 선택 시작 + if (e.target === e.currentTarget && !isPanMode) { + startSelectionDrag(e); + } + }} + onDragOver={(e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }} + onDrop={(e) => { + e.preventDefault(); + // console.log("🎯 캔버스 드롭 이벤트 발생"); + handleDrop(e); + }} + > + {/* 격자 라인 */} + {gridLines.map((line, index) => ( +
+ ))} - // 그룹에 속하지 않은 일반 컴포넌트들 - const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + {/* 컴포넌트들 */} + {(() => { + // 🆕 플로우 버튼 그룹 감지 및 처리 + const topLevelComponents = layout.components.filter((component) => !component.parentId); - return ( - <> - {/* 일반 컴포넌트들 */} - {regularComponents.map((component) => { - const children = - component.type === "group" - ? layout.components.filter((child) => child.parentId === component.id) - : []; + // auto-compact 모드의 버튼들을 그룹별로 묶기 + const buttonGroups: Record = {}; + const processedButtonIds = new Set(); - // 드래그 중 시각적 피드백 (다중 선택 지원) - const isDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === component.id; - const isBeingDragged = - dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); + topLevelComponents.forEach((component) => { + const isButton = + component.type === "button" || + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)); - let displayComponent = component; + if (isButton) { + const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as + | FlowVisibilityConfig + | undefined; - if (isBeingDragged) { - if (isDraggingThis) { - // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 - displayComponent = { - ...component, - position: dragState.currentPosition, - style: { - ...component.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } else { - // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 - const originalComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === component.id, - ); - if (originalComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { + if (!buttonGroups[flowConfig.groupId]) { + buttonGroups[flowConfig.groupId] = []; + } + buttonGroups[flowConfig.groupId].push(component); + processedButtonIds.add(component.id); + } + } + }); + // 그룹에 속하지 않은 일반 컴포넌트들 + const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + + return ( + <> + {/* 일반 컴포넌트들 */} + {regularComponents.map((component) => { + const children = + component.type === "group" + ? layout.components.filter((child) => child.parentId === component.id) + : []; + + // 드래그 중 시각적 피드백 (다중 선택 지원) + const isDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === component.id; + const isBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); + + let displayComponent = component; + + if (isBeingDragged) { + if (isDraggingThis) { + // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 displayComponent = { ...component, - position: { - x: originalComponent.position.x + deltaX, - y: originalComponent.position.y + deltaY, - z: originalComponent.position.z || 1, - } as Position, + position: dragState.currentPosition, style: { ...component.style, opacity: 0.8, + transform: "scale(1.02)", transition: "none", - zIndex: 40, // 주 컴포넌트보다 약간 낮게 + zIndex: 50, }, }; + } else { + // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 + const originalComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === component.id, + ); + if (originalComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayComponent = { + ...component, + position: { + x: originalComponent.position.x + deltaX, + y: originalComponent.position.y + deltaY, + z: originalComponent.position.z || 1, + } as Position, + style: { + ...component.style, + opacity: 0.8, + transition: "none", + zIndex: 40, // 주 컴포넌트보다 약간 낮게 + }, + }; + } } } - } - // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 - const globalFileState = - typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; - const globalFiles = globalFileState[component.id] || []; - const componentFiles = (component as any).uploadedFiles || []; - const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; + // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 + const globalFileState = + typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; + const globalFiles = globalFileState[component.id] || []; + const componentFiles = (component as any).uploadedFiles || []; + const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; - return ( - handleComponentClick(component, e)} - onDoubleClick={(e) => handleComponentDoubleClick(component, e)} - onDragStart={(e) => startComponentDrag(component, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) - onConfigChange={(config) => { - // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); + return ( + handleComponentClick(component, e)} + onDoubleClick={(e) => handleComponentDoubleClick(component, e)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); - // 컴포넌트의 componentConfig 업데이트 - const updatedComponents = layout.components.map((comp) => { - if (comp.id === component.id) { - return { - ...comp, - componentConfig: { - ...comp.componentConfig, - ...config, - }, - }; - } - return comp; - }); + // 컴포넌트의 componentConfig 업데이트 + const updatedComponents = layout.components.map((comp) => { + if (comp.id === component.id) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + ...config, + }, + }; + } + return comp; + }); - const newLayout = { - ...layout, - components: updatedComponents, - }; + const newLayout = { + ...layout, + components: updatedComponents, + }; - setLayout(newLayout); - saveToHistory(newLayout); + setLayout(newLayout); + saveToHistory(newLayout); - console.log("✅ 컴포넌트 설정 업데이트 완료:", { - componentId: component.id, - updatedConfig: config, - }); - }} - > - {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} - {(component.type === "group" || - component.type === "container" || - component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // 자식 컴포넌트에도 드래그 피드백 적용 - const isChildDraggingThis = - dragState.isDragging && dragState.draggedComponent?.id === child.id; - const isChildBeingDragged = + console.log("✅ 컴포넌트 설정 업데이트 완료:", { + componentId: component.id, + updatedConfig: config, + }); + }} + > + {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} + {(component.type === "group" || + component.type === "container" || + component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트에도 드래그 피드백 적용 + const isChildDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === child.id; + const isChildBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); + + let displayChild = child; + + if (isChildBeingDragged) { + if (isChildDraggingThis) { + // 주 드래그 자식 컴포넌트 + displayChild = { + ...child, + position: dragState.currentPosition, + style: { + ...child.style, + opacity: 0.8, + transform: "scale(1.02)", + transition: "none", + zIndex: 50, + }, + }; + } else { + // 다른 선택된 자식 컴포넌트들 + const originalChildComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === child.id, + ); + if (originalChildComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayChild = { + ...child, + position: { + x: originalChildComponent.position.x + deltaX, + y: originalChildComponent.position.y + deltaY, + z: originalChildComponent.position.z || 1, + } as Position, + style: { + ...child.style, + opacity: 0.8, + transition: "none", + zIndex: 8888, + }, + }; + } + } + } + + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...displayChild, + position: { + x: displayChild.position.x - component.position.x, + y: displayChild.position.y - component.position.y, + z: displayChild.position.z || 1, + }, + }; + + return ( + f.objid) || [])}`} + component={relativeChildComponent} + isSelected={ + selectedComponent?.id === child.id || + groupState.selectedComponents.includes(child.id) + } + isDesignMode={true} // 편집 모드로 설정 + onClick={(e) => handleComponentClick(child, e)} + onDoubleClick={(e) => handleComponentDoubleClick(child, e)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} + /> + ); + })} + + ); + })} + + {/* 🆕 플로우 버튼 그룹들 */} + {Object.entries(buttonGroups).map(([groupId, buttons]) => { + if (buttons.length === 0) return null; + + const firstButton = buttons[0]; + const groupConfig = (firstButton as any).webTypeConfig + ?.flowVisibilityConfig as FlowVisibilityConfig; + + // 🔧 그룹의 위치 및 크기 계산 + // 모든 버튼이 같은 위치(groupX, groupY)에 배치되어 있으므로 + // 첫 번째 버튼의 위치를 그룹 시작점으로 사용 + const direction = groupConfig.groupDirection || "horizontal"; + const gap = groupConfig.groupGap ?? 8; + const align = groupConfig.groupAlign || "start"; + + const groupPosition = { + x: buttons[0].position.x, + y: buttons[0].position.y, + z: buttons[0].position.z || 2, + }; + + // 버튼들의 실제 크기 계산 + let groupWidth = 0; + let groupHeight = 0; + + if (direction === "horizontal") { + // 가로 정렬: 모든 버튼의 너비 + 간격 + groupWidth = buttons.reduce((total, button, index) => { + const buttonWidth = button.size?.width || 100; + const gapWidth = index < buttons.length - 1 ? gap : 0; + return total + buttonWidth + gapWidth; + }, 0); + groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); + } else { + // 세로 정렬 + groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); + groupHeight = buttons.reduce((total, button, index) => { + const buttonHeight = button.size?.height || 40; + const gapHeight = index < buttons.length - 1 ? gap : 0; + return total + buttonHeight + gapHeight; + }, 0); + } + + // 🆕 그룹 전체가 선택되었는지 확인 + const isGroupSelected = buttons.every( + (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), + ); + const hasAnySelected = buttons.some( + (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), + ); + + return ( +
+ { + // 드래그 피드백 + const isDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === button.id; + const isBeingDragged = dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); + dragState.draggedComponents.some((dragComp) => dragComp.id === button.id); - let displayChild = child; + let displayButton = button; - if (isChildBeingDragged) { - if (isChildDraggingThis) { - // 주 드래그 자식 컴포넌트 - displayChild = { - ...child, + if (isBeingDragged) { + if (isDraggingThis) { + displayButton = { + ...button, position: dragState.currentPosition, style: { - ...child.style, + ...button.style, opacity: 0.8, transform: "scale(1.02)", transition: "none", zIndex: 50, }, }; - } else { - // 다른 선택된 자식 컴포넌트들 - const originalChildComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === child.id, - ); - if (originalChildComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; - - displayChild = { - ...child, - position: { - x: originalChildComponent.position.x + deltaX, - y: originalChildComponent.position.y + deltaY, - z: originalChildComponent.position.z || 1, - } as Position, - style: { - ...child.style, - opacity: 0.8, - transition: "none", - zIndex: 8888, - }, - }; - } } } - // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 - const relativeChildComponent = { - ...displayChild, + // 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리) + const relativeButton = { + ...displayButton, position: { - x: displayChild.position.x - component.position.x, - y: displayChild.position.y - component.position.y, - z: displayChild.position.z || 1, + x: 0, + y: 0, + z: displayButton.position.z || 1, }, }; return ( - f.objid) || [])}`} - component={relativeChildComponent} - isSelected={ - selectedComponent?.id === child.id || - groupState.selectedComponents.includes(child.id) - } - isDesignMode={true} // 편집 모드로 설정 - onClick={(e) => handleComponentClick(child, e)} - onDoubleClick={(e) => handleComponentDoubleClick(child, e)} - onDragStart={(e) => startComponentDrag(child, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (자식 컴포넌트용) - onConfigChange={(config) => { - // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); - // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 +
- ); - })} - - ); - })} + onMouseDown={(e) => { + // 클릭이 아닌 드래그인 경우에만 드래그 시작 + e.preventDefault(); + e.stopPropagation(); - {/* 🆕 플로우 버튼 그룹들 */} - {Object.entries(buttonGroups).map(([groupId, buttons]) => { - if (buttons.length === 0) return null; + const startX = e.clientX; + const startY = e.clientY; + let isDragging = false; + let dragStarted = false; - const firstButton = buttons[0]; - const groupConfig = (firstButton as any).webTypeConfig - ?.flowVisibilityConfig as FlowVisibilityConfig; + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaX = Math.abs(moveEvent.clientX - startX); + const deltaY = Math.abs(moveEvent.clientY - startY); - // 🔧 그룹의 위치 및 크기 계산 - // 모든 버튼이 같은 위치(groupX, groupY)에 배치되어 있으므로 - // 첫 번째 버튼의 위치를 그룹 시작점으로 사용 - const direction = groupConfig.groupDirection || "horizontal"; - const gap = groupConfig.groupGap ?? 8; - const align = groupConfig.groupAlign || "start"; + // 5픽셀 이상 움직이면 드래그로 간주 + if ((deltaX > 5 || deltaY > 5) && !dragStarted) { + isDragging = true; + dragStarted = true; - const groupPosition = { - x: buttons[0].position.x, - y: buttons[0].position.y, - z: buttons[0].position.z || 2, - }; + // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 + if (!e.shiftKey) { + const buttonIds = buttons.map((b) => b.id); + setGroupState((prev) => ({ + ...prev, + selectedComponents: buttonIds, + })); + } - // 버튼들의 실제 크기 계산 - let groupWidth = 0; - let groupHeight = 0; - - if (direction === "horizontal") { - // 가로 정렬: 모든 버튼의 너비 + 간격 - groupWidth = buttons.reduce((total, button, index) => { - const buttonWidth = button.size?.width || 100; - const gapWidth = index < buttons.length - 1 ? gap : 0; - return total + buttonWidth + gapWidth; - }, 0); - groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40)); - } else { - // 세로 정렬 - groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100)); - groupHeight = buttons.reduce((total, button, index) => { - const buttonHeight = button.size?.height || 40; - const gapHeight = index < buttons.length - 1 ? gap : 0; - return total + buttonHeight + gapHeight; - }, 0); - } - - // 🆕 그룹 전체가 선택되었는지 확인 - const isGroupSelected = buttons.every( - (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), - ); - const hasAnySelected = buttons.some( - (btn) => selectedComponent?.id === btn.id || groupState.selectedComponents.includes(btn.id), - ); - - return ( -
- { - // 드래그 피드백 - const isDraggingThis = - dragState.isDragging && dragState.draggedComponent?.id === button.id; - const isBeingDragged = - dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === button.id); - - let displayButton = button; - - if (isBeingDragged) { - if (isDraggingThis) { - displayButton = { - ...button, - position: dragState.currentPosition, - style: { - ...button.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } - } - - // 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리) - const relativeButton = { - ...displayButton, - position: { - x: 0, - y: 0, - z: displayButton.position.z || 1, - }, - }; - - return ( -
{ - // 클릭이 아닌 드래그인 경우에만 드래그 시작 - e.preventDefault(); - e.stopPropagation(); - - const startX = e.clientX; - const startY = e.clientY; - let isDragging = false; - let dragStarted = false; - - const handleMouseMove = (moveEvent: MouseEvent) => { - const deltaX = Math.abs(moveEvent.clientX - startX); - const deltaY = Math.abs(moveEvent.clientY - startY); - - // 5픽셀 이상 움직이면 드래그로 간주 - if ((deltaX > 5 || deltaY > 5) && !dragStarted) { - isDragging = true; - dragStarted = true; - - // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 - if (!e.shiftKey) { - const buttonIds = buttons.map((b) => b.id); - setGroupState((prev) => ({ - ...prev, - selectedComponents: buttonIds, - })); + // 드래그 시작 + startComponentDrag(button, e as any); } + }; - // 드래그 시작 - startComponentDrag(button, e as any); - } - }; + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - - // 드래그가 아니면 클릭으로 처리 - if (!isDragging) { - // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 - if (!e.shiftKey) { - const buttonIds = buttons.map((b) => b.id); - setGroupState((prev) => ({ - ...prev, - selectedComponents: buttonIds, - })); + // 드래그가 아니면 클릭으로 처리 + if (!isDragging) { + // Shift 키를 누르지 않았으면 같은 그룹의 버튼들도 모두 선택 + if (!e.shiftKey) { + const buttonIds = buttons.map((b) => b.id); + setGroupState((prev) => ({ + ...prev, + selectedComponents: buttonIds, + })); + } + handleComponentClick(button, e); } - handleComponentClick(button, e); - } - }; + }; - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }} - onDoubleClick={(e) => { - e.stopPropagation(); - handleComponentDoubleClick(button, e); - }} - className={ - selectedComponent?.id === button.id || - groupState.selectedComponents.includes(button.id) - ? "outline-1 outline-offset-1 outline-blue-400" - : "" - } - > - {/* 그룹 내부에서는 DynamicComponentRenderer로 직접 렌더링 */} -
- {}} - /> + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + handleComponentDoubleClick(button, e); + }} + className={ + selectedComponent?.id === button.id || + groupState.selectedComponents.includes(button.id) + ? "outline-1 outline-offset-1 outline-blue-400" + : "" + } + > + {/* 그룹 내부에서는 DynamicComponentRenderer로 직접 렌더링 */} +
+ {}} + /> +
-
- ); - }} - /> -
- ); - })} - - ); - })()} + ); + }} + /> +
+ ); + })} + + ); + })()} - {/* 드래그 선택 영역 */} - {selectionDrag.isSelecting && ( -
- )} + {/* 드래그 선택 영역 */} + {selectionDrag.isSelecting && ( +
+ )} - {/* 빈 캔버스 안내 */} - {layout.components.length === 0 && ( -
-
-
- -
-

캔버스가 비어있습니다

-

- 좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요 -

-
-

- 단축키: T(테이블), M(템플릿), P(속성), S(스타일), - R(격자), D(상세설정), E(해상도) -

-

- 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), - Ctrl+Z(실행취소), Delete(삭제) -

-

- ⚠️ - 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 + {/* 빈 캔버스 안내 */} + {layout.components.length === 0 && ( +

+
+
+ +
+

캔버스가 비어있습니다

+

+ 좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요

+
+

+ 단축키: T(테이블), M(템플릿), P(속성), S(스타일), + R(격자), D(상세설정), E(해상도) +

+

+ 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), + Ctrl+Z(실행취소), Delete(삭제) +

+

+ ⚠️ + 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 +

+
-
- )} + )} +
-
-
{" "} - {/* 🔥 줌 래퍼 닫기 */} -
-
{" "} - {/* 메인 컨테이너 닫기 */} - {/* 🆕 플로우 버튼 그룹 생성 다이얼로그 */} - - {/* 모달들 */} - {/* 메뉴 할당 모달 */} - {showMenuAssignmentModal && selectedScreen && ( - setShowMenuAssignmentModal(false)} - onAssignmentComplete={() => { - // 모달을 즉시 닫지 않고, MenuAssignmentModal이 3초 후 자동으로 닫히도록 함 - // setShowMenuAssignmentModal(false); - // toast.success("메뉴에 화면이 할당되었습니다."); - }} - onBackToList={onBackToList} +
{" "} + {/* 🔥 줌 래퍼 닫기 */} +
+
{" "} + {/* 메인 컨테이너 닫기 */} + {/* 🆕 플로우 버튼 그룹 생성 다이얼로그 */} + - )} - {/* 파일첨부 상세 모달 */} - {showFileAttachmentModal && selectedFileComponent && ( - { - setShowFileAttachmentModal(false); - setSelectedFileComponent(null); - }} - component={selectedFileComponent} - screenId={selectedScreen.screenId} - /> - )} -
+ {/* 모달들 */} + {/* 메뉴 할당 모달 */} + {showMenuAssignmentModal && selectedScreen && ( + setShowMenuAssignmentModal(false)} + onAssignmentComplete={() => { + // 모달을 즉시 닫지 않고, MenuAssignmentModal이 3초 후 자동으로 닫히도록 함 + // setShowMenuAssignmentModal(false); + // toast.success("메뉴에 화면이 할당되었습니다."); + }} + onBackToList={onBackToList} + /> + )} + {/* 파일첨부 상세 모달 */} + {showFileAttachmentModal && selectedFileComponent && ( + { + setShowFileAttachmentModal(false); + setSelectedFileComponent(null); + }} + component={selectedFileComponent} + screenId={selectedScreen.screenId} + /> + )} +
+ ); } diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 382ae20c..75a1662c 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -448,10 +448,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr {screens.map((screen) => ( handleScreenSelect(screen)} + onClick={() => onDesignScreen(screen)} >
diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index cd0c052f..e512ad21 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -37,6 +37,7 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; +import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; interface FlowWidgetProps { component: FlowComponent; @@ -53,6 +54,8 @@ export function FlowWidget({ flowRefreshKey, onFlowRefresh, }: FlowWidgetProps) { + const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 + // 🆕 전역 상태 관리 const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep); const resetFlow = useFlowStepStore((state) => state.resetFlow); @@ -312,6 +315,57 @@ export function FlowWidget({ setLoading(true); setError(null); + // 프리뷰 모드에서는 샘플 데이터만 표시 + if (isPreviewMode) { + console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시"); + setFlowData({ + id: flowId || 0, + flowName: flowName || "샘플 플로우", + description: "프리뷰 모드 샘플", + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FlowDefinition); + + const sampleSteps: FlowStep[] = [ + { + id: 1, + flowId: flowId || 0, + stepName: "시작 단계", + stepOrder: 1, + stepType: "start", + stepConfig: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: 2, + flowId: flowId || 0, + stepName: "진행 중", + stepOrder: 2, + stepType: "process", + stepConfig: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: 3, + flowId: flowId || 0, + stepName: "완료", + stepOrder: 3, + stepType: "end", + stepConfig: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]; + setSteps(sampleSteps); + setStepCounts({ 1: 5, 2: 3, 3: 2 }); + setConnections([]); + setLoading(false); + return; + } + // 플로우 정보 조회 const flowResponse = await getFlowById(flowId!); if (!flowResponse.success || !flowResponse.data) { @@ -413,6 +467,11 @@ export function FlowWidget({ // 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가) const handleStepClick = async (stepId: number, stepName: string) => { + // 프리뷰 모드에서는 스텝 클릭 차단 + if (isPreviewMode) { + return; + } + // 외부 콜백 실행 if (onStepClick) { onStepClick(stepId, stepName); @@ -485,6 +544,11 @@ export function FlowWidget({ // 체크박스 토글 const toggleRowSelection = (rowIndex: number) => { + // 프리뷰 모드에서는 행 선택 차단 + if (isPreviewMode) { + return; + } + const newSelected = new Set(selectedRows); if (newSelected.has(rowIndex)) { newSelected.delete(rowIndex); @@ -675,7 +739,13 @@ export function FlowWidget({
@@ -182,14 +129,14 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd type="color" value={localStyle.backgroundColor || "#ffffff"} onChange={(e) => handleStyleChange("backgroundColor", e.target.value)} - className="h-8 w-14 p-1" + className="h-8 w-14 p-1 text-xs" /> handleStyleChange("backgroundColor", e.target.value)} placeholder="#ffffff" - className="h-8 flex-1" + className="h-8 flex-1 text-xs" />
@@ -204,7 +151,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="url('image.jpg')" value={localStyle.backgroundImage || ""} onChange={(e) => handleStyleChange("backgroundImage", e.target.value)} - className="h-8" + className="h-8 text-xs" />
@@ -229,14 +176,14 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd type="color" value={localStyle.color || "#000000"} onChange={(e) => handleStyleChange("color", e.target.value)} - className="h-8 w-14 p-1" + className="h-8 w-14 p-1 text-xs" /> handleStyleChange("color", e.target.value)} placeholder="#000000" - className="h-8 flex-1" + className="h-8 flex-1 text-xs" />
@@ -250,7 +197,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="14px" value={localStyle.fontSize || ""} onChange={(e) => handleStyleChange("fontSize", e.target.value)} - className="h-8" + className="h-8 text-xs" />
@@ -264,17 +211,31 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.fontWeight || "normal"} onValueChange={(value) => handleStyleChange("fontWeight", value)} > - + - 보통 - 굵게 - 100 - 400 - 500 - 600 - 700 + + 보통 + + + 굵게 + + + 100 + + + 400 + + + 500 + + + 600 + + + 700 + @@ -286,14 +247,22 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.textAlign || "left"} onValueChange={(value) => handleStyleChange("textAlign", value)} > - + - 왼쪽 - 가운데 - 오른쪽 - 양쪽 + + 왼쪽 + + + 가운데 + + + 오른쪽 + + + 양쪽 + diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 01663c89..6b1669c4 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -515,94 +515,60 @@ export const ButtonConfigPanel: React.FC = ({ {/* 테이블 이력 보기 액션 설정 */} {(component.componentConfig?.action?.type || "save") === "view_table_history" && (
-

📜 테이블 이력 보기 설정

-
- {!config.action?.historyTableName && !currentTableName ? ( -
-

- ⚠️ 먼저 테이블명을 입력하거나, 현재 화면에 테이블을 연결해주세요. -

-
- ) : ( - <> - {!config.action?.historyTableName && currentTableName && ( -
-

- ✓ 현재 화면의 테이블 {currentTableName}을(를) 자동으로 사용합니다. -

-
- )} - - - - - - - - - - 컬럼을 찾을 수 없습니다. - - {tableColumns.map((column) => ( - { - onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue); - setDisplayColumnOpen(false); - }} - className="text-sm" - > - - {column} - - ))} - - - - - - -

- 전체 테이블 이력에서 레코드를 구분하기 위한 컬럼입니다. -
- 예: device_code를 설정하면 이력에 "DTG-001"로 - 표시됩니다. -
이 컬럼으로 검색도 가능합니다. -

- - {tableColumns.length === 0 && !columnsLoading && ( -

- ⚠️ ID 및 날짜 타입 컬럼을 제외한 사용 가능한 컬럼이 없습니다. -

- )} - - )} + + + + + + + + + 컬럼을 찾을 수 없습니다. + + {tableColumns.map((column) => ( + { + onUpdateProperty("componentConfig.action.historyDisplayColumn", currentValue); + setDisplayColumnOpen(false); + }} + className="text-sm" + > + + {column} + + ))} + + + + +
)} @@ -693,6 +659,7 @@ export const ButtonConfigPanel: React.FC = ({ setLocalInputs((prev) => ({ ...prev, targetUrl: newValue })); onUpdateProperty("componentConfig.action.targetUrl", newValue); }} + className="h-8 text-xs" />

URL을 입력하면 화면 선택보다 우선 적용됩니다

diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index b341cfb2..893bef5f 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -21,14 +21,14 @@ interface ComponentsPanelProps { placedColumns?: Set; // 이미 배치된 컬럼명 집합 } -export function ComponentsPanel({ - className, - tables = [], - searchTerm = "", - onSearchChange, +export function ComponentsPanel({ + className, + tables = [], + searchTerm = "", + onSearchChange, onTableDragStart, selectedTableName, - placedColumns + placedColumns, }: ComponentsPanelProps) { const [searchQuery, setSearchQuery] = useState(""); @@ -176,8 +176,8 @@ export function ComponentsPanel({ {/* 카테고리 탭 */} - - + + 테이블 diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 1320778e..ee50e54d 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -1150,7 +1150,7 @@ export const DetailSettingsPanel: React.FC = ({
- + @@ -93,7 +93,7 @@ const ResolutionPanel: React.FC = ({ currentResolution, on {SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
- + {resolution.name}
@@ -125,7 +125,7 @@ const ResolutionPanel: React.FC = ({ currentResolution, on
사용자 정의
- + 사용자 정의
@@ -139,43 +139,33 @@ const ResolutionPanel: React.FC = ({ currentResolution, on
- + setCustomWidth(e.target.value)} placeholder="1920" min="1" + className="h-8 text-xs" />
- + setCustomHeight(e.target.value)} placeholder="1080" min="1" + className="h-8 text-xs" />
-
)} - - {/* 해상도 정보 */} -
-
- 화면 비율: - {(currentResolution.width / currentResolution.height).toFixed(2)}:1 -
-
- 총 픽셀: - {(currentResolution.width * currentResolution.height).toLocaleString()} -
-
); }; diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index bb6ccf66..6e992de0 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -202,15 +202,6 @@ export const UnifiedPropertiesPanel: React.FC = ({ return (
- {/* 컴포넌트 정보 - 간소화 */} -
-
- - {selectedComponent.type} -
- {selectedComponent.id.slice(0, 8)} -
- {/* 라벨 + 최소 높이 (같은 행) */}
@@ -300,24 +291,15 @@ export const UnifiedPropertiesPanel: React.FC = ({
)} - {/* 위치 */} -
-
- - -
-
- - -
-
- - handleUpdate("position.z", parseInt(e.target.value) || 1)} - /> -
+ {/* Z-Index */} +
+ + handleUpdate("position.z", parseInt(e.target.value) || 1)} + className="h-6 text-[10px]" + />
{/* 라벨 스타일 */} @@ -389,33 +371,6 @@ export const UnifiedPropertiesPanel: React.FC = ({
)}
- - {/* 액션 버튼 */} - -
- {onCopyComponent && ( - - )} - {onDeleteComponent && ( - - )} -
); }; @@ -513,7 +468,7 @@ export const UnifiedPropertiesPanel: React.FC = ({
handleUpdate("webType", value)}> - + diff --git a/frontend/components/screen/panels/webtype-configs/CheckboxTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/CheckboxTypeConfigPanel.tsx index abc400bb..4a6016aa 100644 --- a/frontend/components/screen/panels/webtype-configs/CheckboxTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/CheckboxTypeConfigPanel.tsx @@ -90,11 +90,11 @@ export const CheckboxTypeConfigPanel: React.FC = ( const newConfig = JSON.parse(JSON.stringify(currentValues)); // console.log("☑️ CheckboxTypeConfig 업데이트:", { - // key, - // value, - // oldConfig: safeConfig, - // newConfig, - // localValues, + // key, + // value, + // oldConfig: safeConfig, + // newConfig, + // localValues, // }); setTimeout(() => { @@ -122,7 +122,7 @@ export const CheckboxTypeConfigPanel: React.FC = ( 라벨 위치 updateConfig("language", value)}> - + @@ -140,7 +140,7 @@ export const CodeTypeConfigPanel: React.FC = ({ config 테마 updateConfig("displayFormat", value)}> - + @@ -317,7 +317,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
-
+
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
@@ -334,7 +334,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
{/* 안내 메시지 */} -
+
엔터티 타입 설정 가이드
참조 테이블: 데이터를 가져올 다른 테이블 이름 diff --git a/frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx index 5ad60139..d317c049 100644 --- a/frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/NumberTypeConfigPanel.tsx @@ -89,12 +89,12 @@ export const NumberTypeConfigPanel: React.FC = ({ co const newConfig = JSON.parse(JSON.stringify(currentValues)); // console.log("🔢 NumberTypeConfig 업데이트:", { - // key, - // value, - // oldConfig: safeConfig, - // newConfig, - // localValues, - // timestamp: new Date().toISOString(), + // key, + // value, + // oldConfig: safeConfig, + // newConfig, + // localValues, + // timestamp: new Date().toISOString(), // }); // 약간의 지연을 두고 업데이트 (배치 업데이트 방지) @@ -111,7 +111,7 @@ export const NumberTypeConfigPanel: React.FC = ({ co 숫자 형식 updateConfig("format", value)}> - + @@ -220,13 +220,13 @@ export const TextTypeConfigPanel: React.FC = ({ config
{localValues.autoInput && ( -
+
setSearchQuery(e.target.value)} + onChange={(e) => { + const value = e.target.value; + setSearchQuery(value); + // 테이블 검색도 함께 업데이트 + if (onSearchChange) { + onSearchChange(value); + } + }} className="h-8 pl-8 text-xs" />
@@ -177,26 +184,42 @@ export function ComponentsPanel({ {/* 카테고리 탭 */} - - + + - 테이블 + 테이블 - + - 입력 + 입력 - + - 액션 + 액션 - + - 표시 + 표시 - + - 레이아웃 + 레이아웃 diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index f75b699e..abeff8d6 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -1,23 +1,8 @@ "use client"; -import React, { useState } from "react"; -import { Button } from "@/components/ui/button"; +import React from "react"; import { Badge } from "@/components/ui/badge"; -import { - Database, - ChevronDown, - ChevronRight, - Type, - Hash, - Calendar, - CheckSquare, - List, - AlignLeft, - Code, - Building, - File, - Search, -} from "lucide-react"; +import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File } from "lucide-react"; import { TableInfo, WebType } from "@/types/screen"; interface TablesPanelProps { @@ -65,23 +50,9 @@ const getWidgetIcon = (widgetType: WebType) => { export const TablesPanel: React.FC = ({ tables, searchTerm, - onSearchChange, onDragStart, - selectedTableName, placedColumns = new Set(), }) => { - const [expandedTables, setExpandedTables] = useState>(new Set()); - - const toggleTable = (tableName: string) => { - const newExpanded = new Set(expandedTables); - if (newExpanded.has(tableName)) { - newExpanded.delete(tableName); - } else { - newExpanded.add(tableName); - } - setExpandedTables(newExpanded); - }; - // 이미 배치된 컬럼을 제외한 테이블 정보 생성 const tablesWithAvailableColumns = tables.map((table) => ({ ...table, @@ -91,137 +62,89 @@ export const TablesPanel: React.FC = ({ }), })); + // 검색어가 있으면 컬럼 필터링 const filteredTables = tablesWithAvailableColumns - .filter((table) => table.columns.length > 0) // 사용 가능한 컬럼이 있는 테이블만 표시 - .filter( - (table) => - table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || - table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())), - ); + .map((table) => { + if (!searchTerm) { + return table; + } + + const searchLower = searchTerm.toLowerCase(); + + // 테이블명이 검색어와 일치하면 모든 컬럼 표시 + if ( + table.tableName.toLowerCase().includes(searchLower) || + (table.tableLabel && table.tableLabel.toLowerCase().includes(searchLower)) + ) { + return table; + } + + // 그렇지 않으면 컬럼명/라벨이 검색어와 일치하는 컬럼만 필터링 + const filteredColumns = table.columns.filter( + (col) => + col.columnName.toLowerCase().includes(searchLower) || + (col.columnLabel && col.columnLabel.toLowerCase().includes(searchLower)), + ); + + return { + ...table, + columns: filteredColumns, + }; + }) + .filter((table) => table.columns.length > 0); // 컬럼이 있는 테이블만 표시 return (
- {/* 헤더 */} -
- {selectedTableName && ( -
-
선택된 테이블
-
- - {selectedTableName} -
-
- )} - - {/* 검색 */} -
- - onSearchChange(e.target.value)} - className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none" - /> -
- -
총 {filteredTables.length}개
-
- - {/* 테이블 목록 */} -
-
- {filteredTables.map((table) => { - const isExpanded = expandedTables.has(table.tableName); - - return ( -
- {/* 테이블 헤더 */} -
toggleTable(table.tableName)} - > -
- {isExpanded ? ( - - ) : ( - - )} - -
-
{table.tableLabel || table.tableName}
-
{table.columns.length}개
-
-
- - + {/* 테이블과 컬럼 평면 목록 */} +
+
+ {filteredTables.map((table) => ( +
+ {/* 테이블 헤더 */} +
+
+ + {table.tableLabel || table.tableName} + + {table.columns.length}개 +
+
- {/* 컬럼 목록 */} - {isExpanded && ( -
-
8 ? "max-h-64 overflow-y-auto" : ""}`}> - {table.columns.map((column, index) => ( -
onDragStart(e, table, column)} - > -
- {getWidgetIcon(column.widgetType)} -
-
- {column.columnLabel || column.columnName} -
-
{column.dataType}
-
-
+ {/* 컬럼 목록 (항상 표시) */} +
+ {table.columns.map((column) => ( +
onDragStart(e, table, column)} + > +
+ {getWidgetIcon(column.widgetType)} +
+
{column.columnLabel || column.columnName}
+
{column.dataType}
+
+
-
- - {column.widgetType} - - {column.required && ( - - 필수 - - )} -
-
- ))} - - {/* 컬럼 수가 많을 때 안내 메시지 */} - {table.columns.length > 8 && ( -
-
- 📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기) -
-
+
+ + {column.widgetType} + + {column.required && ( + + 필수 + )}
- )} + ))}
- ); - })} +
+ ))}
- - {/* 푸터 */} -
-
💡 테이블이나 컬럼을 캔버스로 드래그하세요
-
); }; -- 2.43.0 From 743ae6dbf1520895c65edb71a96e05b5d90c5776 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Oct 2025 17:33:03 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EC=A4=91=EA=B0=84=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 135 +- frontend/components/screen/StyleEditor.tsx | 140 +- .../config-panels/ButtonConfigPanel.tsx | 16 +- .../config-panels/CheckboxConfigPanel.tsx | 24 +- .../screen/config-panels/CodeConfigPanel.tsx | 12 +- .../screen/config-panels/DateConfigPanel.tsx | 12 +- .../config-panels/EntityConfigPanel.tsx | 36 +- .../screen/config-panels/FileConfigPanel.tsx | 12 +- .../FlowVisibilityConfigPanel.tsx | 10 +- .../config-panels/FlowWidgetConfigPanel.tsx | 2 +- .../config-panels/NumberConfigPanel.tsx | 14 +- .../screen/config-panels/RadioConfigPanel.tsx | 22 +- .../config-panels/SelectConfigPanel.tsx | 22 +- .../screen/config-panels/TextConfigPanel.tsx | 12 +- .../config-panels/TextareaConfigPanel.tsx | 14 +- .../screen/dialogs/FlowButtonGroupDialog.tsx | 4 +- .../screen/panels/ComponentsPanel.tsx | 2 +- .../screen/panels/DataTableConfigPanel.tsx | 112 +- .../screen/panels/DetailSettingsPanel.tsx | 75 +- .../screen/panels/FlowButtonGroupPanel.tsx | 4 +- .../components/screen/panels/GridPanel.tsx | 4 +- .../components/screen/panels/LayoutsPanel.tsx | 2 +- .../screen/panels/PropertiesPanel.tsx | 6 +- .../screen/panels/ResolutionPanel.tsx | 15 +- .../screen/panels/RowSettingsPanel.tsx | 4 +- .../screen/panels/TemplatesPanel.tsx | 2 +- .../screen/panels/UnifiedPropertiesPanel.tsx | 173 +- .../screen/panels/WebTypeConfigPanel.tsx | 8 +- .../CheckboxTypeConfigPanel.tsx | 8 +- .../webtype-configs/CodeTypeConfigPanel.tsx | 4 +- .../webtype-configs/DateTypeConfigPanel.tsx | 2 +- .../webtype-configs/EntityTypeConfigPanel.tsx | 6 +- .../webtype-configs/NumberTypeConfigPanel.tsx | 2 +- .../webtype-configs/RadioTypeConfigPanel.tsx | 2 +- .../webtype-configs/SelectTypeConfigPanel.tsx | 6 +- .../webtype-configs/TextTypeConfigPanel.tsx | 4 +- .../TextareaTypeConfigPanel.tsx | 2 +- .../screen/toolbar/LeftUnifiedToolbar.tsx | 22 +- .../components/screen/widgets/FlowWidget.tsx | 2 +- .../components/screen/widgets/InputWidget.tsx | 3 +- .../screen/widgets/SelectWidget.tsx | 2 +- frontend/components/ui/select.tsx | 8 +- .../table-list/TableListConfigPanel.tsx | 1890 ++++++----------- 43 files changed, 1191 insertions(+), 1666 deletions(-) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index c4eaa89d..d669431d 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -87,21 +87,12 @@ interface ScreenDesignerProps { onBackToList: () => void; } -// 패널 설정 (컴포넌트와 편집 2개) +// 패널 설정 (통합 패널 1개) const panelConfigs: PanelConfig[] = [ - // 컴포넌트 패널 (테이블 + 컴포넌트 탭) + // 통합 패널 (컴포넌트 + 편집 탭) { - id: "components", - title: "컴포넌트", - defaultPosition: "left", - defaultWidth: 240, - defaultHeight: 700, - shortcutKey: "c", - }, - // 편집 패널 (속성 + 스타일 & 해상도 탭) - { - id: "properties", - title: "편집", + id: "unified", + title: "패널", defaultPosition: "left", defaultWidth: 240, defaultHeight: 700, @@ -141,14 +132,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const [selectedComponent, setSelectedComponent] = useState(null); - // 컴포넌트 선택 시 속성 패널 자동 열기 + // 컴포넌트 선택 시 통합 패널 자동 열기 const handleComponentSelect = useCallback( (component: ComponentData | null) => { setSelectedComponent(component); - // 컴포넌트가 선택되면 속성 패널 자동 열기 + // 컴포넌트가 선택되면 통합 패널 자동 열기 if (component) { - openPanel("properties"); + openPanel("unified"); } }, [openPanel], @@ -4119,74 +4110,72 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 좌측 통합 툴바 */} - {/* 열린 패널들 (좌측에서 우측으로 누적) */} - {panelStates.components?.isOpen && ( + {/* 통합 패널 */} + {panelStates.unified?.isOpen && (
-
-

컴포넌트

+
+

패널

-
- { - const dragData = { - type: column ? "column" : "table", - table, - column, - }; - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - selectedTableName={selectedScreen.tableName} - placedColumns={placedColumns} - /> +
+ + + + 컴포넌트 + + + 편집 + + + + + { + const dragData = { + type: column ? "column" : "table", + table, + column, + }; + e.dataTransfer.setData("application/json", JSON.stringify(dragData)); + }} + selectedTableName={selectedScreen.tableName} + placedColumns={placedColumns} + /> + + + + 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + dragState={dragState} + onStyleChange={(style) => { + if (selectedComponent) { + updateComponentProperty(selectedComponent.id, "style", style); + } + }} + currentResolution={screenResolution} + onResolutionChange={handleResolutionChange} + allComponents={layout.components} // 🆕 플로우 위젯 감지용 + /> + +
)} - {panelStates.properties?.isOpen && ( -
-
-

속성

- -
-
- 0 ? tables[0] : undefined} - currentTableName={selectedScreen?.tableName} - dragState={dragState} - onStyleChange={(style) => { - if (selectedComponent) { - updateComponentProperty(selectedComponent.id, "style", style); - } - }} - currentResolution={screenResolution} - onResolutionChange={handleResolutionChange} - allComponents={layout.components} // 🆕 플로우 위젯 감지용 - /> -
-
- )} - - {/* 스타일과 해상도 패널은 속성 패널의 탭으로 통합됨 */} - {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */}
{/* Pan 모드 안내 - 제거됨 */} diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index cadeb641..95523901 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -28,17 +28,17 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd }; return ( -
+
{/* 테두리 섹션 */} -
+
- +

테두리

- -
-
-
+ +
+
+
@@ -48,10 +48,11 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="1px" value={localStyle.borderWidth || ""} onChange={(e) => handleStyleChange("borderWidth", e.target.value)} - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -59,42 +60,52 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd value={localStyle.borderStyle || "solid"} onValueChange={(value) => handleStyleChange("borderStyle", value)} > - + - 실선 - 파선 - 점선 - 없음 + + 실선 + + + 파선 + + + 점선 + + + 없음 +
-
-
+
+
-
+
handleStyleChange("borderColor", e.target.value)} - className="h-8 w-14 p-1 text-xs" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("borderColor", e.target.value)} placeholder="#000000" - className="h-8 flex-1 text-xs" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -104,7 +115,8 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="5px" value={localStyle.borderRadius || ""} onChange={(e) => handleStyleChange("borderRadius", e.target.value)} - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
@@ -112,38 +124,40 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
{/* 배경 섹션 */} -
+
- +

배경

- -
-
+ +
+
-
+
handleStyleChange("backgroundColor", e.target.value)} - className="h-8 w-14 p-1 text-xs" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("backgroundColor", e.target.value)} placeholder="#ffffff" - className="h-8 flex-1 text-xs" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
handleStyleChange("backgroundImage", e.target.value)} - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
{/* 텍스트 섹션 */} -
+
- +

텍스트

- -
-
-
+ +
+
+
-
+
handleStyleChange("color", e.target.value)} - className="h-8 w-14 p-1 text-xs" + className="h-6 w-12 p-1" + style={{ fontSize: "12px" }} /> handleStyleChange("color", e.target.value)} placeholder="#000000" - className="h-8 flex-1 text-xs" + className="h-6 flex-1 text-xs" + style={{ fontSize: "12px" }} />
-
+
@@ -197,70 +214,71 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="14px" value={localStyle.fontSize || ""} onChange={(e) => handleStyleChange("fontSize", e.target.value)} - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} />
-
-
@@ -330,7 +330,7 @@ export const CodeConfigPanel: React.FC = ({ value={localConfig.defaultValue || ""} onChange={(e) => updateConfig("defaultValue", e.target.value)} placeholder="기본 코드 내용" - className="font-mono text-xs" + className="font-mono text-xs" style={{ fontSize: "12px" }} rows={4} />
diff --git a/frontend/components/screen/config-panels/DateConfigPanel.tsx b/frontend/components/screen/config-panels/DateConfigPanel.tsx index bbea14d4..7fcacc57 100644 --- a/frontend/components/screen/config-panels/DateConfigPanel.tsx +++ b/frontend/components/screen/config-panels/DateConfigPanel.tsx @@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC = ({ return ( - + 날짜 설정 @@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="날짜를 선택하세요" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC = ({ type={localConfig.showTime ? "datetime-local" : "date"} value={localConfig.minDate || ""} onChange={(e) => updateConfig("minDate", e.target.value)} - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -213,7 +213,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.apiEndpoint || ""} onChange={(e) => updateConfig("apiEndpoint", e.target.value)} placeholder="/api/entities/user" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -232,7 +232,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.valueField || ""} onChange={(e) => updateConfig("valueField", e.target.value)} placeholder="id" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -245,7 +245,7 @@ export const EntityConfigPanel: React.FC = ({ value={localConfig.labelField || ""} onChange={(e) => updateConfig("labelField", e.target.value)} placeholder="name" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -263,13 +263,13 @@ export const EntityConfigPanel: React.FC = ({ value={newFieldName} onChange={(e) => setNewFieldName(e.target.value)} placeholder="필드명" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewFieldLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> updateDisplayField(index, "label", e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC = ({ value={bulkOptions} onChange={(e) => setBulkOptions(e.target.value)} placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu" - className="h-20 text-xs" + className="h-20 text-xs" style={{ fontSize: "12px" }} />
@@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC = ({ value={localConfig.emptyMessage || ""} onChange={(e) => updateConfig("emptyMessage", e.target.value)} placeholder="선택 가능한 옵션이 없습니다" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC = ({ value={newOptionLabel} onChange={(e) => setNewOptionLabel(e.target.value)} placeholder="라벨" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> setNewOptionValue(e.target.value)} placeholder="값" - className="flex-1 text-xs" + className="flex-1 text-xs" style={{ fontSize: "12px" }} /> @@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC = ({ value={bulkOptions} onChange={(e) => setBulkOptions(e.target.value)} placeholder="한 줄당 하나씩 입력하세요. 라벨만 입력하면 값과 동일하게 설정됩니다. 라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다. 예시: 서울 부산 대구시|daegu" - className="h-20 text-xs" + className="h-20 text-xs" style={{ fontSize: "12px" }} />
@@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC = ({ onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)} placeholder="0" min="0" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -102,7 +102,7 @@ export const TextConfigPanel: React.FC = ({ onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)} placeholder="100" min="1" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC = ({ value={localConfig.pattern || ""} onChange={(e) => updateConfig("pattern", e.target.value)} placeholder="예: [A-Za-z0-9]+" - className="font-mono text-xs" + className="font-mono text-xs" style={{ fontSize: "12px" }} />

JavaScript 정규식 패턴을 입력하세요.

@@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC = ({ minLength={localConfig.minLength} pattern={localConfig.pattern} autoComplete={localConfig.autoComplete} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
diff --git a/frontend/components/screen/config-panels/TextareaConfigPanel.tsx b/frontend/components/screen/config-panels/TextareaConfigPanel.tsx index 5cd0c825..f700e61d 100644 --- a/frontend/components/screen/config-panels/TextareaConfigPanel.tsx +++ b/frontend/components/screen/config-panels/TextareaConfigPanel.tsx @@ -68,7 +68,7 @@ export const TextareaConfigPanel: React.FC = ({ return ( - + 텍스트영역 설정 @@ -88,7 +88,7 @@ export const TextareaConfigPanel: React.FC = ({ value={localConfig.placeholder || ""} onChange={(e) => updateConfig("placeholder", e.target.value)} placeholder="내용을 입력하세요" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -101,7 +101,7 @@ export const TextareaConfigPanel: React.FC = ({ value={localConfig.defaultValue || ""} onChange={(e) => updateConfig("defaultValue", e.target.value)} placeholder="기본 텍스트 내용" - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} rows={3} /> {localConfig.showCharCount && ( @@ -151,7 +151,7 @@ export const TextareaConfigPanel: React.FC = ({ placeholder="자동 (CSS로 제어)" min={10} max={200} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />

비워두면 CSS width로 제어됩니다.

@@ -203,7 +203,7 @@ export const TextareaConfigPanel: React.FC = ({ }} placeholder="제한 없음" min={0} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC = ({ }} placeholder="제한 없음" min={1} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} />
@@ -333,7 +333,7 @@ export const TextareaConfigPanel: React.FC = ({ resize: localConfig.resizable ? "both" : "none", minHeight: localConfig.autoHeight ? "auto" : undefined, }} - className="text-xs" + className="text-xs" style={{ fontSize: "12px" }} wrap={localConfig.wrap} /> {localConfig.showCharCount && ( diff --git a/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx b/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx index 659c5fa1..606a3071 100644 --- a/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx +++ b/frontend/components/screen/dialogs/FlowButtonGroupDialog.tsx @@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC = ({ max={100} value={gap} onChange={(e) => setGap(Number(e.target.value))} - className="h-9 text-sm sm:h-10" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} /> {gap}px @@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC = ({ 정렬 방식 = ({ }} placeholder="추가" disabled={!localValues.enableAdd} - className="h-8 text-sm" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
-
-
@@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC = ({
-
-
-
@@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 컬럼 설정 @@ -1535,7 +1535,7 @@ const DataTableConfigPanelComponent: React.FC = ({
{/* 파일 컬럼 추가 버튼 */} - @@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC = ({ } }} placeholder="표시명을 입력하세요" - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
@@ -1673,7 +1673,7 @@ const DataTableConfigPanelComponent: React.FC = ({ updateColumn(column.id, { gridColumns: newGridColumns }); }} > - + @@ -1861,7 +1861,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} > - + @@ -1902,7 +1902,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} > - + @@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} placeholder="고정값 입력..." - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />
)} @@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 필터 설정 @@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC = ({ {component.filters.length === 0 ? (
-

필터가 없습니다

+

필터가 없습니다

컬럼을 추가하면 자동으로 필터가 생성됩니다

) : ( @@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC = ({ updateFilter(index, { label: newValue }); }} placeholder="필터 이름 입력..." - className="h-8 text-xs" + className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} />

@@ -2112,7 +2112,7 @@ const DataTableConfigPanelComponent: React.FC = ({ } }} > - + @@ -2144,7 +2144,7 @@ const DataTableConfigPanelComponent: React.FC = ({ value={filter.gridColumns.toString()} onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })} > - + @@ -2192,7 +2192,7 @@ const DataTableConfigPanelComponent: React.FC = ({ - + 모달 및 페이징 설정 @@ -2258,7 +2258,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} /> -

@@ -2278,7 +2278,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} /> -
@@ -2298,7 +2298,7 @@ const DataTableConfigPanelComponent: React.FC = ({ }); }} /> -
diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index ee50e54d..14c7e388 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -148,7 +148,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", newZones); } }} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -185,7 +186,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", newZones); } }} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -199,7 +201,8 @@ export const DetailSettingsPanel: React.FC = ({ onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value)) } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -243,7 +246,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", updatedZones); } }} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} > @@ -302,7 +306,8 @@ export const DetailSettingsPanel: React.FC = ({ onUpdateProperty(layoutComponent.id, "zones", newZones); } }} - className="w-20 rounded border border-gray-300 px-2 py-1 text-sm" + className="w-20 rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -317,7 +322,8 @@ export const DetailSettingsPanel: React.FC = ({ onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value)) } - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + style={{ fontSize: "12px" }} />
@@ -332,7 +338,8 @@ export const DetailSettingsPanel: React.FC = ({ - + diff --git a/frontend/components/screen/panels/FlowButtonGroupPanel.tsx b/frontend/components/screen/panels/FlowButtonGroupPanel.tsx index 83720945..5a28aa70 100644 --- a/frontend/components/screen/panels/FlowButtonGroupPanel.tsx +++ b/frontend/components/screen/panels/FlowButtonGroupPanel.tsx @@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC = ({ size="sm" variant="ghost" onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))} - className="h-7 px-2 text-xs" + className="h-7 px-2 text-xs" style={{ fontSize: "12px" }} > 선택 @@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC = ({ {groupInfo.buttons.map((button) => (
diff --git a/frontend/components/screen/panels/GridPanel.tsx b/frontend/components/screen/panels/GridPanel.tsx index 4df77897..16178e57 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -68,7 +68,7 @@ export const GridPanel: React.FC = ({ size="sm" variant="outline" onClick={onForceGridUpdate} - className="h-7 px-2 text-xs" + className="h-7 px-2 text-xs" style={{ fontSize: "12px" }} title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다" > @@ -266,7 +266,7 @@ export const GridPanel: React.FC = ({

격자 정보

-
+
해상도: diff --git a/frontend/components/screen/panels/LayoutsPanel.tsx b/frontend/components/screen/panels/LayoutsPanel.tsx index c38082b9..760a8229 100644 --- a/frontend/components/screen/panels/LayoutsPanel.tsx +++ b/frontend/components/screen/panels/LayoutsPanel.tsx @@ -214,7 +214,7 @@ export default function LayoutsPanel({
- {layout.name} + {layout.name} {layout.description && ( diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 2b10322c..b45bc517 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -645,7 +645,7 @@ const PropertiesPanelComponent: React.FC = ({ }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" /> -
@@ -661,7 +661,7 @@ const PropertiesPanelComponent: React.FC = ({ }} className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2" /> -
@@ -942,7 +942,7 @@ const PropertiesPanelComponent: React.FC = ({ ) : (
-

카드 레이아웃은 자동으로 크기가 계산됩니다

+

카드 레이아웃은 자동으로 크기가 계산됩니다

카드 개수와 간격 설정은 상세설정에서 조정하세요

)} diff --git a/frontend/components/screen/panels/ResolutionPanel.tsx b/frontend/components/screen/panels/ResolutionPanel.tsx index 44cc5266..90680f01 100644 --- a/frontend/components/screen/panels/ResolutionPanel.tsx +++ b/frontend/components/screen/panels/ResolutionPanel.tsx @@ -84,7 +84,7 @@ const ResolutionPanel: React.FC = ({ currentResolution, on
handleUpdate("label", e.target.value)} placeholder="라벨" - className="h-6 text-[10px]" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
-
- +
+ = ({ }} step={40} placeholder="40" - className="h-6 text-[10px]" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
{/* Placeholder (widget만) */} {selectedComponent.type === "widget" && ( -
- +
+ handleUpdate("placeholder", e.target.value)} placeholder="입력 안내 텍스트" - className="h-6 text-[10px]" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
)} {/* Title (group/area) */} {(selectedComponent.type === "group" || selectedComponent.type === "area") && ( -
- +
+ handleUpdate("title", e.target.value)} placeholder="제목" - className="h-6 text-[10px]" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
)} {/* Description (area만) */} {selectedComponent.type === "area" && ( -
- +
+ handleUpdate("description", e.target.value)} placeholder="설명" - className="h-6 text-[10px]" + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
)} - {/* Grid Columns */} - {(selectedComponent as any).gridColumns !== undefined && ( -
- - + {/* Grid Columns + Z-Index (같은 행) */} +
+ {(selectedComponent as any).gridColumns !== undefined && ( +
+ + +
+ )} +
+ + handleUpdate("position.z", parseInt(e.target.value) || 1)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} + />
- )} - - {/* Z-Index */} -
- - handleUpdate("position.z", parseInt(e.target.value) || 1)} - className="h-6 text-[10px]" - />
{/* 라벨 스타일 */} - + 라벨 스타일 - + -
- +
+ handleUpdate("style.labelText", e.target.value)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
-
- +
+ handleUpdate("style.labelFontSize", e.target.value)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
-
- +
+ handleUpdate("style.labelColor", e.target.value)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} />
-
- - handleUpdate("style.labelMarginBottom", e.target.value)} - /> -
-
- handleUpdate("style.labelDisplay", checked)} - /> - +
+
+ + handleUpdate("style.labelMarginBottom", e.target.value)} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + style={{ fontSize: "12px" }} + /> +
+
+ handleUpdate("style.labelDisplay", checked)} + className="h-4 w-4" + /> + +
@@ -357,8 +384,9 @@ export const UnifiedPropertiesPanel: React.FC = ({ handleUpdate("componentConfig.required", checked)} + className="h-4 w-4" /> - +
)} {widget.readonly !== undefined && ( @@ -366,8 +394,9 @@ export const UnifiedPropertiesPanel: React.FC = ({ handleUpdate("componentConfig.readonly", checked)} + className="h-4 w-4" /> - +
)}
@@ -468,7 +497,7 @@ export const UnifiedPropertiesPanel: React.FC = ({
handleUpdate("webType", value)}> - + diff --git a/frontend/components/screen/panels/WebTypeConfigPanel.tsx b/frontend/components/screen/panels/WebTypeConfigPanel.tsx index 9227c269..8c44fb48 100644 --- a/frontend/components/screen/panels/WebTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/WebTypeConfigPanel.tsx @@ -109,7 +109,7 @@ export const WebTypeConfigPanel: React.FC = ({ webType, <>
-
-