"use client"; import React, { useEffect, useState, useMemo } from "react"; import { useParams, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition, LayoutData, ComponentData } from "@/types/screen"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { initializeComponents } from "@/lib/registry/components"; import { EditModal } from "@/components/screen/EditModal"; import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic"; 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"; import { useAuth } from "@/hooks/useAuth"; // πŸ†• μ‚¬μš©μž 정보 import { useResponsive } from "@/lib/hooks/useResponsive"; // πŸ†• λ°˜μ‘ν˜• 감지 import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // ν…Œμ΄λΈ” μ˜΅μ…˜ import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 높이 관리 import { ScreenContextProvider } from "@/contexts/ScreenContext"; // μ»΄ν¬λ„ŒνŠΈ κ°„ 톡신 import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // λΆ„ν•  νŒ¨λ„ λ¦¬μ‚¬μ΄μ¦ˆ import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // ν™œμ„± νƒ­ 관리 import { evaluateConditional } from "@/lib/utils/conditionalEvaluator"; // 쑰건뢀 ν‘œμ‹œ 평가 import { ScreenMultiLangProvider } from "@/contexts/ScreenMultiLangContext"; // ν™”λ©΄ λ‹€κ΅­μ–΄ import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; // V2 Zod 기반 λ³€ν™˜ function ScreenViewPage() { const params = useParams(); const searchParams = useSearchParams(); const router = useRouter(); const screenId = parseInt(params.screenId as string); // URL μΏΌλ¦¬μ—μ„œ menuObjid κ°€μ Έμ˜€κΈ° (메뉴 μŠ€μ½”ν”„) const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; // URL μΏΌλ¦¬μ—μ„œ ν”„λ¦¬λ·°μš© company_code κ°€μ Έμ˜€κΈ° const previewCompanyCode = searchParams.get("company_code"); // 프리뷰 λͺ¨λ“œ 감지 (iframeμ—μ„œ λ‘œλ“œλ  λ•Œ) const isPreviewMode = searchParams.get("preview") === "true"; // πŸ†• ν˜„μž¬ λ‘œκ·ΈμΈν•œ μ‚¬μš©μž 정보 const { user, userName, companyCode: authCompanyCode } = useAuth(); // 프리뷰 λͺ¨λ“œμ—μ„œλŠ” URL νŒŒλΌλ―Έν„°μ˜ company_code μš°μ„  μ‚¬μš© const companyCode = previewCompanyCode || authCompanyCode; // πŸ†• λͺ¨λ°”일 ν™˜κ²½ 감지 const { isMobile } = useResponsive(); // πŸ†• TableSearchWidget 높이 관리 const { getHeightDiff } = useTableSearchWidgetHeight(); const [screen, setScreen] = useState(null); const [layout, setLayout] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); // ν…Œμ΄λΈ”μ—μ„œ μ„ νƒλœ ν–‰ 데이터 (λ²„νŠΌ μ•‘μ…˜μ— 전달) const [selectedRowsData, setSelectedRowsData] = useState([]); // ν…Œμ΄λΈ” μ •λ ¬ 정보 (μ—‘μ…€ λ‹€μš΄λ‘œλ“œμš©) const [tableSortBy, setTableSortBy] = useState(); const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc"); const [tableColumnOrder, setTableColumnOrder] = useState(); const [tableDisplayData, setTableDisplayData] = useState([]); // 화면에 ν‘œμ‹œλœ 데이터 (컬럼 μˆœμ„œ 포함) // ν”Œλ‘œμš°μ—μ„œ μ„ νƒλœ 데이터 (λ²„νŠΌ μ•‘μ…˜μ— 전달) const [flowSelectedData, setFlowSelectedData] = useState([]); const [flowSelectedStepId, setFlowSelectedStepId] = useState(null); // ν…Œμ΄λΈ” μƒˆλ‘œκ³ μΉ¨μ„ μœ„ν•œ ν‚€ (값이 λ³€κ²½λ˜λ©΄ ν…Œμ΄λΈ”μ΄ λ¦¬λ Œλ”λ§λ¨) const [tableRefreshKey, setTableRefreshKey] = useState(0); // ν”Œλ‘œμš° μƒˆλ‘œκ³ μΉ¨μ„ μœ„ν•œ ν‚€ (값이 λ³€κ²½λ˜λ©΄ ν”Œλ‘œμš° 데이터가 λ¦¬λ Œλ”λ§λ¨) const [flowRefreshKey, setFlowRefreshKey] = useState(0); // πŸ†• 쑰건뢀 μ»¨ν…Œμ΄λ„ˆ 높이 좔적 (μ»΄ν¬λ„ŒνŠΈ ID β†’ 높이) const [conditionalContainerHeights, setConditionalContainerHeights] = useState>({}); // νŽΈμ§‘ λͺ¨λ‹¬ μƒνƒœ const [editModalOpen, setEditModalOpen] = useState(false); const [editModalConfig, setEditModalConfig] = useState<{ screenId?: number; modalSize?: "sm" | "md" | "lg" | "xl" | "full"; editData?: Record; onSave?: () => void; modalTitle?: string; modalDescription?: string; }>({}); // λ ˆμ΄μ•„μ›ƒ μ€€λΉ„ μ™„λ£Œ μƒνƒœ (λ²„νŠΌ μœ„μΉ˜ 계산 μ™„λ£Œ ν›„ ν™”λ©΄ ν‘œμ‹œ) const [layoutReady, setLayoutReady] = useState(true); const containerRef = React.useRef(null); const [scale, setScale] = useState(1); const [containerWidth, setContainerWidth] = useState(0); useEffect(() => { const initComponents = async () => { try { await initializeComponents(); } catch (error) { console.error("❌ ν• λ‹Ήλœ ν™”λ©΄μ—μ„œ μ»΄ν¬λ„ŒνŠΈ μ‹œμŠ€ν…œ μ΄ˆκΈ°ν™” μ‹€νŒ¨:", error); } }; initComponents(); }, []); // νŽΈμ§‘ λͺ¨λ‹¬ 이벀트 λ¦¬μŠ€λ„ˆ 등둝 useEffect(() => { const handleOpenEditModal = (event: CustomEvent) => { // console.log("🎭 νŽΈμ§‘ λͺ¨λ‹¬ μ—΄κΈ° 이벀트 μˆ˜μ‹ :", event.detail); setEditModalConfig({ screenId: event.detail.screenId, modalSize: event.detail.modalSize, editData: event.detail.editData, onSave: event.detail.onSave, modalTitle: event.detail.modalTitle, modalDescription: event.detail.modalDescription, }); setEditModalOpen(true); }; // @ts-expect-error - CustomEvent type window.addEventListener("openEditModal", handleOpenEditModal); return () => { // @ts-expect-error - CustomEvent type window.removeEventListener("openEditModal", handleOpenEditModal); }; }, []); useEffect(() => { const loadScreen = async () => { try { setLoading(true); setLayoutReady(false); // ν™”λ©΄ λ‘œλ“œ μ‹œ λ ˆμ΄μ•„μ›ƒ μ€€λΉ„ μ΄ˆκΈ°ν™” setError(null); // ν™”λ©΄ 정보 λ‘œλ“œ const screenData = await screenApi.getScreen(screenId); setScreen(screenData); // λ ˆμ΄μ•„μ›ƒ λ‘œλ“œ (V2 μš°μ„ , Zod 기반 κΈ°λ³Έκ°’ 병합) try { // V2 API λ¨Όμ € μ‹œλ„ const v2Response = await screenApi.getLayoutV2(screenId); if (v2Response && isValidV2Layout(v2Response)) { // V2 λ ˆμ΄μ•„μ›ƒ: Zod 기반 λ³€ν™˜ (κΈ°λ³Έκ°’ 병합) const convertedLayout = convertV2ToLegacy(v2Response); if (convertedLayout) { console.log("πŸ“¦ V2 λ ˆμ΄μ•„μ›ƒ λ‘œλ“œ (Zod 기반):", v2Response.components?.length || 0, "개 μ»΄ν¬λ„ŒνŠΈ"); setLayout({ ...convertedLayout, screenResolution: v2Response.screenResolution || convertedLayout.screenResolution, } as LayoutData); } else { throw new Error("V2 λ ˆμ΄μ•„μ›ƒ λ³€ν™˜ μ‹€νŒ¨"); } } else { // V1 λ ˆμ΄μ•„μ›ƒ λ˜λŠ” 빈 λ ˆμ΄μ•„μ›ƒ const layoutData = await screenApi.getLayout(screenId); setLayout(layoutData); } } catch (layoutError) { console.warn("λ ˆμ΄μ•„μ›ƒ λ‘œλ“œ μ‹€νŒ¨, 빈 λ ˆμ΄μ•„μ›ƒ μ‚¬μš©:", layoutError); setLayout({ screenId, components: [], gridSettings: { columns: 12, gap: 16, padding: 16, enabled: true, size: 8, color: "#e0e0e0", opacity: 0.5, snapToGrid: true, }, }); } } catch (error) { console.error("ν™”λ©΄ λ‘œλ“œ μ‹€νŒ¨:", error); setError("화면을 λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); toast.error("화면을 λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); } finally { setLoading(false); } }; if (screenId) { loadScreen(); } }, [screenId]); // πŸ†• 메인 ν…Œμ΄λΈ” 데이터 μžλ™ λ‘œλ“œ (단일 λ ˆμ½”λ“œ 폼) // ν™”λ©΄μ˜ 메인 ν…Œμ΄λΈ”μ—μ„œ μ‚¬μš©μž νšŒμ‚¬ μ½”λ“œλ‘œ 데이터λ₯Ό μ‘°νšŒν•˜μ—¬ 폼에 μžλ™ 채움 useEffect(() => { const loadMainTableData = async () => { if (!screen || !layout || !layout.components || !companyCode) { return; } const mainTableName = screen.tableName; if (!mainTableName) { return; } // ν…Œμ΄λΈ” μœ„μ ―μ΄ μ—†λŠ” κ²½μš°μ—λ§Œ μžλ™ λ‘œλ“œ (ν…Œμ΄λΈ”μ΄ 있으면 ν–‰ μ„ νƒμœΌλ‘œ 데이터 λ‘œλ“œ) const hasTableWidget = layout.components.some( (comp: any) => comp.componentType === "table-list" || comp.componentType === "v2-table-list" || comp.widgetType === "table" ); if (hasTableWidget) { console.log("πŸ“‹ ν…Œμ΄λΈ” μœ„μ ―μ΄ μžˆμ–΄ μžλ™ λ‘œλ“œ κ±΄λ„ˆλœ€ (ν–‰ μ„ νƒμœΌλ‘œ 데이터 λ‘œλ“œ)"); return; } // 인풋 μ»΄ν¬λ„ŒνŠΈλ“€ 쀑 메인 ν…Œμ΄λΈ”μ˜ μ»¬λŸΌμ„ μ‚¬μš©ν•˜λŠ” 것듀 μ°ΎκΈ° const inputComponents = layout.components.filter((comp: any) => { const compType = comp.componentType || comp.widgetType; const isInputType = compType?.includes("input") || compType?.includes("select") || compType?.includes("textarea") || compType?.includes("v2-input") || compType?.includes("v2-select"); const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); if (inputComponents.length === 0) { return; } // 메인 ν…Œμ΄λΈ”μ—μ„œ ν˜„μž¬ νšŒμ‚¬μ˜ 데이터 쑰회 try { const { tableTypeApi } = await import("@/lib/api/screen"); // company_code둜 ν•„ν„°λ§ν•˜μ—¬ 단일 λ ˆμ½”λ“œ 쑰회 const result = await tableTypeApi.getTableRecord( mainTableName, "company_code", companyCode, "*" // λͺ¨λ“  컬럼 ); if (result && result.record) { console.log("πŸ“¦ 메인 ν…Œμ΄λΈ” 데이터 μžλ™ λ‘œλ“œ:", mainTableName, result.record); // 각 인풋 μ»΄ν¬λ„ŒνŠΈμ— ν•΄λ‹Ήν•˜λŠ” 데이터 μ±„μš°κΈ° const newFormData: Record = {}; inputComponents.forEach((comp: any) => { const columnName = comp.columnName; if (columnName && result.record[columnName] !== undefined) { newFormData[columnName] = result.record[columnName]; } }); if (Object.keys(newFormData).length > 0) { setFormData((prev) => ({ ...prev, ...newFormData, })); } } } catch (error) { console.log("메인 ν…Œμ΄λΈ” μžλ™ λ‘œλ“œ μ‹€νŒ¨ (정상일 수 있음):", error); // μ—λŸ¬λŠ” λ¬΄μ‹œ - 데이터가 μ—†κ±°λ‚˜ κΆŒν•œμ΄ 없을 수 있음 } }; loadMainTableData(); }, [screen, layout, companyCode]); // πŸ†• κ°œλ³„ autoFill 처리 (메인 ν…Œμ΄λΈ”κ³Ό λ‹€λ₯Έ ν…Œμ΄λΈ”μ—μ„œ μ‘°νšŒν•˜λŠ” 경우) useEffect(() => { const initAutoFill = async () => { if (!layout || !layout.components || !user) { return; } for (const comp of layout.components) { // type: "component" λ˜λŠ” type: "widget" λͺ¨λ‘ 처리 if (comp.type === "widget" || comp.type === "component") { const widget = comp as any; const fieldName = widget.columnName || widget.id; // autoFill 처리 (λͺ…μ‹œμ μœΌλ‘œ μ„€μ •λœ 경우만) if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; if (currentValue === undefined || currentValue === "") { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; // μ‚¬μš©μž μ •λ³΄μ—μ„œ ν•„ν„° κ°’ κ°€μ Έμ˜€κΈ° const userValue = user?.[userField as keyof typeof user]; if (userValue && sourceTable && filterColumn && displayColumn) { try { const { tableTypeApi } = await import("@/lib/api/screen"); const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn); setFormData((prev) => ({ ...prev, [fieldName]: result.value, })); } catch (error) { console.error(`autoFill 쑰회 μ‹€νŒ¨: ${fieldName}`, error); } } } } } } }; initAutoFill(); }, [layout, user]); // πŸ†• 쑰건뢀 λΉ„ν™œμ„±ν™”/μˆ¨κΉ€ μ‹œ ν•΄λ‹Ή ν•„λ“œ κ°’ μ΄ˆκΈ°ν™” // 쑰건 ν•„λ“œλ“€μ˜ 값을 μΆ”μ ν•˜μ—¬ λ³€κ²½ μ‹œμ—λ§Œ μ‹€ν–‰ const conditionalFieldValues = useMemo(() => { if (!layout?.components) return ""; // 쑰건뢀 섀정에 μ‚¬μš©λ˜λŠ” ν•„λ“œλ“€μ˜ ν˜„μž¬ 값을 JSON λ¬Έμžμ—΄λ‘œ λ§Œλ“€μ–΄ 비ꡐ const conditionFields = new Set(); layout.components.forEach((component) => { const conditional = (component as any).conditional; if (conditional?.enabled && conditional.field) { conditionFields.add(conditional.field); } }); const values: Record = {}; conditionFields.forEach((field) => { values[field] = (formData as Record)[field]; }); return JSON.stringify(values); }, [layout?.components, formData]); useEffect(() => { if (!layout?.components) return; const fieldsToReset: string[] = []; layout.components.forEach((component) => { const conditional = (component as any).conditional; if (!conditional?.enabled) return; const conditionalResult = evaluateConditional(conditional, formData as Record, layout.components); // μˆ¨κΉ€ λ˜λŠ” λΉ„ν™œμ„±ν™” μƒνƒœμΈ 경우 if (!conditionalResult.visible || conditionalResult.disabled) { const fieldName = (component as any).columnName || component.id; const currentValue = (formData as Record)[fieldName]; // 값이 있으면 μ΄ˆκΈ°ν™” λŒ€μƒμ— μΆ”κ°€ if (currentValue !== undefined && currentValue !== "" && currentValue !== null) { fieldsToReset.push(fieldName); } } }); // μ΄ˆκΈ°ν™”ν•  ν•„λ“œκ°€ 있으면 ν•œ λ²ˆμ— 처리 if (fieldsToReset.length > 0) { setFormData((prev) => { const updated = { ...prev }; fieldsToReset.forEach((fieldName) => { updated[fieldName] = ""; }); return updated; }); } }, [conditionalFieldValues, layout?.components]); // μΊ”λ²„μŠ€ λΉ„μœ¨ μ‘°μ • (μ‚¬μš©μž 화면에 맞게 μžλ™ μŠ€μΌ€μΌ) - 초기 λ‘œλ”© μ‹œμ—λ§Œ 계산 // λΈŒλΌμš°μ € 배율 μ‘°μ • μ‹œ 메뉴와 화면이 ν•¨κ»˜ μΆ•μ†Œ/ν™•λŒ€λ˜λ„λ‘ resize μ΄λ²€νŠΈλŠ” κ°μ§€ν•˜μ§€ μ•ŠμŒ useEffect(() => { // λͺ¨λ°”일 ν™˜κ²½μ—μ„œλŠ” μŠ€μΌ€μΌ μ‘°μ • λΉ„ν™œμ„±ν™” (λ°˜μ‘ν˜•λ§Œ μž‘λ™) if (isMobile) { setScale(1); setLayoutReady(true); // λͺ¨λ°”μΌμ—μ„œλ„ λ ˆμ΄μ•„μ›ƒ μ€€λΉ„ μ™„λ£Œ ν‘œμ‹œ return; } const updateScale = () => { if (containerRef.current && layout) { const designWidth = layout?.screenResolution?.width || 1200; const designHeight = layout?.screenResolution?.height || 800; // μ»¨ν…Œμ΄λ„ˆμ˜ μ‹€μ œ 크기 (프리뷰 λͺ¨λ“œμ—μ„œλŠ” window 크기 μ‚¬μš©) let containerWidth: number; let containerHeight: number; if (isPreviewMode) { // iframeμ—μ„œλŠ” window 크기λ₯Ό 직접 μ‚¬μš© containerWidth = window.innerWidth; containerHeight = window.innerHeight; } else { containerWidth = containerRef.current.offsetWidth; containerHeight = containerRef.current.offsetHeight; } let newScale: number; if (isPreviewMode) { // 프리뷰 λͺ¨λ“œ: κ°€λ‘œ/μ„Έλ‘œ λͺ¨λ‘ fitν•˜λ„λ‘ (μ—¬λ°± 없이) const scaleX = containerWidth / designWidth; const scaleY = containerHeight / designHeight; newScale = Math.min(scaleX, scaleY, 1); // μ΅œλŒ€ 1배율 } else { // 일반 λͺ¨λ“œ: κ°€λ‘œ κΈ°μ€€ μŠ€μΌ€μΌ (쒌우 μ—¬λ°± 16pxμ”© κ³ μ •) const MARGIN_X = 32; const availableWidth = containerWidth - MARGIN_X; newScale = availableWidth / designWidth; } // console.log("πŸ“ μŠ€μΌ€μΌ 계산:", { // containerWidth, // containerHeight, // designWidth, // designHeight, // finalScale: newScale, // isPreviewMode, // }); setScale(newScale); // μ»¨ν…Œμ΄λ„ˆ λ„ˆλΉ„ μ—…λ°μ΄νŠΈ setContainerWidth(containerWidth); // μŠ€μΌ€μΌ 계산 μ™„λ£Œ ν›„ λ ˆμ΄μ•„μ›ƒ μ€€λΉ„ μ™„λ£Œ ν‘œμ‹œ setLayoutReady(true); } }; // 초기 μΈ‘μ • (ν•œ 번만 μ‹€ν–‰) const timer = setTimeout(updateScale, 100); // resize μ΄λ²€νŠΈλŠ” κ°μ§€ν•˜μ§€ μ•ŠμŒ - λΈŒλΌμš°μ € 배율 μ‘°μ • μ‹œ 메뉴와 화면이 ν•¨κ»˜ λ³€κ²½λ˜λ„λ‘ return () => { clearTimeout(timer); }; }, [layout, isMobile, isPreviewMode]); if (loading) { return (

화면을 λΆˆλŸ¬μ˜€λŠ” 쀑...

); } if (error || !screen) { return (
⚠️

화면을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€

{error || "μš”μ²­ν•˜μ‹  화면이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."}

); } // ν™”λ©΄ 해상도 정보가 있으면 ν•΄λ‹Ή 크기둜, μ—†μœΌλ©΄ κΈ°λ³Έ 크기 μ‚¬μš© const screenWidth = layout?.screenResolution?.width || 1200; const screenHeight = layout?.screenResolution?.height || 800; return (
{/* λ ˆμ΄μ•„μ›ƒ μ€€λΉ„ 쀑 λ‘œλ”© ν‘œμ‹œ */} {!layoutReady && (

ν™”λ©΄ μ€€λΉ„ 쀑...

)} {/* μ ˆλŒ€ μœ„μΉ˜ 기반 λ Œλ”λ§ (화면관리와 λ™μΌν•œ 방식) */} {layoutReady && layout && layout.components.length > 0 ? (
{/* μ΅œμƒμœ„ μ»΄ν¬λ„ŒνŠΈλ“€ λ Œλ”λ§ */} {(() => { // πŸ†• ν”Œλ‘œμš° λ²„νŠΌ κ·Έλ£Ή 감지 및 처리 const topLevelComponents = layout.components.filter((component) => !component.parentId); // ν™”λ©΄ κ΄€λ¦¬μ—μ„œ μ„€μ •ν•œ 해상도λ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ widthOffset 계산 λΆˆν•„μš” // λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈλŠ” 원본 μœ„μΉ˜ κ·ΈλŒ€λ‘œ μ‚¬μš© const widthOffset = 0; const buttonGroups: Record = {}; const processedButtonIds = new Set(); // πŸ” 전체 λ²„νŠΌ λͺ©λ‘ 확인 const allButtons = topLevelComponents.filter((component) => { const isButton = (component.type === "component" && ["button-primary", "button-secondary"].includes((component as any).componentType)) || (component.type === "widget" && (component as any).widgetType === "button"); return isButton; }); topLevelComponents.forEach((component) => { const isButton = (component.type === "component" && ["button-primary", "button-secondary"].includes((component as any).componentType)) || (component.type === "widget" && (component as any).widgetType === "button"); if (isButton) { const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as | FlowVisibilityConfig | undefined; // πŸ”§ μž„μ‹œ: λ²„νŠΌ κ·Έλ£Ή κΈ°λŠ₯ μ™„μ „ λΉ„ν™œμ„±ν™” // TODO: μ‚¬μš©μžκ°€ λͺ…μ‹œμ μœΌλ‘œ 그룹을 μ›ν•˜λŠ” κ²½μš°μ—λ§Œ ν™œμ„±ν™”ν•˜λ„λ‘ UI κ°œμ„  ν•„μš” const DISABLE_BUTTON_GROUPS = false; if ( !DISABLE_BUTTON_GROUPS && flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId ) { if (!buttonGroups[flowConfig.groupId]) { buttonGroups[flowConfig.groupId] = []; } buttonGroups[flowConfig.groupId].push(component); processedButtonIds.add(component.id); } // else: λͺ¨λ“  λ²„νŠΌμ„ κ°œλ³„ λ Œλ”λ§ } }); const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); // TableSearchWidget듀을 λ¨Όμ € μ°ΎκΈ° const tableSearchWidgets = regularComponents.filter( (c) => (c as any).componentId === "table-search-widget", ); // 쑰건뢀 μ»¨ν…Œμ΄λ„ˆλ“€μ„ μ°ΎκΈ° const conditionalContainers = regularComponents.filter( (c) => (c as any).componentId === "conditional-container" || (c as any).componentType === "conditional-container", ); // πŸ†• 같은 X μ˜μ—­(μ„Ήμ…˜)μ—μ„œ μ»΄ν¬λ„ŒνŠΈλ“€μ΄ κ²ΉμΉ˜μ§€ μ•Šλ„λ‘ μžλ™ 수직 μ •λ ¬ // ⚠️ V2 λ ˆμ΄μ•„μ›ƒμ—μ„œλŠ” μ‚¬μš©μžκ°€ λ°°μΉ˜ν•œ μœ„μΉ˜λ₯Ό μ‘΄μ€‘ν•˜λ―€λ‘œ μžλ™ μ •λ ¬ λΉ„ν™œμ„±ν™” const autoLayoutComponents = regularComponents; // TableSearchWidget 및 쑰건뢀 μ»¨ν…Œμ΄λ„ˆ 높이 차이λ₯Ό κ³„μ‚°ν•˜μ—¬ Y μœ„μΉ˜ μΆ”κ°€ μ‘°μ • const adjustedComponents = autoLayoutComponents.map((component) => { const isTableSearchWidget = (component as any).componentId === "table-search-widget"; const isConditionalContainer = (component as any).componentId === "conditional-container"; if (isTableSearchWidget || isConditionalContainer) { // 자기 μžμ‹ μ€ μ‘°μ •ν•˜μ§€ μ•ŠμŒ return component; } let totalHeightAdjustment = 0; // TableSearchWidget 높이 μ‘°μ • for (const widget of tableSearchWidgets) { const isBelow = component.position.y > widget.position.y; const heightDiff = getHeightDiff(screenId, widget.id); if (isBelow && heightDiff > 0) { totalHeightAdjustment += heightDiff; } } // 쑰건뢀 μ»¨ν…Œμ΄λ„ˆ 높이 μ‘°μ • for (const container of conditionalContainers) { const isBelow = component.position.y > container.position.y; const actualHeight = conditionalContainerHeights[container.id]; const originalHeight = container.size?.height || 200; const heightDiff = actualHeight ? actualHeight - originalHeight : 0; if (isBelow && heightDiff > 0) { totalHeightAdjustment += heightDiff; } } if (totalHeightAdjustment > 0) { return { ...component, position: { ...component.position, y: component.position.y + totalHeightAdjustment, }, }; } return component; }); return ( <> {/* 일반 μ»΄ν¬λ„ŒνŠΈλ“€ */} {adjustedComponents.map((component) => { // 쑰건뢀 ν‘œμ‹œ 섀정이 μžˆλŠ” κ²½μš°μ—λ§Œ 평가 const conditional = (component as any).conditional; let conditionalDisabled = false; if (conditional?.enabled) { const conditionalResult = evaluateConditional( conditional, formData as Record, layout?.components || [], ); // 쑰건에 따라 μˆ¨κΉ€ 처리 if (!conditionalResult.visible) { return null; } // 쑰건에 따라 λΉ„ν™œμ„±ν™” 처리 conditionalDisabled = conditionalResult.disabled; } // ν™”λ©΄ 관리 해상도λ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ μœ„μΉ˜ μ‘°μ • λΆˆν•„μš” return ( {}} menuObjid={menuObjid} screenId={screenId} tableName={screen?.tableName} userId={user?.userId} userName={userName} companyCode={companyCode} menuObjid={menuObjid} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} tableDisplayData={tableDisplayData} onSelectedRowsChange={( _, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData, ) => { setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); setTableDisplayData(tableDisplayData || []); }} 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); }} formData={formData} onFormDataChange={(fieldName, value) => { setFormData((prev) => ({ ...prev, [fieldName]: value })); }} onHeightChange={(componentId, newHeight) => { setConditionalContainerHeights((prev) => ({ ...prev, [componentId]: newHeight, })); }} > {/* μžμ‹ μ»΄ν¬λ„ŒνŠΈλ“€ */} {(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 ( {}} menuObjid={menuObjid} screenId={screenId} tableName={screen?.tableName} userId={user?.userId} userName={userName} companyCode={companyCode} menuObjid={menuObjid} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} tableDisplayData={tableDisplayData} onSelectedRowsChange={( _, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData, ) => { setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); setTableDisplayData(tableDisplayData || []); }} refreshKey={tableRefreshKey} onRefresh={() => { setTableRefreshKey((prev) => prev + 1); setSelectedRowsData([]); // 선택 ν•΄μ œ }} formData={formData} onFormDataChange={(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; // πŸ” λ²„νŠΌ κ·Έλ£Ή μ„€μ • 확인 console.log("πŸ” λ²„νŠΌ κ·Έλ£Ή μ„€μ •:", { groupId, buttonCount: buttons.length, buttons: buttons.map((b) => ({ id: b.id, label: b.label, x: b.position.x, y: b.position.y, })), groupConfig: { layoutBehavior: groupConfig.layoutBehavior, groupDirection: groupConfig.groupDirection, groupAlign: groupConfig.groupAlign, groupGap: groupConfig.groupGap, }, }); // πŸ”§ μˆ˜μ •: κ·Έλ£Ή μ»¨ν…Œμ΄λ„ˆλŠ” 첫 번째 λ²„νŠΌ μœ„μΉ˜λ₯Ό κΈ°μ€€μœΌλ‘œ ν•˜λ˜, // 각 λ²„νŠΌμ˜ μƒλŒ€ μœ„μΉ˜λŠ” μ›λž˜ μœ„μΉ˜λ₯Ό μœ μ§€ const firstButtonPosition = { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2, }; // λ²„νŠΌ κ·Έλ£Ή μœ„μΉ˜μ—λ„ widthOffset 적용 const adjustedGroupPosition = { ...firstButtonPosition, x: firstButtonPosition.x + widthOffset, }; // 그룹의 크기 계산: λ²„νŠΌλ“€μ˜ μ‹€μ œ 크기 + 간격을 κΈ°μ€€μœΌλ‘œ 계산 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: button.position.x - firstButtonPosition.x, y: button.position.y - firstButtonPosition.y, z: button.position.z || 1, }, }; return (
{}} screenId={screenId} tableName={screen?.tableName} userId={user?.userId} userName={userName} companyCode={companyCode} tableDisplayData={tableDisplayData} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} columnOrder={tableColumnOrder} onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => { setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); setTableColumnOrder(columnOrder); }} 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; }); }} />
); } // μ‹€μ œ μ»΄ν¬λ„ŒνŠΈλ₯Ό Provider둜 감싸기 function ScreenViewPageWrapper() { return ( ); } export default ScreenViewPageWrapper;