2025-09-01 11:48:12 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-09-01 16:40:24 +09:00
|
|
|
|
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
2025-10-22 17:19:47 +09:00
|
|
|
|
import { Database, Cog } from "lucide-react";
|
2026-02-09 13:21:56 +09:00
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-10-22 17:19:47 +09:00
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2025-10-24 10:37:02 +09:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-09-01 11:48:12 +09:00
|
|
|
|
import {
|
|
|
|
|
|
ScreenDefinition,
|
|
|
|
|
|
ComponentData,
|
|
|
|
|
|
LayoutData,
|
|
|
|
|
|
GroupState,
|
2025-09-01 14:00:31 +09:00
|
|
|
|
TableInfo,
|
2025-09-02 11:16:40 +09:00
|
|
|
|
Position,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
ColumnInfo,
|
|
|
|
|
|
GridSettings,
|
2025-09-04 15:20:26 +09:00
|
|
|
|
ScreenResolution,
|
|
|
|
|
|
SCREEN_RESOLUTIONS,
|
2025-09-01 11:48:12 +09:00
|
|
|
|
} from "@/types/screen";
|
|
|
|
|
|
import { generateComponentId } from "@/lib/utils/generateId";
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
import {
|
|
|
|
|
|
getComponentIdFromWebType,
|
|
|
|
|
|
createV2ConfigFromColumn,
|
|
|
|
|
|
getV2ConfigFromWebType,
|
|
|
|
|
|
} from "@/lib/utils/webTypeMapping";
|
2025-09-01 15:22:47 +09:00
|
|
|
|
import {
|
|
|
|
|
|
createGroupComponent,
|
|
|
|
|
|
calculateBoundingBox,
|
|
|
|
|
|
calculateRelativePositions,
|
|
|
|
|
|
restoreAbsolutePositions,
|
|
|
|
|
|
} from "@/lib/utils/groupingUtils";
|
2025-11-10 15:45:51 +09:00
|
|
|
|
import {
|
|
|
|
|
|
adjustGridColumnsFromSize,
|
|
|
|
|
|
updateSizeFromGridColumns,
|
|
|
|
|
|
calculateWidthFromColumns,
|
|
|
|
|
|
snapSizeToGrid,
|
|
|
|
|
|
snapToGrid,
|
|
|
|
|
|
} from "@/lib/utils/gridUtils";
|
2026-02-06 15:18:27 +09:00
|
|
|
|
import {
|
|
|
|
|
|
alignComponents,
|
|
|
|
|
|
distributeComponents,
|
|
|
|
|
|
matchComponentSize,
|
|
|
|
|
|
toggleAllLabels,
|
|
|
|
|
|
nudgeComponents,
|
|
|
|
|
|
AlignMode,
|
|
|
|
|
|
DistributeDirection,
|
|
|
|
|
|
MatchSizeMode,
|
|
|
|
|
|
} from "@/lib/utils/alignmentUtils";
|
|
|
|
|
|
import { KeyboardShortcutsModal } from "./modals/KeyboardShortcutsModal";
|
2025-11-10 14:43:09 +09:00
|
|
|
|
|
|
|
|
|
|
// 10px 단위 스냅 함수
|
|
|
|
|
|
const snapTo10px = (value: number): number => {
|
|
|
|
|
|
return Math.round(value / 10) * 10;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const snapPositionTo10px = (position: Position): Position => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: snapTo10px(position.x),
|
|
|
|
|
|
y: snapTo10px(position.y),
|
|
|
|
|
|
z: position.z,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const snapSizeTo10px = (size: { width: number; height: number }): { width: number; height: number } => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: snapTo10px(size.width),
|
|
|
|
|
|
height: snapTo10px(size.height),
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-11-10 15:09:27 +09:00
|
|
|
|
|
|
|
|
|
|
// calculateGridInfo 더미 함수 (하위 호환성을 위해 유지)
|
|
|
|
|
|
const calculateGridInfo = (width: number, height: number, settings: any) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
columnWidth: 10,
|
|
|
|
|
|
totalWidth: width,
|
|
|
|
|
|
totalHeight: height,
|
|
|
|
|
|
columns: settings.columns || 12,
|
|
|
|
|
|
gap: settings.gap || 0,
|
|
|
|
|
|
padding: settings.padding || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-09-01 15:22:47 +09:00
|
|
|
|
import { GroupingToolbar } from "./GroupingToolbar";
|
2025-09-02 16:18:38 +09:00
|
|
|
|
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
2025-10-16 15:05:24 +09:00
|
|
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
2025-11-28 14:45:04 +09:00
|
|
|
|
import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection";
|
2025-09-01 18:42:59 +09:00
|
|
|
|
import { toast } from "sonner";
|
2025-09-05 10:25:40 +09:00
|
|
|
|
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
2025-09-26 13:11:34 +09:00
|
|
|
|
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
import { initializeComponents } from "@/lib/registry/components";
|
2025-09-26 17:12:03 +09:00
|
|
|
|
import { ScreenFileAPI } from "@/lib/api/screenFile";
|
2025-10-13 18:28:03 +09:00
|
|
|
|
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
|
2026-01-28 11:24:25 +09:00
|
|
|
|
import { convertV2ToLegacy, convertLegacyToV2, isValidV2Layout } from "@/lib/utils/layoutV2Converter";
|
|
|
|
|
|
|
|
|
|
|
|
// V2 API 사용 플래그 (true: V2, false: 기존)
|
|
|
|
|
|
const USE_V2_API = true;
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
|
import StyleEditor from "./StyleEditor";
|
2025-09-10 14:09:32 +09:00
|
|
|
|
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
2025-09-02 16:18:38 +09:00
|
|
|
|
import FloatingPanel from "./FloatingPanel";
|
2025-10-24 10:37:02 +09:00
|
|
|
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
2026-01-14 11:51:24 +09:00
|
|
|
|
import { MultilangSettingsModal } from "./modals/MultilangSettingsModal";
|
2025-09-02 16:18:38 +09:00
|
|
|
|
import DesignerToolbar from "./DesignerToolbar";
|
|
|
|
|
|
import TablesPanel from "./panels/TablesPanel";
|
2025-09-03 15:23:12 +09:00
|
|
|
|
import { TemplatesPanel, TemplateComponent } from "./panels/TemplatesPanel";
|
2025-10-02 14:34:15 +09:00
|
|
|
|
import { ComponentsPanel } from "./panels/ComponentsPanel";
|
2025-09-02 16:18:38 +09:00
|
|
|
|
import PropertiesPanel from "./panels/PropertiesPanel";
|
2025-09-03 11:32:09 +09:00
|
|
|
|
import DetailSettingsPanel from "./panels/DetailSettingsPanel";
|
2025-09-04 15:20:26 +09:00
|
|
|
|
import ResolutionPanel from "./panels/ResolutionPanel";
|
2025-09-02 16:18:38 +09:00
|
|
|
|
import { usePanelState, PanelConfig } from "@/hooks/usePanelState";
|
2025-10-24 10:37:02 +09:00
|
|
|
|
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
|
|
|
|
|
import { FlowVisibilityConfig } from "@/types/control-management";
|
|
|
|
|
|
import {
|
|
|
|
|
|
areAllButtons,
|
|
|
|
|
|
generateGroupId,
|
|
|
|
|
|
groupButtons,
|
|
|
|
|
|
ungroupButtons,
|
|
|
|
|
|
findAllButtonGroups,
|
|
|
|
|
|
} from "@/lib/utils/flowButtonGroupUtils";
|
|
|
|
|
|
import { FlowButtonGroupDialog } from "./dialogs/FlowButtonGroupDialog";
|
2025-10-28 15:39:22 +09:00
|
|
|
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
2025-11-12 10:48:24 +09:00
|
|
|
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-10-15 10:24:33 +09:00
|
|
|
|
// 새로운 통합 UI 컴포넌트
|
|
|
|
|
|
import { SlimToolbar } from "./toolbar/SlimToolbar";
|
2026-01-28 17:36:19 +09:00
|
|
|
|
import { V2PropertiesPanel } from "./panels/V2PropertiesPanel";
|
2025-09-10 18:36:28 +09:00
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 컴포넌트 초기화 (새 시스템)
|
|
|
|
|
|
import "@/lib/registry/components";
|
|
|
|
|
|
// 성능 최적화 도구 초기화 (필요시 사용)
|
|
|
|
|
|
import "@/lib/registry/utils/performanceOptimizer";
|
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
|
interface ScreenDesignerProps {
|
2025-09-01 14:00:31 +09:00
|
|
|
|
selectedScreen: ScreenDefinition | null;
|
|
|
|
|
|
onBackToList: () => void;
|
2026-01-27 10:06:40 +09:00
|
|
|
|
onScreenUpdate?: (updatedScreen: Partial<ScreenDefinition>) => void;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
// POP 모드 지원
|
|
|
|
|
|
isPop?: boolean;
|
|
|
|
|
|
defaultDevicePreview?: "mobile" | "tablet";
|
2025-09-01 11:48:12 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 09:51:29 +09:00
|
|
|
|
import { useLayerOptional, LayerProvider, createDefaultLayer } from "@/contexts/LayerContext";
|
|
|
|
|
|
import { LayerManagerPanel } from "./LayerManagerPanel";
|
|
|
|
|
|
import { LayerType, LayerDefinition } from "@/types/screen-management";
|
|
|
|
|
|
|
|
|
|
|
|
// 패널 설정 업데이트
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const panelConfigs: PanelConfig[] = [
|
2025-09-03 15:23:12 +09:00
|
|
|
|
{
|
2026-01-28 17:36:19 +09:00
|
|
|
|
id: "v2",
|
2025-10-28 17:33:03 +09:00
|
|
|
|
title: "패널",
|
2025-09-10 18:36:28 +09:00
|
|
|
|
defaultPosition: "left",
|
2025-10-27 16:40:59 +09:00
|
|
|
|
defaultWidth: 240,
|
2025-09-10 18:36:28 +09:00
|
|
|
|
defaultHeight: 700,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
shortcutKey: "p",
|
|
|
|
|
|
},
|
2026-02-06 09:51:29 +09:00
|
|
|
|
{
|
|
|
|
|
|
id: "layer",
|
|
|
|
|
|
title: "레이어",
|
|
|
|
|
|
defaultPosition: "right",
|
|
|
|
|
|
defaultWidth: 240,
|
|
|
|
|
|
defaultHeight: 500,
|
|
|
|
|
|
shortcutKey: "l",
|
|
|
|
|
|
},
|
2025-09-02 16:18:38 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
|
export default function ScreenDesigner({
|
|
|
|
|
|
selectedScreen,
|
|
|
|
|
|
onBackToList,
|
|
|
|
|
|
onScreenUpdate,
|
|
|
|
|
|
isPop = false,
|
|
|
|
|
|
defaultDevicePreview = "tablet"
|
|
|
|
|
|
}: ScreenDesignerProps) {
|
|
|
|
|
|
// POP 모드 여부에 따른 API 분기
|
|
|
|
|
|
const USE_POP_API = isPop;
|
2025-09-01 11:48:12 +09:00
|
|
|
|
const [layout, setLayout] = useState<LayoutData>({
|
|
|
|
|
|
components: [],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
gridSettings: {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
columns: 12,
|
|
|
|
|
|
gap: 16,
|
|
|
|
|
|
padding: 0,
|
|
|
|
|
|
snapToGrid: true,
|
|
|
|
|
|
showGrid: false, // 기본값 false로 변경
|
2025-09-02 16:18:38 +09:00
|
|
|
|
gridColor: "#d1d5db",
|
|
|
|
|
|
gridOpacity: 0.5,
|
|
|
|
|
|
},
|
2025-09-01 11:48:12 +09:00
|
|
|
|
});
|
2025-09-01 18:42:59 +09:00
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2026-01-14 10:20:27 +09:00
|
|
|
|
const [isGeneratingMultilang, setIsGeneratingMultilang] = useState(false);
|
2026-01-14 11:51:24 +09:00
|
|
|
|
const [showMultilangSettingsModal, setShowMultilangSettingsModal] = useState(false);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-11-11 16:28:17 +09:00
|
|
|
|
// 🆕 화면에 할당된 메뉴 OBJID
|
|
|
|
|
|
const [menuObjid, setMenuObjid] = useState<number | undefined>(undefined);
|
|
|
|
|
|
|
2025-09-05 10:25:40 +09:00
|
|
|
|
// 메뉴 할당 모달 상태
|
|
|
|
|
|
const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false);
|
|
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
// 단축키 도움말 모달 상태
|
|
|
|
|
|
const [showShortcutsModal, setShowShortcutsModal] = useState(false);
|
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// 파일첨부 상세 모달 상태
|
|
|
|
|
|
const [showFileAttachmentModal, setShowFileAttachmentModal] = useState(false);
|
|
|
|
|
|
const [selectedFileComponent, setSelectedFileComponent] = useState<ComponentData | null>(null);
|
|
|
|
|
|
|
2025-09-04 15:20:26 +09:00
|
|
|
|
// 해상도 설정 상태
|
|
|
|
|
|
const [screenResolution, setScreenResolution] = useState<ScreenResolution>(
|
|
|
|
|
|
SCREEN_RESOLUTIONS[0], // 기본값: Full HD
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-06 09:51:29 +09:00
|
|
|
|
// 🆕 패널 상태 관리 (usePanelState 훅)
|
|
|
|
|
|
const { panelStates, togglePanel, openPanel, closePanel, closeAllPanels, updatePanelPosition, updatePanelSize } =
|
|
|
|
|
|
usePanelState(panelConfigs);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 탭 내부 컴포넌트 선택 상태 (중첩 구조 지원)
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const [selectedTabComponentInfo, setSelectedTabComponentInfo] = useState<{
|
|
|
|
|
|
tabsComponentId: string; // 탭 컴포넌트 ID
|
|
|
|
|
|
tabId: string; // 탭 ID
|
|
|
|
|
|
componentId: string; // 탭 내부 컴포넌트 ID
|
|
|
|
|
|
component: any; // 탭 내부 컴포넌트 데이터
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 중첩 구조용: 부모 분할 패널 정보
|
|
|
|
|
|
parentSplitPanelId?: string | null;
|
|
|
|
|
|
parentPanelSide?: "left" | "right" | null;
|
2026-01-20 10:46:34 +09:00
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 🆕 분할 패널 내부 컴포넌트 선택 상태
|
|
|
|
|
|
const [selectedPanelComponentInfo, setSelectedPanelComponentInfo] = useState<{
|
|
|
|
|
|
splitPanelId: string; // 분할 패널 컴포넌트 ID
|
|
|
|
|
|
panelSide: "left" | "right"; // 좌측/우측 패널
|
|
|
|
|
|
componentId: string; // 패널 내부 컴포넌트 ID
|
|
|
|
|
|
component: any; // 패널 내부 컴포넌트 데이터
|
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
2025-10-28 17:33:03 +09:00
|
|
|
|
// 컴포넌트 선택 시 통합 패널 자동 열기
|
2025-10-15 10:24:33 +09:00
|
|
|
|
const handleComponentSelect = useCallback(
|
|
|
|
|
|
(component: ComponentData | null) => {
|
|
|
|
|
|
setSelectedComponent(component);
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 일반 컴포넌트 선택 시 탭 내부 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
2026-01-20 10:46:34 +09:00
|
|
|
|
if (component) {
|
|
|
|
|
|
setSelectedTabComponentInfo(null);
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setSelectedPanelComponentInfo(null);
|
2026-01-20 10:46:34 +09:00
|
|
|
|
}
|
2025-10-15 10:24:33 +09:00
|
|
|
|
|
2025-10-28 17:33:03 +09:00
|
|
|
|
// 컴포넌트가 선택되면 통합 패널 자동 열기
|
2025-10-15 10:24:33 +09:00
|
|
|
|
if (component) {
|
2026-01-28 17:36:19 +09:00
|
|
|
|
openPanel("v2");
|
2025-10-15 10:24:33 +09:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[openPanel],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 탭 내부 컴포넌트 선택 핸들러 (중첩 구조 지원)
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const handleSelectTabComponent = useCallback(
|
2026-02-02 17:11:00 +09:00
|
|
|
|
(
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
tabsComponentId: string,
|
|
|
|
|
|
tabId: string,
|
|
|
|
|
|
compId: string,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
comp: any,
|
|
|
|
|
|
// 🆕 중첩 구조용: 부모 분할 패널 정보 (선택적)
|
|
|
|
|
|
parentSplitPanelId?: string | null,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
parentPanelSide?: "left" | "right" | null,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
) => {
|
2026-01-20 10:46:34 +09:00
|
|
|
|
if (!compId) {
|
|
|
|
|
|
// 탭 영역 빈 공간 클릭 시 선택 해제
|
|
|
|
|
|
setSelectedTabComponentInfo(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
setSelectedTabComponentInfo({
|
|
|
|
|
|
tabsComponentId,
|
|
|
|
|
|
tabId,
|
|
|
|
|
|
componentId: compId,
|
|
|
|
|
|
component: comp,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
parentSplitPanelId: parentSplitPanelId || null,
|
|
|
|
|
|
parentPanelSide: parentPanelSide || null,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
});
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 탭 내부 컴포넌트 선택 시 일반 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
2026-01-20 10:46:34 +09:00
|
|
|
|
setSelectedComponent(null);
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setSelectedPanelComponentInfo(null);
|
|
|
|
|
|
openPanel("v2");
|
|
|
|
|
|
},
|
|
|
|
|
|
[openPanel],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
|
|
|
|
|
const handleSelectPanelComponent = useCallback(
|
|
|
|
|
|
(splitPanelId: string, panelSide: "left" | "right", compId: string, comp: any) => {
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🐛 디버깅: 전달받은 comp 확인
|
|
|
|
|
|
console.log("🐛 [handleSelectPanelComponent] comp:", {
|
|
|
|
|
|
compId,
|
|
|
|
|
|
componentType: comp?.componentType,
|
|
|
|
|
|
selectedTable: comp?.componentConfig?.selectedTable,
|
|
|
|
|
|
fieldMapping: comp?.componentConfig?.fieldMapping,
|
|
|
|
|
|
fieldMappingKeys: comp?.componentConfig?.fieldMapping ? Object.keys(comp.componentConfig.fieldMapping) : [],
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
if (!compId) {
|
|
|
|
|
|
// 패널 영역 빈 공간 클릭 시 선택 해제
|
|
|
|
|
|
setSelectedPanelComponentInfo(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setSelectedPanelComponentInfo({
|
|
|
|
|
|
splitPanelId,
|
|
|
|
|
|
panelSide,
|
|
|
|
|
|
componentId: compId,
|
|
|
|
|
|
component: comp,
|
|
|
|
|
|
});
|
|
|
|
|
|
// 분할 패널 내부 컴포넌트 선택 시 일반 컴포넌트/탭 내부 컴포넌트 선택 해제
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
setSelectedTabComponentInfo(null);
|
2026-01-28 17:36:19 +09:00
|
|
|
|
openPanel("v2");
|
2026-01-20 10:46:34 +09:00
|
|
|
|
},
|
|
|
|
|
|
[openPanel],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 중첩된 탭 컴포넌트 선택 이벤트 리스너 (분할 패널 안의 탭 안의 컴포넌트)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleNestedTabComponentSelect = (event: CustomEvent) => {
|
|
|
|
|
|
const { tabsComponentId, tabId, componentId, component, parentSplitPanelId, parentPanelSide } = event.detail;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (!componentId) {
|
|
|
|
|
|
setSelectedTabComponentInfo(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
console.log("🎯 중첩된 탭 컴포넌트 선택:", event.detail);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
setSelectedTabComponentInfo({
|
|
|
|
|
|
tabsComponentId,
|
|
|
|
|
|
tabId,
|
|
|
|
|
|
componentId,
|
|
|
|
|
|
component,
|
|
|
|
|
|
parentSplitPanelId,
|
|
|
|
|
|
parentPanelSide,
|
|
|
|
|
|
});
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
setSelectedPanelComponentInfo(null);
|
|
|
|
|
|
openPanel("v2");
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
window.addEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [openPanel]);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 클립보드 상태
|
|
|
|
|
|
const [clipboard, setClipboard] = useState<ComponentData[]>([]);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 실행취소/다시실행을 위한 히스토리 상태
|
|
|
|
|
|
const [history, setHistory] = useState<LayoutData[]>([]);
|
|
|
|
|
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 그룹 상태
|
|
|
|
|
|
const [groupState, setGroupState] = useState<GroupState>({
|
|
|
|
|
|
selectedComponents: [],
|
|
|
|
|
|
isGrouping: false,
|
|
|
|
|
|
});
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그 상태
|
2025-09-01 14:26:39 +09:00
|
|
|
|
const [dragState, setDragState] = useState({
|
2025-09-01 11:48:12 +09:00
|
|
|
|
isDragging: false,
|
2025-09-01 14:26:39 +09:00
|
|
|
|
draggedComponent: null as ComponentData | null,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
draggedComponents: [] as ComponentData[], // 다중 드래그를 위한 컴포넌트 배열
|
|
|
|
|
|
originalPosition: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
currentPosition: { x: 0, y: 0, z: 1 },
|
2025-09-01 16:40:24 +09:00
|
|
|
|
grabOffset: { x: 0, y: 0 },
|
2025-09-02 16:18:38 +09:00
|
|
|
|
justFinishedDrag: false, // 드래그 종료 직후 클릭 방지용
|
2025-09-01 11:48:12 +09:00
|
|
|
|
});
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
2025-10-15 10:44:05 +09:00
|
|
|
|
// Pan 모드 상태 (스페이스바 + 드래그)
|
|
|
|
|
|
const [isPanMode, setIsPanMode] = useState(false);
|
|
|
|
|
|
const [panState, setPanState] = useState({
|
|
|
|
|
|
isPanning: false,
|
|
|
|
|
|
startX: 0,
|
|
|
|
|
|
startY: 0,
|
2025-10-22 17:19:47 +09:00
|
|
|
|
outerScrollLeft: 0,
|
|
|
|
|
|
outerScrollTop: 0,
|
|
|
|
|
|
innerScrollLeft: 0,
|
|
|
|
|
|
innerScrollTop: 0,
|
2025-10-15 10:44:05 +09:00
|
|
|
|
});
|
|
|
|
|
|
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// Zoom 상태
|
|
|
|
|
|
const [zoomLevel, setZoomLevel] = useState(1); // 1 = 100%
|
|
|
|
|
|
const MIN_ZOOM = 0.1; // 10%
|
|
|
|
|
|
const MAX_ZOOM = 3; // 300%
|
2026-02-06 15:18:27 +09:00
|
|
|
|
const zoomRafRef = useRef<number | null>(null); // 줌 RAF throttle용
|
2025-10-15 10:44:05 +09:00
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// 전역 파일 상태 변경 시 강제 리렌더링을 위한 상태
|
|
|
|
|
|
const [forceRenderTrigger, setForceRenderTrigger] = useState(0);
|
|
|
|
|
|
|
2025-09-26 17:12:03 +09:00
|
|
|
|
// 파일 컴포넌트 데이터 복원 함수 (실제 DB에서 조회)
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const restoreFileComponentsData = useCallback(
|
|
|
|
|
|
async (components: ComponentData[]) => {
|
|
|
|
|
|
if (!selectedScreen?.screenId) return;
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
// console.log("🔄 파일 컴포넌트 데이터 복원 시작:", components.length);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 실제 DB에서 화면의 모든 파일 정보 조회
|
|
|
|
|
|
const fileResponse = await ScreenFileAPI.getScreenComponentFiles(selectedScreen.screenId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fileResponse.success) {
|
|
|
|
|
|
// console.warn("⚠️ 파일 정보 조회 실패:", fileResponse);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { componentFiles } = fileResponse;
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
|
// 전역 파일 상태 초기화
|
|
|
|
|
|
const globalFileState: { [key: string]: any[] } = {};
|
|
|
|
|
|
let restoredCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// DB에서 조회한 파일 정보를 전역 상태로 복원
|
|
|
|
|
|
Object.keys(componentFiles).forEach((componentId) => {
|
|
|
|
|
|
const files = componentFiles[componentId];
|
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
|
globalFileState[componentId] = files;
|
|
|
|
|
|
restoredCount++;
|
|
|
|
|
|
|
|
|
|
|
|
// localStorage에도 백업
|
|
|
|
|
|
const backupKey = `fileComponent_${componentId}_files`;
|
|
|
|
|
|
localStorage.setItem(backupKey, JSON.stringify(files));
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📁 DB에서 파일 컴포넌트 데이터 복원:", {
|
|
|
|
|
|
componentId: componentId,
|
|
|
|
|
|
fileCount: files.length,
|
|
|
|
|
|
files: files.map((f) => ({ objid: f.objid, name: f.realFileName })),
|
|
|
|
|
|
});
|
2025-09-26 17:12:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
// 전역 상태 업데이트
|
|
|
|
|
|
(window as any).globalFileState = globalFileState;
|
|
|
|
|
|
|
|
|
|
|
|
// 모든 파일 컴포넌트에 복원 완료 이벤트 발생
|
|
|
|
|
|
Object.keys(globalFileState).forEach((componentId) => {
|
|
|
|
|
|
const files = globalFileState[componentId];
|
|
|
|
|
|
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
componentId: componentId,
|
|
|
|
|
|
files: files,
|
|
|
|
|
|
fileCount: files.length,
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
isRestore: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
window.dispatchEvent(syncEvent);
|
|
|
|
|
|
});
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (restoredCount > 0) {
|
|
|
|
|
|
toast.success(
|
|
|
|
|
|
`${restoredCount}개 파일 컴포넌트 데이터가 DB에서 복원되었습니다. (총 ${fileResponse.totalFiles}개 파일)`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-26 17:12:03 +09:00
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// console.error("❌ 파일 컴포넌트 데이터 복원 실패:", error);
|
|
|
|
|
|
toast.error("파일 데이터 복원 중 오류가 발생했습니다.");
|
2025-09-26 17:12:03 +09:00
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
},
|
|
|
|
|
|
[selectedScreen?.screenId],
|
|
|
|
|
|
);
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그 선택 상태
|
|
|
|
|
|
const [selectionDrag, setSelectionDrag] = useState({
|
|
|
|
|
|
isSelecting: false,
|
|
|
|
|
|
startPoint: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
currentPoint: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
wasSelecting: false, // 방금 전에 드래그 선택이 진행 중이었는지 추적
|
2025-09-01 11:48:12 +09:00
|
|
|
|
});
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 테이블 데이터
|
|
|
|
|
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
|
|
|
2026-02-06 09:51:29 +09:00
|
|
|
|
// 🆕 검색어로 필터링된 테이블 목록
|
|
|
|
|
|
const filteredTables = useMemo(() => {
|
|
|
|
|
|
if (!searchTerm.trim()) return tables;
|
|
|
|
|
|
const term = searchTerm.toLowerCase();
|
|
|
|
|
|
return tables.filter(
|
|
|
|
|
|
(table) =>
|
|
|
|
|
|
table.tableName.toLowerCase().includes(term) ||
|
|
|
|
|
|
table.columns?.some((col) => col.columnName.toLowerCase().includes(term)),
|
|
|
|
|
|
);
|
|
|
|
|
|
}, [tables, searchTerm]);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 그룹 생성 다이얼로그
|
2025-09-02 10:33:41 +09:00
|
|
|
|
const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false);
|
|
|
|
|
|
|
2025-09-02 11:16:40 +09:00
|
|
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
2025-11-10 14:45:19 +09:00
|
|
|
|
// 10px 격자 라인 생성 (시각적 가이드용)
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const gridLines = useMemo(() => {
|
2025-11-10 14:45:19 +09:00
|
|
|
|
if (!layout.gridSettings?.showGrid) return [];
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
2025-09-04 15:20:26 +09:00
|
|
|
|
const width = screenResolution.width;
|
|
|
|
|
|
const height = screenResolution.height;
|
2025-11-10 14:45:19 +09:00
|
|
|
|
const lines: Array<{ type: "vertical" | "horizontal"; position: number }> = [];
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-11-10 14:45:19 +09:00
|
|
|
|
// 10px 단위로 격자 라인 생성
|
|
|
|
|
|
for (let x = 0; x <= width; x += 10) {
|
|
|
|
|
|
lines.push({ type: "vertical", position: x });
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let y = 0; y <= height; y += 10) {
|
|
|
|
|
|
lines.push({ type: "horizontal", position: y });
|
|
|
|
|
|
}
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-11-10 14:45:19 +09:00
|
|
|
|
return lines;
|
|
|
|
|
|
}, [layout.gridSettings?.showGrid, screenResolution.width, screenResolution.height]);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 🆕 현재 편집 중인 레이어 ID (DB의 layer_id, 1 = 기본 레이어)
|
|
|
|
|
|
const [activeLayerId, setActiveLayerIdLocal] = useState<number>(1);
|
|
|
|
|
|
const activeLayerIdRef = useRef<number>(1);
|
|
|
|
|
|
const setActiveLayerIdWithRef = useCallback((id: number) => {
|
|
|
|
|
|
setActiveLayerIdLocal(id);
|
|
|
|
|
|
activeLayerIdRef.current = id;
|
|
|
|
|
|
}, []);
|
2026-02-06 09:51:29 +09:00
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 🆕 좌측 패널 탭 상태 관리
|
|
|
|
|
|
const [leftPanelTab, setLeftPanelTab] = useState<string>("components");
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 레이어 영역 (기본 레이어에서 조건부 레이어들의 displayRegion 표시)
|
|
|
|
|
|
const [layerRegions, setLayerRegions] = useState<Record<number, { x: number; y: number; width: number; height: number; layerName: string }>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 조건부 영역 드래그 상태 (캔버스에서 드래그로 영역 설정)
|
|
|
|
|
|
const [regionDrag, setRegionDrag] = useState<{
|
|
|
|
|
|
isDrawing: boolean; // 새 영역 그리기 모드
|
|
|
|
|
|
isDragging: boolean; // 기존 영역 이동 모드
|
|
|
|
|
|
isResizing: boolean; // 기존 영역 리사이즈 모드
|
|
|
|
|
|
targetLayerId: string | null; // 대상 레이어 ID
|
|
|
|
|
|
startX: number;
|
|
|
|
|
|
startY: number;
|
|
|
|
|
|
currentX: number;
|
|
|
|
|
|
currentY: number;
|
|
|
|
|
|
resizeHandle: string | null; // 리사이즈 핸들 위치
|
|
|
|
|
|
originalRegion: { x: number; y: number; width: number; height: number } | null;
|
|
|
|
|
|
}>({
|
|
|
|
|
|
isDrawing: false,
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
isResizing: false,
|
|
|
|
|
|
targetLayerId: null,
|
|
|
|
|
|
startX: 0,
|
|
|
|
|
|
startY: 0,
|
|
|
|
|
|
currentX: 0,
|
|
|
|
|
|
currentY: 0,
|
|
|
|
|
|
resizeHandle: null,
|
|
|
|
|
|
originalRegion: null,
|
|
|
|
|
|
});
|
2026-02-06 09:51:29 +09:00
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 캔버스에 렌더링할 컴포넌트 (DB 기반 레이어: 각 레이어별로 별도 로드되므로 전체 표시)
|
|
|
|
|
|
const visibleComponents = useMemo(() => {
|
|
|
|
|
|
return layout.components;
|
|
|
|
|
|
}, [layout.components]);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
2025-10-23 10:07:55 +09:00
|
|
|
|
// 이미 배치된 컬럼 목록 계산
|
|
|
|
|
|
const placedColumns = useMemo(() => {
|
|
|
|
|
|
const placed = new Set<string>();
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 화면의 메인 테이블명을 fallback으로 사용
|
|
|
|
|
|
const screenTableName = selectedScreen?.tableName;
|
2025-10-23 10:07:55 +09:00
|
|
|
|
|
|
|
|
|
|
const collectColumns = (components: ComponentData[]) => {
|
|
|
|
|
|
components.forEach((comp) => {
|
|
|
|
|
|
const anyComp = comp as any;
|
|
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 tableName과 columnName을 여러 위치에서 찾기 (최상위, componentConfig, 또는 화면 테이블명)
|
|
|
|
|
|
const tableName = anyComp.tableName || anyComp.componentConfig?.tableName || screenTableName;
|
|
|
|
|
|
const columnName = anyComp.columnName || anyComp.componentConfig?.columnName;
|
|
|
|
|
|
|
|
|
|
|
|
// widget 타입 또는 component 타입에서 columnName 확인 (tableName은 화면 테이블명으로 fallback)
|
|
|
|
|
|
if ((comp.type === "widget" || comp.type === "component") && tableName && columnName) {
|
|
|
|
|
|
const key = `${tableName}.${columnName}`;
|
2025-10-23 10:07:55 +09:00
|
|
|
|
placed.add(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 자식 컴포넌트도 확인 (재귀)
|
|
|
|
|
|
if (comp.children && comp.children.length > 0) {
|
|
|
|
|
|
collectColumns(comp.children);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
collectColumns(layout.components);
|
|
|
|
|
|
return placed;
|
2026-02-04 18:01:20 +09:00
|
|
|
|
}, [layout.components, selectedScreen?.tableName]);
|
2025-10-23 10:07:55 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 히스토리에 저장
|
|
|
|
|
|
const saveToHistory = useCallback(
|
|
|
|
|
|
(newLayout: LayoutData) => {
|
|
|
|
|
|
setHistory((prev) => {
|
|
|
|
|
|
const newHistory = prev.slice(0, historyIndex + 1);
|
|
|
|
|
|
newHistory.push(newLayout);
|
|
|
|
|
|
return newHistory.slice(-50); // 최대 50개까지만 저장
|
|
|
|
|
|
});
|
|
|
|
|
|
setHistoryIndex((prev) => Math.min(prev + 1, 49));
|
|
|
|
|
|
},
|
|
|
|
|
|
[historyIndex],
|
|
|
|
|
|
);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 탭 내부 컴포넌트 설정 업데이트 핸들러 (중첩 구조 지원)
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const handleUpdateTabComponentConfig = useCallback(
|
|
|
|
|
|
(path: string, value: any) => {
|
|
|
|
|
|
if (!selectedTabComponentInfo) return;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const { tabsComponentId, tabId, componentId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 탭 컴포넌트 업데이트 함수 (재사용)
|
|
|
|
|
|
const updateTabsComponent = (tabsComponent: any) => {
|
|
|
|
|
|
const currentConfig = tabsComponent.componentConfig || {};
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const tabs = currentConfig.tabs || [];
|
|
|
|
|
|
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === tabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: (tab.components || []).map((comp: any) => {
|
|
|
|
|
|
if (comp.id === componentId) {
|
|
|
|
|
|
if (path.startsWith("componentConfig.")) {
|
|
|
|
|
|
const configPath = path.replace("componentConfig.", "");
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
componentConfig: { ...comp.componentConfig, [configPath]: value },
|
2026-01-20 10:46:34 +09:00
|
|
|
|
};
|
|
|
|
|
|
} else if (path.startsWith("style.")) {
|
|
|
|
|
|
const stylePath = path.replace("style.", "");
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return { ...comp, style: { ...comp.style, [stylePath]: value } };
|
2026-01-20 10:46:34 +09:00
|
|
|
|
} else if (path.startsWith("size.")) {
|
|
|
|
|
|
const sizePath = path.replace("size.", "");
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return { ...comp, size: { ...comp.size, [sizePath]: value } };
|
2026-01-20 10:46:34 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
return { ...comp, [path]: value };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return comp;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs } };
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
let newLayout;
|
|
|
|
|
|
let updatedTabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const tabsComponent = panelComponents.find((pc: any) => pc.id === tabsComponentId);
|
|
|
|
|
|
if (!tabsComponent) return c;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponent(tabsComponent);
|
|
|
|
|
|
updatedTabs = updatedTabsComponent.componentConfig.tabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
pc.id === tabsComponentId ? updatedTabsComponent : pc,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭 업데이트
|
|
|
|
|
|
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
|
|
|
|
|
|
if (!tabsComponent) return prevLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponent(tabsComponent);
|
|
|
|
|
|
updatedTabs = updatedTabsComponent.componentConfig.tabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: prevLayout.components.map((c) => (c.id === tabsComponentId ? updatedTabsComponent : c)),
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
|
|
|
|
|
|
// 선택된 컴포넌트 정보도 업데이트
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (updatedTabs) {
|
|
|
|
|
|
const updatedComp = updatedTabs
|
|
|
|
|
|
.find((t: any) => t.id === tabId)
|
|
|
|
|
|
?.components?.find((c: any) => c.id === componentId);
|
|
|
|
|
|
if (updatedComp) {
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
setSelectedTabComponentInfo((prev) => (prev ? { ...prev, component: updatedComp } : null));
|
2026-02-02 17:11:00 +09:00
|
|
|
|
}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return newLayout;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[selectedTabComponentInfo],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 실행취소
|
|
|
|
|
|
const undo = useCallback(() => {
|
2025-11-03 09:58:04 +09:00
|
|
|
|
setHistoryIndex((prevIndex) => {
|
|
|
|
|
|
if (prevIndex > 0) {
|
|
|
|
|
|
const newIndex = prevIndex - 1;
|
|
|
|
|
|
setHistory((prevHistory) => {
|
|
|
|
|
|
if (prevHistory[newIndex]) {
|
|
|
|
|
|
setLayout(prevHistory[newIndex]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return prevHistory;
|
|
|
|
|
|
});
|
|
|
|
|
|
return newIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
return prevIndex;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 다시실행
|
|
|
|
|
|
const redo = useCallback(() => {
|
2025-11-03 09:58:04 +09:00
|
|
|
|
setHistoryIndex((prevIndex) => {
|
|
|
|
|
|
let newIndex = prevIndex;
|
|
|
|
|
|
setHistory((prevHistory) => {
|
|
|
|
|
|
if (prevIndex < prevHistory.length - 1) {
|
|
|
|
|
|
newIndex = prevIndex + 1;
|
|
|
|
|
|
if (prevHistory[newIndex]) {
|
|
|
|
|
|
setLayout(prevHistory[newIndex]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return prevHistory;
|
|
|
|
|
|
});
|
|
|
|
|
|
return newIndex;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 컴포넌트 속성 업데이트
|
|
|
|
|
|
const updateComponentProperty = useCallback(
|
|
|
|
|
|
(componentId: string, path: string, value: any) => {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 🔥 함수형 업데이트로 변경하여 최신 layout 사용
|
|
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const targetComponent = prevLayout.components.find((comp) => comp.id === componentId);
|
|
|
|
|
|
const isLayoutComponent = targetComponent?.type === "layout";
|
|
|
|
|
|
|
2025-10-29 11:26:00 +09:00
|
|
|
|
// 🆕 그룹 설정 변경 시 같은 그룹의 모든 버튼에 일괄 적용
|
|
|
|
|
|
const isGroupSetting = path === "webTypeConfig.flowVisibilityConfig.groupAlign";
|
|
|
|
|
|
|
|
|
|
|
|
let affectedComponents: string[] = [componentId]; // 기본적으로 현재 컴포넌트만
|
|
|
|
|
|
|
|
|
|
|
|
if (isGroupSetting && targetComponent) {
|
|
|
|
|
|
const flowConfig = (targetComponent as any).webTypeConfig?.flowVisibilityConfig;
|
|
|
|
|
|
const currentGroupId = flowConfig?.groupId;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentGroupId) {
|
|
|
|
|
|
// 같은 그룹의 모든 버튼 찾기
|
|
|
|
|
|
affectedComponents = prevLayout.components
|
|
|
|
|
|
.filter((comp) => {
|
|
|
|
|
|
const compConfig = (comp as any).webTypeConfig?.flowVisibilityConfig;
|
|
|
|
|
|
return compConfig?.groupId === currentGroupId && compConfig?.enabled;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((comp) => comp.id);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 그룹 설정 일괄 적용:", {
|
|
|
|
|
|
groupId: currentGroupId,
|
|
|
|
|
|
setting: path.split(".").pop(),
|
|
|
|
|
|
value,
|
|
|
|
|
|
affectedButtons: affectedComponents,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 레이아웃 컴포넌트의 위치가 변경되는 경우 존에 속한 컴포넌트들도 함께 이동
|
|
|
|
|
|
const positionDelta = { x: 0, y: 0 };
|
|
|
|
|
|
if (isLayoutComponent && (path === "position.x" || path === "position.y" || path === "position")) {
|
|
|
|
|
|
const oldPosition = targetComponent.position;
|
|
|
|
|
|
let newPosition = { ...oldPosition };
|
|
|
|
|
|
|
|
|
|
|
|
if (path === "position.x") {
|
|
|
|
|
|
newPosition.x = value;
|
|
|
|
|
|
positionDelta.x = value - oldPosition.x;
|
|
|
|
|
|
} else if (path === "position.y") {
|
|
|
|
|
|
newPosition.y = value;
|
|
|
|
|
|
positionDelta.y = value - oldPosition.y;
|
|
|
|
|
|
} else if (path === "position") {
|
|
|
|
|
|
newPosition = value;
|
|
|
|
|
|
positionDelta.x = value.x - oldPosition.x;
|
|
|
|
|
|
positionDelta.y = value.y - oldPosition.y;
|
|
|
|
|
|
}
|
2025-09-11 12:22:39 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
console.log("📐 레이아웃 이동 감지:", {
|
|
|
|
|
|
layoutId: componentId,
|
|
|
|
|
|
oldPosition,
|
|
|
|
|
|
newPosition,
|
|
|
|
|
|
positionDelta,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-11 12:22:39 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
const pathParts = path.split(".");
|
|
|
|
|
|
const updatedComponents = prevLayout.components.map((comp) => {
|
2025-10-29 11:26:00 +09:00
|
|
|
|
// 🆕 그룹 설정이면 같은 그룹의 모든 버튼에 적용
|
|
|
|
|
|
const shouldUpdate = isGroupSetting ? affectedComponents.includes(comp.id) : comp.id === componentId;
|
|
|
|
|
|
|
|
|
|
|
|
if (!shouldUpdate) {
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 레이아웃 이동 시 존에 속한 컴포넌트들도 함께 이동
|
|
|
|
|
|
if (isLayoutComponent && (positionDelta.x !== 0 || positionDelta.y !== 0)) {
|
|
|
|
|
|
// 이 레이아웃의 존에 속한 컴포넌트인지 확인
|
|
|
|
|
|
const isInLayoutZone = comp.parentId === componentId && comp.zoneId;
|
|
|
|
|
|
if (isInLayoutZone) {
|
|
|
|
|
|
console.log("🔄 존 컴포넌트 함께 이동:", {
|
|
|
|
|
|
componentId: comp.id,
|
|
|
|
|
|
zoneId: comp.zoneId,
|
|
|
|
|
|
oldPosition: comp.position,
|
|
|
|
|
|
delta: positionDelta,
|
|
|
|
|
|
});
|
2025-09-11 12:22:39 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
position: {
|
|
|
|
|
|
...comp.position,
|
|
|
|
|
|
x: comp.position.x + positionDelta.x,
|
|
|
|
|
|
y: comp.position.y + positionDelta.y,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-11 12:22:39 +09:00
|
|
|
|
}
|
2025-10-22 14:54:50 +09:00
|
|
|
|
return comp;
|
2025-09-11 12:22:39 +09:00
|
|
|
|
}
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 중첩 경로를 고려한 안전한 복사
|
|
|
|
|
|
const newComp = { ...comp };
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 경로를 따라 내려가면서 각 레벨을 새 객체로 복사
|
|
|
|
|
|
let current: any = newComp;
|
|
|
|
|
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
|
|
|
|
const key = pathParts[i];
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 다음 레벨이 없거나 객체가 아니면 새 객체 생성
|
|
|
|
|
|
if (!current[key] || typeof current[key] !== "object" || Array.isArray(current[key])) {
|
|
|
|
|
|
current[key] = {};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 기존 객체를 복사하여 불변성 유지
|
|
|
|
|
|
current[key] = { ...current[key] };
|
|
|
|
|
|
}
|
|
|
|
|
|
current = current[key];
|
|
|
|
|
|
}
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 최종 값 설정
|
|
|
|
|
|
const finalKey = pathParts[pathParts.length - 1];
|
|
|
|
|
|
current[finalKey] = value;
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 style 관련 업데이트 디버그 로그
|
|
|
|
|
|
if (path.includes("style") || path.includes("labelDisplay")) {
|
|
|
|
|
|
console.log("🎨 style 업데이트 제대로 렌더링된거다 내가바꿈:", {
|
|
|
|
|
|
componentId: comp.id,
|
|
|
|
|
|
path,
|
|
|
|
|
|
value,
|
|
|
|
|
|
updatedStyle: newComp.style,
|
|
|
|
|
|
pathIncludesLabelDisplay: path.includes("labelDisplay"),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🆕 labelDisplay 변경 시 강제 리렌더링 트리거 (조건문 밖으로 이동)
|
|
|
|
|
|
if (path === "style.labelDisplay") {
|
|
|
|
|
|
console.log("⏰⏰⏰ labelDisplay 변경 감지! forceRenderTrigger 실행 예정");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 10:01:09 +09:00
|
|
|
|
// 🆕 size 변경 시 style도 함께 업데이트 (파란 테두리와 실제 크기 동기화)
|
|
|
|
|
|
if (path === "size.width" || path === "size.height" || path === "size") {
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// 🔧 style 객체를 새로 복사하여 불변성 유지
|
|
|
|
|
|
newComp.style = { ...(newComp.style || {}) };
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-11-17 10:01:09 +09:00
|
|
|
|
if (path === "size.width") {
|
|
|
|
|
|
newComp.style.width = `${value}px`;
|
|
|
|
|
|
} else if (path === "size.height") {
|
|
|
|
|
|
newComp.style.height = `${value}px`;
|
|
|
|
|
|
} else if (path === "size") {
|
|
|
|
|
|
// size 객체 전체가 변경된 경우
|
|
|
|
|
|
if (value.width !== undefined) {
|
|
|
|
|
|
newComp.style.width = `${value.width}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (value.height !== undefined) {
|
|
|
|
|
|
newComp.style.height = `${value.height}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-11-17 10:01:09 +09:00
|
|
|
|
console.log("🔄 size 변경 → style 동기화:", {
|
|
|
|
|
|
componentId: newComp.id,
|
|
|
|
|
|
path,
|
|
|
|
|
|
value,
|
|
|
|
|
|
updatedStyle: newComp.style,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 15:58:56 +09:00
|
|
|
|
// gridColumns 변경 시 크기 자동 업데이트 제거 (격자 시스템 제거됨)
|
|
|
|
|
|
// if (path === "gridColumns" && prevLayout.gridSettings) {
|
|
|
|
|
|
// const updatedSize = updateSizeFromGridColumns(newComp, prevLayout.gridSettings as GridUtilSettings);
|
|
|
|
|
|
// newComp.size = updatedSize;
|
|
|
|
|
|
// }
|
2025-09-04 17:01:07 +09:00
|
|
|
|
|
2025-11-10 15:49:48 +09:00
|
|
|
|
// 크기 변경 시 격자 스냅 적용 제거 (직접 입력 시 불필요)
|
|
|
|
|
|
// 드래그/리사이즈 시에는 별도 로직에서 처리됨
|
|
|
|
|
|
// if (
|
|
|
|
|
|
// (path === "size.width" || path === "size.height") &&
|
|
|
|
|
|
// prevLayout.gridSettings?.snapToGrid &&
|
|
|
|
|
|
// newComp.type !== "group"
|
|
|
|
|
|
// ) {
|
|
|
|
|
|
// const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
// columns: prevLayout.gridSettings.columns,
|
|
|
|
|
|
// gap: prevLayout.gridSettings.gap,
|
|
|
|
|
|
// padding: prevLayout.gridSettings.padding,
|
|
|
|
|
|
// snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
// });
|
|
|
|
|
|
// const snappedSize = snapSizeToGrid(
|
|
|
|
|
|
// newComp.size,
|
|
|
|
|
|
// currentGridInfo,
|
|
|
|
|
|
// prevLayout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
// );
|
|
|
|
|
|
// newComp.size = snappedSize;
|
|
|
|
|
|
//
|
|
|
|
|
|
// const adjustedColumns = adjustGridColumnsFromSize(
|
|
|
|
|
|
// newComp,
|
|
|
|
|
|
// currentGridInfo,
|
|
|
|
|
|
// prevLayout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
// );
|
|
|
|
|
|
// if (newComp.gridColumns !== adjustedColumns) {
|
|
|
|
|
|
// newComp.gridColumns = adjustedColumns;
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-11-10 15:58:56 +09:00
|
|
|
|
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정 제거 (격자 시스템 제거됨)
|
|
|
|
|
|
// if (path === "gridColumns" && prevLayout.gridSettings?.snapToGrid && newComp.type !== "group") {
|
|
|
|
|
|
// const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
// columns: prevLayout.gridSettings.columns,
|
|
|
|
|
|
// gap: prevLayout.gridSettings.gap,
|
|
|
|
|
|
// padding: prevLayout.gridSettings.padding,
|
|
|
|
|
|
// snapToGrid: prevLayout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
// });
|
|
|
|
|
|
//
|
|
|
|
|
|
// const newWidth = calculateWidthFromColumns(
|
|
|
|
|
|
// newComp.gridColumns,
|
|
|
|
|
|
// currentGridInfo,
|
|
|
|
|
|
// prevLayout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
// );
|
|
|
|
|
|
// newComp.size = {
|
|
|
|
|
|
// ...newComp.size,
|
|
|
|
|
|
// width: newWidth,
|
|
|
|
|
|
// };
|
|
|
|
|
|
// }
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-10-22 14:54:50 +09:00
|
|
|
|
// 위치 변경 시 격자 스냅 적용 (그룹 내부 컴포넌트 포함)
|
|
|
|
|
|
if (
|
|
|
|
|
|
(path === "position.x" || path === "position.y" || path === "position") &&
|
|
|
|
|
|
layout.gridSettings?.snapToGrid
|
|
|
|
|
|
) {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// 현재 해상도에 맞는 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용
|
|
|
|
|
|
if (newComp.parentId && currentGridInfo) {
|
|
|
|
|
|
const { columnWidth } = currentGridInfo;
|
|
|
|
|
|
const { gap } = layout.gridSettings;
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 내부 패딩 고려한 격자 정렬
|
|
|
|
|
|
const padding = 16;
|
|
|
|
|
|
const effectiveX = newComp.position.x - padding;
|
|
|
|
|
|
const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16)));
|
|
|
|
|
|
const snappedX = padding + columnIndex * (columnWidth + (gap || 16));
|
|
|
|
|
|
|
|
|
|
|
|
// Y 좌표는 10px 단위로 스냅
|
|
|
|
|
|
const effectiveY = newComp.position.y - padding;
|
|
|
|
|
|
const rowIndex = Math.round(effectiveY / 10);
|
|
|
|
|
|
const snappedY = padding + rowIndex * 10;
|
|
|
|
|
|
|
|
|
|
|
|
// 크기도 외부 격자와 동일하게 스냅
|
|
|
|
|
|
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
|
|
|
|
|
const widthInColumns = Math.max(1, Math.round(newComp.size.width / fullColumnWidth));
|
|
|
|
|
|
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
|
|
|
|
|
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
|
|
|
|
|
|
const snappedHeight = Math.max(10, newComp.size.height);
|
|
|
|
|
|
|
|
|
|
|
|
newComp.position = {
|
|
|
|
|
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
|
|
|
|
|
y: Math.max(padding, snappedY),
|
|
|
|
|
|
z: newComp.position.z || 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
newComp.size = {
|
|
|
|
|
|
width: snappedWidth,
|
|
|
|
|
|
height: snappedHeight,
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (newComp.type !== "group") {
|
|
|
|
|
|
// 그룹이 아닌 일반 컴포넌트만 격자 스냅 적용
|
2025-11-10 14:45:19 +09:00
|
|
|
|
const snappedPosition = snapPositionTo10px(
|
2025-11-10 14:21:29 +09:00
|
|
|
|
newComp.position,
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
);
|
|
|
|
|
|
newComp.position = snappedPosition;
|
|
|
|
|
|
}
|
2025-09-03 11:32:09 +09:00
|
|
|
|
}
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
return newComp;
|
|
|
|
|
|
});
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 🔥 새로운 layout 생성
|
|
|
|
|
|
const newLayout = { ...prevLayout, components: updatedComponents };
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
saveToHistory(newLayout);
|
2025-10-22 14:54:50 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
|
|
|
|
|
setSelectedComponent((prevSelected) => {
|
|
|
|
|
|
if (prevSelected && prevSelected.id === componentId) {
|
|
|
|
|
|
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
|
|
|
|
|
if (updatedSelectedComponent) {
|
|
|
|
|
|
// 🔧 완전히 새로운 객체를 만들어서 React가 변경을 감지하도록 함
|
|
|
|
|
|
const newSelectedComponent = JSON.parse(JSON.stringify(updatedSelectedComponent));
|
|
|
|
|
|
return newSelectedComponent;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return prevSelected;
|
|
|
|
|
|
});
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// webTypeConfig 업데이트 후 레이아웃 상태 확인
|
|
|
|
|
|
if (path === "webTypeConfig") {
|
|
|
|
|
|
const updatedComponent = newLayout.components.find((c) => c.id === componentId);
|
|
|
|
|
|
console.log("🔄 레이아웃 업데이트 후 컴포넌트 상태:", {
|
2025-09-03 15:23:12 +09:00
|
|
|
|
componentId,
|
2025-10-21 17:32:54 +09:00
|
|
|
|
updatedComponent: updatedComponent
|
|
|
|
|
|
? {
|
|
|
|
|
|
id: updatedComponent.id,
|
|
|
|
|
|
type: updatedComponent.type,
|
|
|
|
|
|
webTypeConfig: updatedComponent.type === "widget" ? (updatedComponent as any).webTypeConfig : null,
|
|
|
|
|
|
}
|
|
|
|
|
|
: null,
|
|
|
|
|
|
layoutComponentsCount: newLayout.components.length,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
return newLayout;
|
|
|
|
|
|
});
|
2025-09-02 11:16:40 +09:00
|
|
|
|
},
|
2025-11-10 14:46:30 +09:00
|
|
|
|
[saveToHistory],
|
2025-09-02 11:16:40 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 컴포넌트 시스템 초기화
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const initComponents = async () => {
|
|
|
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🚀 컴포넌트 시스템 초기화 시작...");
|
2025-09-12 14:24:25 +09:00
|
|
|
|
await initializeComponents();
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 컴포넌트 시스템 초기화 완료");
|
2025-09-12 14:24:25 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 컴포넌트 시스템 초기화 실패:", error);
|
2025-09-12 14:24:25 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
initComponents();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
|
// 화면 선택 시 파일 복원
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedScreen?.screenId) {
|
|
|
|
|
|
restoreScreenFiles();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedScreen?.screenId]);
|
|
|
|
|
|
|
|
|
|
|
|
// 화면의 모든 파일 컴포넌트 파일 복원
|
|
|
|
|
|
const restoreScreenFiles = useCallback(async () => {
|
|
|
|
|
|
if (!selectedScreen?.screenId) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 화면 파일 복원 시작:", selectedScreen.screenId);
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
|
// 해당 화면의 모든 파일 조회
|
|
|
|
|
|
const response = await ScreenFileAPI.getScreenComponentFiles(selectedScreen.screenId);
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
|
if (response.success && response.componentFiles) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📁 복원할 파일 데이터:", response.componentFiles);
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
// 각 컴포넌트별로 파일 복원 (전역 상태와 localStorage 우선 적용)
|
|
|
|
|
|
Object.entries(response.componentFiles).forEach(([componentId, serverFiles]) => {
|
|
|
|
|
|
if (Array.isArray(serverFiles) && serverFiles.length > 0) {
|
|
|
|
|
|
// 🎯 전역 상태와 localStorage에서 현재 파일 상태 확인
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
2025-09-29 17:21:47 +09:00
|
|
|
|
const currentGlobalFiles = globalFileState[componentId] || [];
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
let currentLocalStorageFiles: any[] = [];
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
2025-09-29 17:21:47 +09:00
|
|
|
|
try {
|
|
|
|
|
|
const storedFiles = localStorage.getItem(`fileComponent_${componentId}_files`);
|
|
|
|
|
|
if (storedFiles) {
|
|
|
|
|
|
currentLocalStorageFiles = JSON.parse(storedFiles);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.warn("localStorage 파일 파싱 실패:", e);
|
2025-09-29 17:21:47 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
// 🎯 우선순위: 전역 상태 > localStorage > 서버 데이터
|
|
|
|
|
|
let finalFiles = serverFiles;
|
|
|
|
|
|
if (currentGlobalFiles.length > 0) {
|
|
|
|
|
|
finalFiles = currentGlobalFiles;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`📂 컴포넌트 ${componentId} 전역 상태 우선 적용:`, finalFiles.length, "개");
|
2025-09-29 17:21:47 +09:00
|
|
|
|
} else if (currentLocalStorageFiles.length > 0) {
|
|
|
|
|
|
finalFiles = currentLocalStorageFiles;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`📂 컴포넌트 ${componentId} localStorage 우선 적용:`, finalFiles.length, "개");
|
2025-09-29 17:21:47 +09:00
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`📂 컴포넌트 ${componentId} 서버 데이터 적용:`, finalFiles.length, "개");
|
2025-09-29 17:21:47 +09:00
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
// 전역 상태에 파일 저장
|
|
|
|
|
|
globalFileState[componentId] = finalFiles;
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
2025-09-29 13:29:03 +09:00
|
|
|
|
(window as any).globalFileState = globalFileState;
|
|
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
|
// localStorage에도 백업
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
2025-09-29 17:21:47 +09:00
|
|
|
|
localStorage.setItem(`fileComponent_${componentId}_files`, JSON.stringify(finalFiles));
|
2025-09-29 13:29:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
// 레이아웃의 컴포넌트들에 파일 정보 적용 (전역 상태 우선)
|
2025-10-13 18:28:03 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const updatedComponents = prevLayout.components.map((comp) => {
|
2025-09-29 17:21:47 +09:00
|
|
|
|
// 🎯 전역 상태에서 최신 파일 정보 가져오기
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
2025-09-29 17:21:47 +09:00
|
|
|
|
const finalFiles = globalFileState[comp.id] || [];
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 17:21:47 +09:00
|
|
|
|
if (finalFiles.length > 0) {
|
2025-09-29 13:29:03 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
2025-09-29 17:21:47 +09:00
|
|
|
|
uploadedFiles: finalFiles,
|
2025-10-13 18:28:03 +09:00
|
|
|
|
lastFileUpdate: Date.now(),
|
2025-09-29 13:29:03 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return comp;
|
|
|
|
|
|
});
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...prevLayout,
|
2025-10-13 18:28:03 +09:00
|
|
|
|
components: updatedComponents,
|
2025-09-29 13:29:03 +09:00
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 화면 파일 복원 완료");
|
2025-09-29 13:29:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("❌ 화면 파일 복원 오류:", error);
|
2025-09-29 13:29:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
}, [selectedScreen?.screenId]);
|
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// 전역 파일 상태 변경 이벤트 리스너
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 ScreenDesigner: 전역 파일 상태 변경 감지", event.detail);
|
2025-10-13 18:28:03 +09:00
|
|
|
|
setForceRenderTrigger((prev) => prev + 1);
|
2025-09-26 13:11:34 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
|
window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
return () => {
|
2025-10-13 18:28:03 +09:00
|
|
|
|
window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener);
|
2025-09-26 13:11:34 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-11-28 14:45:04 +09:00
|
|
|
|
// 화면의 기본 테이블/REST API 정보 로드
|
2025-09-02 16:18:38 +09:00
|
|
|
|
useEffect(() => {
|
2025-11-28 14:45:04 +09:00
|
|
|
|
const loadScreenDataSource = async () => {
|
2025-12-02 13:20:49 +09:00
|
|
|
|
console.log("🔍 [ScreenDesigner] 데이터 소스 로드 시작:", {
|
|
|
|
|
|
screenId: selectedScreen?.screenId,
|
|
|
|
|
|
screenName: selectedScreen?.screenName,
|
|
|
|
|
|
dataSourceType: selectedScreen?.dataSourceType,
|
|
|
|
|
|
tableName: selectedScreen?.tableName,
|
|
|
|
|
|
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
|
|
|
|
|
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
|
|
|
|
|
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
2025-12-02 14:24:43 +09:00
|
|
|
|
// 전체 selectedScreen 객체도 출력
|
|
|
|
|
|
fullScreen: selectedScreen,
|
2025-12-02 13:20:49 +09:00
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-11-28 14:45:04 +09:00
|
|
|
|
// REST API 데이터 소스인 경우
|
2025-12-02 14:24:43 +09:00
|
|
|
|
// 1. dataSourceType이 "restapi"인 경우
|
|
|
|
|
|
// 2. tableName이 restapi_ 또는 _restapi_로 시작하는 경우
|
|
|
|
|
|
// 3. restApiConnectionId가 있는 경우
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const isRestApi =
|
|
|
|
|
|
selectedScreen?.dataSourceType === "restapi" ||
|
|
|
|
|
|
selectedScreen?.tableName?.startsWith("restapi_") ||
|
|
|
|
|
|
selectedScreen?.tableName?.startsWith("_restapi_") ||
|
|
|
|
|
|
!!selectedScreen?.restApiConnectionId;
|
|
|
|
|
|
|
2025-12-02 14:24:43 +09:00
|
|
|
|
console.log("🔍 [ScreenDesigner] REST API 여부:", { isRestApi });
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-12-02 13:20:49 +09:00
|
|
|
|
if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) {
|
2025-11-28 14:45:04 +09:00
|
|
|
|
try {
|
2025-12-02 13:20:49 +09:00
|
|
|
|
// 연결 ID 추출 (restApiConnectionId가 없으면 tableName에서 추출)
|
|
|
|
|
|
let connectionId = selectedScreen?.restApiConnectionId;
|
|
|
|
|
|
if (!connectionId && selectedScreen?.tableName) {
|
|
|
|
|
|
const match = selectedScreen.tableName.match(/restapi_(\d+)/);
|
|
|
|
|
|
connectionId = match ? parseInt(match[1]) : undefined;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-12-02 13:20:49 +09:00
|
|
|
|
if (!connectionId) {
|
|
|
|
|
|
throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-12-02 13:20:49 +09:00
|
|
|
|
console.log("🌐 [ScreenDesigner] REST API 데이터 로드:", { connectionId });
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-11-28 14:45:04 +09:00
|
|
|
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
2025-12-02 13:20:49 +09:00
|
|
|
|
connectionId,
|
|
|
|
|
|
selectedScreen?.restApiEndpoint,
|
|
|
|
|
|
selectedScreen?.restApiJsonPath || "response", // 기본값을 response로 변경
|
2025-11-28 14:45:04 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// REST API 응답에서 컬럼 정보 생성
|
|
|
|
|
|
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
2025-12-02 13:20:49 +09:00
|
|
|
|
tableName: `restapi_${connectionId}`,
|
2025-11-28 14:45:04 +09:00
|
|
|
|
columnName: col.columnName,
|
|
|
|
|
|
columnLabel: col.columnLabel,
|
|
|
|
|
|
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
|
|
|
|
|
webType: col.dataType === "number" ? "number" : "text",
|
|
|
|
|
|
input_type: "text",
|
|
|
|
|
|
widgetType: col.dataType === "number" ? "number" : "text",
|
|
|
|
|
|
isNullable: "YES",
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const tableInfo: TableInfo = {
|
2025-12-02 13:20:49 +09:00
|
|
|
|
tableName: `restapi_${connectionId}`,
|
2025-11-28 14:45:04 +09:00
|
|
|
|
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
|
|
|
|
|
columns,
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-12-02 13:20:49 +09:00
|
|
|
|
console.log("✅ [ScreenDesigner] REST API 컬럼 로드 완료:", {
|
|
|
|
|
|
tableName: tableInfo.tableName,
|
|
|
|
|
|
tableLabel: tableInfo.tableLabel,
|
|
|
|
|
|
columnsCount: columns.length,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
columns: columns.map((c) => c.columnName),
|
2025-12-02 13:20:49 +09:00
|
|
|
|
});
|
2025-11-28 14:45:04 +09:00
|
|
|
|
|
|
|
|
|
|
setTables([tableInfo]);
|
|
|
|
|
|
console.log("REST API 데이터 소스 로드 완료:", {
|
|
|
|
|
|
connectionName: restApiData.connectionInfo.connectionName,
|
|
|
|
|
|
columnsCount: columns.length,
|
|
|
|
|
|
rowsCount: restApiData.total,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("REST API 데이터 소스 로드 실패:", error);
|
|
|
|
|
|
toast.error("REST API 데이터를 불러오는데 실패했습니다.");
|
|
|
|
|
|
setTables([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터베이스 데이터 소스인 경우 (기존 로직)
|
2025-10-16 15:05:24 +09:00
|
|
|
|
const tableName = selectedScreen?.tableName;
|
|
|
|
|
|
if (!tableName) {
|
|
|
|
|
|
setTables([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
|
try {
|
|
|
|
|
|
// 테이블 라벨 조회
|
|
|
|
|
|
const tableListResponse = await tableManagementApi.getTableList();
|
|
|
|
|
|
const currentTable =
|
|
|
|
|
|
tableListResponse.success && tableListResponse.data
|
|
|
|
|
|
? tableListResponse.data.find((t) => t.tableName === tableName)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
const tableLabel = currentTable?.displayName || tableName;
|
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
|
// 현재 화면의 테이블 컬럼 정보 조회 (캐시 버스팅으로 최신 데이터 가져오기)
|
|
|
|
|
|
const columnsResponse = await tableTypeApi.getColumns(tableName, true);
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
|
// widgetType 결정: inputType(entity 등) > webType > widget_type
|
|
|
|
|
|
const inputType = col.inputType || col.input_type;
|
|
|
|
|
|
const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type;
|
|
|
|
|
|
|
|
|
|
|
|
// detailSettings 파싱 (문자열이면 JSON 파싱)
|
|
|
|
|
|
let detailSettings = col.detailSettings || col.detail_settings;
|
|
|
|
|
|
if (typeof detailSettings === "string") {
|
2026-01-29 14:45:04 +09:00
|
|
|
|
// JSON 형식인 경우에만 파싱 시도 (중괄호로 시작하는 경우)
|
|
|
|
|
|
if (detailSettings.trim().startsWith("{")) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
detailSettings = JSON.parse(detailSettings);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn("detailSettings 파싱 실패:", e);
|
|
|
|
|
|
detailSettings = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// JSON이 아닌 일반 문자열인 경우 빈 객체로 처리
|
2025-12-19 15:44:38 +09:00
|
|
|
|
detailSettings = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 엔티티 타입 디버깅
|
|
|
|
|
|
if (inputType === "entity" || widgetType === "entity") {
|
|
|
|
|
|
console.log("🔍 엔티티 컬럼 감지:", {
|
|
|
|
|
|
columnName: col.columnName || col.column_name,
|
|
|
|
|
|
inputType,
|
|
|
|
|
|
widgetType,
|
|
|
|
|
|
detailSettings,
|
|
|
|
|
|
referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-10 14:33:15 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
return {
|
|
|
|
|
|
tableName: col.tableName || tableName,
|
|
|
|
|
|
columnName: col.columnName || col.column_name,
|
|
|
|
|
|
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
|
|
|
|
|
dataType: col.dataType || col.data_type || col.dbType,
|
|
|
|
|
|
webType: col.webType || col.web_type,
|
2025-12-19 15:44:38 +09:00
|
|
|
|
input_type: inputType,
|
|
|
|
|
|
inputType: inputType,
|
2025-11-06 12:11:49 +09:00
|
|
|
|
widgetType,
|
|
|
|
|
|
isNullable: col.isNullable || col.is_nullable,
|
|
|
|
|
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
|
|
|
|
|
columnDefault: col.columnDefault || col.column_default,
|
|
|
|
|
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
|
|
|
|
|
codeCategory: col.codeCategory || col.code_category,
|
|
|
|
|
|
codeValue: col.codeValue || col.code_value,
|
2025-12-19 15:44:38 +09:00
|
|
|
|
// 엔티티 타입용 참조 테이블 정보 (detailSettings에서 추출)
|
|
|
|
|
|
referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table,
|
|
|
|
|
|
referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column,
|
|
|
|
|
|
displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column,
|
2026-01-28 17:36:19 +09:00
|
|
|
|
// detailSettings 전체 보존 (V2 컴포넌트용)
|
2025-12-19 15:44:38 +09:00
|
|
|
|
detailSettings,
|
2025-11-06 12:11:49 +09:00
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
|
|
|
|
|
const tableInfo: TableInfo = {
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
tableLabel,
|
|
|
|
|
|
columns,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
setTables([tableInfo]); // 현재 화면의 테이블만 저장 (원래대로)
|
2025-10-16 15:05:24 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("화면 테이블 정보 로드 실패:", error);
|
|
|
|
|
|
setTables([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-28 14:45:04 +09:00
|
|
|
|
loadScreenDataSource();
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
}, [
|
|
|
|
|
|
selectedScreen?.tableName,
|
|
|
|
|
|
selectedScreen?.screenName,
|
|
|
|
|
|
selectedScreen?.dataSourceType,
|
|
|
|
|
|
selectedScreen?.restApiConnectionId,
|
|
|
|
|
|
selectedScreen?.restApiEndpoint,
|
|
|
|
|
|
selectedScreen?.restApiJsonPath,
|
|
|
|
|
|
]);
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2026-01-15 15:17:52 +09:00
|
|
|
|
// 테이블 선택 핸들러 - 사이드바에서 테이블 선택 시 호출
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const handleTableSelect = useCallback(
|
|
|
|
|
|
async (tableName: string) => {
|
|
|
|
|
|
console.log("📊 테이블 선택:", tableName);
|
2026-01-15 15:17:52 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
try {
|
|
|
|
|
|
// 테이블 라벨 조회
|
|
|
|
|
|
const tableListResponse = await tableManagementApi.getTableList();
|
|
|
|
|
|
const currentTable =
|
|
|
|
|
|
tableListResponse.success && tableListResponse.data
|
|
|
|
|
|
? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName;
|
2026-01-15 15:17:52 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 테이블 컬럼 정보 조회
|
|
|
|
|
|
const columnsResponse = await tableTypeApi.getColumns(tableName, true);
|
2026-01-15 15:17:52 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => {
|
|
|
|
|
|
const inputType = col.inputType || col.input_type;
|
|
|
|
|
|
const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type;
|
2026-01-15 15:17:52 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
let detailSettings = col.detailSettings || col.detail_settings;
|
|
|
|
|
|
if (typeof detailSettings === "string") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
detailSettings = JSON.parse(detailSettings);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
detailSettings = {};
|
2026-01-15 15:17:52 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
tableName: col.tableName || tableName,
|
|
|
|
|
|
columnName: col.columnName || col.column_name,
|
|
|
|
|
|
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
|
|
|
|
|
dataType: col.dataType || col.data_type || col.dbType,
|
|
|
|
|
|
webType: col.webType || col.web_type,
|
|
|
|
|
|
input_type: inputType,
|
|
|
|
|
|
inputType: inputType,
|
|
|
|
|
|
widgetType,
|
|
|
|
|
|
isNullable: col.isNullable || col.is_nullable,
|
|
|
|
|
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
|
|
|
|
|
columnDefault: col.columnDefault || col.column_default,
|
|
|
|
|
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
|
|
|
|
|
codeCategory: col.codeCategory || col.code_category,
|
|
|
|
|
|
codeValue: col.codeValue || col.code_value,
|
|
|
|
|
|
referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table,
|
|
|
|
|
|
referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column,
|
|
|
|
|
|
displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column,
|
|
|
|
|
|
detailSettings,
|
2026-01-15 15:17:52 +09:00
|
|
|
|
};
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
const tableInfo: TableInfo = {
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
tableLabel,
|
|
|
|
|
|
columns,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setTables([tableInfo]);
|
|
|
|
|
|
toast.success(`테이블 "${tableLabel}" 선택됨`);
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 테이블과 다른 테이블 선택 시, 기존 컴포넌트 중 다른 테이블 컬럼은 제거
|
|
|
|
|
|
if (tables.length > 0 && tables[0].tableName !== tableName) {
|
|
|
|
|
|
setLayout((prev) => {
|
|
|
|
|
|
const newComponents = prev.components.filter((comp) => {
|
|
|
|
|
|
// 테이블 컬럼 기반 컴포넌트인지 확인
|
|
|
|
|
|
if (comp.tableName && comp.tableName !== tableName) {
|
|
|
|
|
|
console.log("🗑️ 다른 테이블 컴포넌트 제거:", comp.tableName, comp.columnName);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (newComponents.length < prev.components.length) {
|
|
|
|
|
|
toast.info(
|
|
|
|
|
|
`이전 테이블(${tables[0].tableName})의 컴포넌트가 ${prev.components.length - newComponents.length}개 제거되었습니다.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
components: newComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("테이블 정보 로드 실패:", error);
|
|
|
|
|
|
toast.error("테이블 정보를 불러오는데 실패했습니다.");
|
2026-01-15 15:17:52 +09:00
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
},
|
|
|
|
|
|
[tables],
|
|
|
|
|
|
);
|
2026-01-15 15:17:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 화면 레이아웃 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedScreen?.screenId) {
|
2025-09-26 17:12:03 +09:00
|
|
|
|
// 현재 화면 ID를 전역 변수로 설정 (파일 업로드 시 사용)
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (typeof window !== "undefined") {
|
2025-09-26 17:12:03 +09:00
|
|
|
|
(window as any).__CURRENT_SCREEN_ID__ = selectedScreen.screenId;
|
|
|
|
|
|
}
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const loadLayout = async () => {
|
|
|
|
|
|
try {
|
2025-11-11 16:28:17 +09:00
|
|
|
|
// 🆕 화면에 할당된 메뉴 조회
|
|
|
|
|
|
const menuInfo = await screenApi.getScreenMenu(selectedScreen.screenId);
|
|
|
|
|
|
if (menuInfo) {
|
|
|
|
|
|
setMenuObjid(menuInfo.menuObjid);
|
|
|
|
|
|
console.log("🔗 화면에 할당된 메뉴:", menuInfo);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ 화면에 할당된 메뉴가 없습니다");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
|
// V2/POP API 사용 여부에 따라 분기
|
2026-01-28 11:24:25 +09:00
|
|
|
|
let response: any;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
if (USE_POP_API) {
|
|
|
|
|
|
// POP 모드: screen_layouts_pop 테이블 사용
|
|
|
|
|
|
const popResponse = await screenApi.getLayoutPop(selectedScreen.screenId);
|
|
|
|
|
|
response = popResponse ? convertV2ToLegacy(popResponse) : null;
|
|
|
|
|
|
console.log("📱 POP 레이아웃 로드:", popResponse?.components?.length || 0, "개 컴포넌트");
|
|
|
|
|
|
} else if (USE_V2_API) {
|
|
|
|
|
|
// 데스크톱 V2 모드: screen_layouts_v2 테이블 사용
|
2026-01-28 11:24:25 +09:00
|
|
|
|
const v2Response = await screenApi.getLayoutV2(selectedScreen.screenId);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🐛 디버깅: API 응답에서 fieldMapping.id 확인
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const splitPanelInV2 = v2Response?.components?.find((c: any) => c.url?.includes("v2-split-panel-layout"));
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const finishedTimelineInV2 = splitPanelInV2?.overrides?.rightPanel?.components?.find(
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
(c: any) => c.id === "finished_timeline",
|
2026-02-02 17:11:00 +09:00
|
|
|
|
);
|
|
|
|
|
|
console.log("🐛 [API 응답 RAW] finished_timeline:", JSON.stringify(finishedTimelineInV2, null, 2));
|
|
|
|
|
|
console.log("🐛 [API 응답] finished_timeline fieldMapping:", {
|
|
|
|
|
|
fieldMapping: JSON.stringify(finishedTimelineInV2?.componentConfig?.fieldMapping),
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
fieldMappingKeys: finishedTimelineInV2?.componentConfig?.fieldMapping
|
|
|
|
|
|
? Object.keys(finishedTimelineInV2?.componentConfig?.fieldMapping)
|
|
|
|
|
|
: [],
|
2026-02-02 17:11:00 +09:00
|
|
|
|
hasId: !!finishedTimelineInV2?.componentConfig?.fieldMapping?.id,
|
|
|
|
|
|
idValue: finishedTimelineInV2?.componentConfig?.fieldMapping?.id,
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-28 11:24:25 +09:00
|
|
|
|
response = v2Response ? convertV2ToLegacy(v2Response) : null;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response = await screenApi.getLayout(selectedScreen.screenId);
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (response) {
|
2026-01-28 11:24:25 +09:00
|
|
|
|
// 🔄 마이그레이션 필요 여부 확인 (V2는 스킵)
|
2025-10-13 18:28:03 +09:00
|
|
|
|
let layoutToUse = response;
|
|
|
|
|
|
|
2026-01-28 11:24:25 +09:00
|
|
|
|
if (!USE_V2_API && needsMigration(response)) {
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const canvasWidth = response.screenResolution?.width || 1920;
|
|
|
|
|
|
layoutToUse = safeMigrateLayout(response, canvasWidth);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 14:27:07 +09:00
|
|
|
|
// 🔄 webTypeConfig를 autoGeneration으로 변환
|
|
|
|
|
|
const { convertLayoutComponents } = await import("@/lib/utils/webTypeConfigConverter");
|
|
|
|
|
|
const convertedComponents = convertLayoutComponents(layoutToUse.components);
|
|
|
|
|
|
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// 기본 격자 설정 보장 (격자 표시와 스냅 기본 활성화)
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const layoutWithDefaultGrid = {
|
2025-10-13 18:28:03 +09:00
|
|
|
|
...layoutToUse,
|
2025-11-07 14:27:07 +09:00
|
|
|
|
components: convertedComponents, // 변환된 컴포넌트 사용
|
2025-09-02 16:18:38 +09:00
|
|
|
|
gridSettings: {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
columns: layoutToUse.gridSettings?.columns || 12, // DB 값 우선, 없으면 기본값 12
|
|
|
|
|
|
gap: layoutToUse.gridSettings?.gap ?? 16, // DB 값 우선, 없으면 기본값 16
|
|
|
|
|
|
padding: 0, // padding은 항상 0으로 강제
|
2025-11-06 17:01:13 +09:00
|
|
|
|
snapToGrid: layoutToUse.gridSettings?.snapToGrid ?? true, // DB 값 우선
|
|
|
|
|
|
showGrid: layoutToUse.gridSettings?.showGrid ?? false, // DB 값 우선
|
|
|
|
|
|
gridColor: layoutToUse.gridSettings?.gridColor || "#d1d5db",
|
|
|
|
|
|
gridOpacity: layoutToUse.gridSettings?.gridOpacity ?? 0.5,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
|
|
|
|
|
// 저장된 해상도 정보가 있으면 적용, 없으면 기본값 사용
|
2025-10-13 18:28:03 +09:00
|
|
|
|
if (layoutToUse.screenResolution) {
|
|
|
|
|
|
setScreenResolution(layoutToUse.screenResolution);
|
|
|
|
|
|
// console.log("💾 저장된 해상도 불러옴:", layoutToUse.screenResolution);
|
2025-09-04 15:20:26 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
// 기본 해상도 (Full HD)
|
|
|
|
|
|
const defaultResolution =
|
|
|
|
|
|
SCREEN_RESOLUTIONS.find((r) => r.name === "Full HD (1920×1080)") || SCREEN_RESOLUTIONS[0];
|
|
|
|
|
|
setScreenResolution(defaultResolution);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 기본 해상도 적용:", defaultResolution);
|
2025-09-04 15:20:26 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 18:35:07 +09:00
|
|
|
|
// 🔍 디버깅: 로드된 버튼 컴포넌트의 action 확인
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const buttonComponents = layoutWithDefaultGrid.components.filter((c: any) =>
|
|
|
|
|
|
c.componentType?.startsWith("button"),
|
|
|
|
|
|
);
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
"🔍 [로드] 버튼 컴포넌트 action 확인:",
|
|
|
|
|
|
buttonComponents.map((c: any) => ({
|
|
|
|
|
|
id: c.id,
|
|
|
|
|
|
type: c.componentType,
|
|
|
|
|
|
actionType: c.componentConfig?.action?.type,
|
|
|
|
|
|
fullAction: c.componentConfig?.action,
|
|
|
|
|
|
})),
|
2025-11-28 18:35:07 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setLayout(layoutWithDefaultGrid);
|
|
|
|
|
|
setHistory([layoutWithDefaultGrid]);
|
|
|
|
|
|
setHistoryIndex(0);
|
2025-09-26 17:12:03 +09:00
|
|
|
|
|
|
|
|
|
|
// 파일 컴포넌트 데이터 복원 (비동기)
|
|
|
|
|
|
restoreFileComponentsData(layoutWithDefaultGrid.components);
|
2026-02-09 13:21:56 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 레이어 영역 로드 (조건부 레이어의 displayRegion)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const layers = await screenApi.getScreenLayers(selectedScreen.screenId);
|
|
|
|
|
|
const regions: Record<number, any> = {};
|
|
|
|
|
|
for (const layer of layers) {
|
|
|
|
|
|
if (layer.layer_id > 1 && layer.condition_config?.displayRegion) {
|
|
|
|
|
|
regions[layer.layer_id] = {
|
|
|
|
|
|
...layer.condition_config.displayRegion,
|
|
|
|
|
|
layerName: layer.layer_name,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setLayerRegions(regions);
|
|
|
|
|
|
} catch { /* 레이어 로드 실패 무시 */ }
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("레이아웃 로드 실패:", error);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.error("화면 레이아웃을 불러오는데 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
loadLayout();
|
2025-09-01 17:05:36 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}, [selectedScreen?.screenId]);
|
|
|
|
|
|
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
|
2025-10-15 10:44:05 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
2025-11-25 13:04:58 +09:00
|
|
|
|
// 입력 필드에서는 스페이스바 무시 (activeElement로 정확하게 체크)
|
|
|
|
|
|
const activeElement = document.activeElement;
|
|
|
|
|
|
if (
|
|
|
|
|
|
activeElement instanceof HTMLInputElement ||
|
|
|
|
|
|
activeElement instanceof HTMLTextAreaElement ||
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
activeElement?.getAttribute("contenteditable") === "true" ||
|
|
|
|
|
|
activeElement?.getAttribute("role") === "textbox"
|
2025-11-25 13:04:58 +09:00
|
|
|
|
) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// e.target도 함께 체크 (이중 방어)
|
2025-10-15 10:44:05 +09:00
|
|
|
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (e.code === "Space") {
|
|
|
|
|
|
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
|
|
|
|
|
|
if (!isPanMode) {
|
|
|
|
|
|
setIsPanMode(true);
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// body에 커서 스타일 추가
|
|
|
|
|
|
document.body.style.cursor = "grab";
|
2025-10-15 10:44:05 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => {
|
2025-11-25 13:04:58 +09:00
|
|
|
|
// 입력 필드에서는 스페이스바 무시
|
|
|
|
|
|
const activeElement = document.activeElement;
|
|
|
|
|
|
if (
|
|
|
|
|
|
activeElement instanceof HTMLInputElement ||
|
|
|
|
|
|
activeElement instanceof HTMLTextAreaElement ||
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
activeElement?.getAttribute("contenteditable") === "true" ||
|
|
|
|
|
|
activeElement?.getAttribute("role") === "textbox"
|
2025-11-25 13:04:58 +09:00
|
|
|
|
) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:44:05 +09:00
|
|
|
|
if (e.code === "Space") {
|
|
|
|
|
|
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
|
|
|
|
|
|
setIsPanMode(false);
|
|
|
|
|
|
setPanState((prev) => ({ ...prev, isPanning: false }));
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// body 커서 스타일 복원
|
|
|
|
|
|
document.body.style.cursor = "default";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseDown = (e: MouseEvent) => {
|
2025-10-22 17:19:47 +09:00
|
|
|
|
if (isPanMode) {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
e.preventDefault();
|
2025-10-22 17:19:47 +09:00
|
|
|
|
// 외부와 내부 스크롤 컨테이너 모두 저장
|
2025-10-15 13:30:11 +09:00
|
|
|
|
setPanState({
|
|
|
|
|
|
isPanning: true,
|
|
|
|
|
|
startX: e.pageX,
|
|
|
|
|
|
startY: e.pageY,
|
2025-10-22 17:19:47 +09:00
|
|
|
|
outerScrollLeft: canvasContainerRef.current?.scrollLeft || 0,
|
|
|
|
|
|
outerScrollTop: canvasContainerRef.current?.scrollTop || 0,
|
|
|
|
|
|
innerScrollLeft: canvasRef.current?.scrollLeft || 0,
|
|
|
|
|
|
innerScrollTop: canvasRef.current?.scrollTop || 0,
|
2025-10-15 13:30:11 +09:00
|
|
|
|
});
|
|
|
|
|
|
// 드래그 중 커서 변경
|
|
|
|
|
|
document.body.style.cursor = "grabbing";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
2025-10-22 17:19:47 +09:00
|
|
|
|
if (isPanMode && panState.isPanning) {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const dx = e.pageX - panState.startX;
|
|
|
|
|
|
const dy = e.pageY - panState.startY;
|
2025-10-22 17:19:47 +09:00
|
|
|
|
|
|
|
|
|
|
// 외부 컨테이너 스크롤
|
|
|
|
|
|
if (canvasContainerRef.current) {
|
|
|
|
|
|
canvasContainerRef.current.scrollLeft = panState.outerScrollLeft - dx;
|
|
|
|
|
|
canvasContainerRef.current.scrollTop = panState.outerScrollTop - dy;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 내부 캔버스 스크롤
|
|
|
|
|
|
if (canvasRef.current) {
|
|
|
|
|
|
canvasRef.current.scrollLeft = panState.innerScrollLeft - dx;
|
|
|
|
|
|
canvasRef.current.scrollTop = panState.innerScrollTop - dy;
|
|
|
|
|
|
}
|
2025-10-15 13:30:11 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
|
if (isPanMode) {
|
|
|
|
|
|
setPanState((prev) => ({ ...prev, isPanning: false }));
|
|
|
|
|
|
// 드래그 종료 시 커서 복원
|
|
|
|
|
|
document.body.style.cursor = "grab";
|
2025-10-15 10:44:05 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
window.addEventListener("keyup", handleKeyUp);
|
2025-10-15 13:30:11 +09:00
|
|
|
|
window.addEventListener("mousedown", handleMouseDown);
|
|
|
|
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
|
|
|
|
window.addEventListener("mouseup", handleMouseUp);
|
2025-10-15 10:44:05 +09:00
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
window.removeEventListener("keyup", handleKeyUp);
|
2025-10-15 13:30:11 +09:00
|
|
|
|
window.removeEventListener("mousedown", handleMouseDown);
|
|
|
|
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
|
|
|
|
window.removeEventListener("mouseup", handleMouseUp);
|
2025-10-15 10:44:05 +09:00
|
|
|
|
};
|
2025-10-22 17:19:47 +09:00
|
|
|
|
}, [
|
|
|
|
|
|
isPanMode,
|
|
|
|
|
|
panState.isPanning,
|
|
|
|
|
|
panState.startX,
|
|
|
|
|
|
panState.startY,
|
|
|
|
|
|
panState.outerScrollLeft,
|
|
|
|
|
|
panState.outerScrollTop,
|
|
|
|
|
|
panState.innerScrollLeft,
|
|
|
|
|
|
panState.innerScrollTop,
|
|
|
|
|
|
]);
|
2025-10-15 10:44:05 +09:00
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
// 마우스 휠로 줌 제어 (RAF throttle 적용으로 깜빡임 방지)
|
2025-10-15 10:44:05 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleWheel = (e: WheelEvent) => {
|
|
|
|
|
|
// 캔버스 컨테이너 내에서만 동작
|
|
|
|
|
|
if (canvasContainerRef.current && canvasContainerRef.current.contains(e.target as Node)) {
|
|
|
|
|
|
// Shift 키를 누르지 않은 경우에만 줌 (Shift + 휠은 수평 스크롤용)
|
|
|
|
|
|
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
|
|
|
|
|
// 기본 스크롤 동작 방지
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const delta = e.deltaY;
|
|
|
|
|
|
const zoomFactor = 0.001; // 줌 속도 조절
|
|
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
// RAF throttle: 프레임당 한 번만 상태 업데이트
|
|
|
|
|
|
if (zoomRafRef.current !== null) {
|
|
|
|
|
|
cancelAnimationFrame(zoomRafRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
zoomRafRef.current = requestAnimationFrame(() => {
|
|
|
|
|
|
setZoomLevel((prevZoom) => {
|
|
|
|
|
|
const newZoom = prevZoom - delta * zoomFactor;
|
|
|
|
|
|
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
|
|
|
|
|
|
});
|
|
|
|
|
|
zoomRafRef.current = null;
|
2025-10-15 10:44:05 +09:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// passive: false로 설정하여 preventDefault() 가능하게 함
|
|
|
|
|
|
canvasContainerRef.current?.addEventListener("wheel", handleWheel, { passive: false });
|
|
|
|
|
|
|
|
|
|
|
|
const containerRef = canvasContainerRef.current;
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
containerRef?.removeEventListener("wheel", handleWheel);
|
2026-02-06 15:18:27 +09:00
|
|
|
|
if (zoomRafRef.current !== null) {
|
|
|
|
|
|
cancelAnimationFrame(zoomRafRef.current);
|
|
|
|
|
|
}
|
2025-10-15 10:44:05 +09:00
|
|
|
|
};
|
|
|
|
|
|
}, [MIN_ZOOM, MAX_ZOOM]);
|
|
|
|
|
|
|
2026-02-04 09:28:16 +09:00
|
|
|
|
// 격자 설정 업데이트 (컴포넌트 자동 조정 제거됨)
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const updateGridSettings = useCallback(
|
|
|
|
|
|
(newGridSettings: GridSettings) => {
|
|
|
|
|
|
const newLayout = { ...layout, gridSettings: newGridSettings };
|
2026-02-04 09:28:16 +09:00
|
|
|
|
// 🆕 격자 설정 변경 시 컴포넌트 크기/위치 자동 조정 로직 제거됨
|
|
|
|
|
|
// 사용자가 명시적으로 "격자 재조정" 버튼을 클릭해야만 조정됨
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-09-01 16:40:24 +09:00
|
|
|
|
},
|
2026-02-04 09:28:16 +09:00
|
|
|
|
[layout, saveToHistory],
|
2025-09-04 15:20:26 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-12-03 10:03:24 +09:00
|
|
|
|
// 해상도 변경 핸들러 (컴포넌트 크기/위치 유지)
|
2025-09-04 15:20:26 +09:00
|
|
|
|
const handleResolutionChange = useCallback(
|
|
|
|
|
|
(newResolution: ScreenResolution) => {
|
2025-10-23 11:25:28 +09:00
|
|
|
|
const oldWidth = screenResolution.width;
|
|
|
|
|
|
const oldHeight = screenResolution.height;
|
|
|
|
|
|
const newWidth = newResolution.width;
|
|
|
|
|
|
const newHeight = newResolution.height;
|
|
|
|
|
|
|
2025-12-03 10:03:24 +09:00
|
|
|
|
console.log("📱 해상도 변경:", {
|
2025-10-23 11:25:28 +09:00
|
|
|
|
from: `${oldWidth}x${oldHeight}`,
|
|
|
|
|
|
to: `${newWidth}x${newHeight}`,
|
2025-12-03 10:03:24 +09:00
|
|
|
|
componentsCount: layout.components.length,
|
2025-09-04 17:01:07 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-04 15:20:26 +09:00
|
|
|
|
setScreenResolution(newResolution);
|
|
|
|
|
|
|
2025-12-03 10:03:24 +09:00
|
|
|
|
// 해상도만 변경하고 컴포넌트 크기/위치는 그대로 유지
|
2025-09-04 17:01:07 +09:00
|
|
|
|
const updatedLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
screenResolution: newResolution,
|
|
|
|
|
|
};
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
setLayout(updatedLayout);
|
|
|
|
|
|
saveToHistory(updatedLayout);
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
toast.success("해상도가 변경되었습니다.", {
|
2025-10-23 11:25:28 +09:00
|
|
|
|
description: `${oldWidth}×${oldHeight} → ${newWidth}×${newHeight}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-03 10:03:24 +09:00
|
|
|
|
console.log("✅ 해상도 변경 완료 (컴포넌트 크기/위치 유지)");
|
2025-09-04 17:01:07 +09:00
|
|
|
|
},
|
|
|
|
|
|
[layout, saveToHistory, screenResolution],
|
|
|
|
|
|
);
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
// 강제 격자 재조정 핸들러 (해상도 변경 후 수동 격자 맞춤용)
|
|
|
|
|
|
const handleForceGridUpdate = useCallback(() => {
|
|
|
|
|
|
if (!layout.gridSettings?.snapToGrid || layout.components.length === 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("격자 재조정 생략: 스냅 비활성화 또는 컴포넌트 없음");
|
2025-09-04 17:01:07 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-04 15:20:26 +09:00
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
console.log("🔄 격자 강제 재조정 시작:", {
|
|
|
|
|
|
componentsCount: layout.components.length,
|
|
|
|
|
|
resolution: `${screenResolution.width}x${screenResolution.height}`,
|
|
|
|
|
|
gridSettings: layout.gridSettings,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 해상도로 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const gridUtilSettings = {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
2025-11-10 14:50:24 +09:00
|
|
|
|
snapToGrid: true,
|
2025-09-04 17:01:07 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const adjustedComponents = layout.components.map((comp) => {
|
2025-11-10 15:45:51 +09:00
|
|
|
|
const snappedPosition = snapToGrid(comp.position, currentGridInfo, gridUtilSettings);
|
|
|
|
|
|
const snappedSize = snapSizeToGrid(comp.size, currentGridInfo, gridUtilSettings);
|
2025-09-04 17:01:07 +09:00
|
|
|
|
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// gridColumns가 없거나 범위를 벗어나면 자동 조정
|
|
|
|
|
|
let adjustedGridColumns = comp.gridColumns;
|
|
|
|
|
|
if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > layout.gridSettings!.columns) {
|
|
|
|
|
|
adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, currentGridInfo, gridUtilSettings);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
position: snappedPosition,
|
|
|
|
|
|
size: snappedSize,
|
2025-11-10 14:21:29 +09:00
|
|
|
|
gridColumns: adjustedGridColumns,
|
2025-09-04 17:01:07 +09:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = { ...layout, components: adjustedComponents };
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 격자 강제 재조정 완료:", {
|
|
|
|
|
|
adjustedComponents: adjustedComponents.length,
|
|
|
|
|
|
gridInfo: {
|
|
|
|
|
|
columnWidth: currentGridInfo.columnWidth.toFixed(2),
|
|
|
|
|
|
totalWidth: currentGridInfo.totalWidth,
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${adjustedComponents.length}개 컴포넌트가 격자에 맞게 재정렬되었습니다.`);
|
|
|
|
|
|
}, [layout, screenResolution, saveToHistory]);
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
// === 정렬/배분/동일크기/라벨토글/Nudge 핸들러 ===
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 정렬
|
|
|
|
|
|
const handleGroupAlign = useCallback(
|
|
|
|
|
|
(mode: AlignMode) => {
|
|
|
|
|
|
if (groupState.selectedComponents.length < 2) {
|
|
|
|
|
|
toast.warning("2개 이상의 컴포넌트를 선택해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
saveToHistory(layout);
|
|
|
|
|
|
const newComponents = alignComponents(layout.components, groupState.selectedComponents, mode);
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: newComponents }));
|
|
|
|
|
|
|
|
|
|
|
|
const modeNames: Record<AlignMode, string> = {
|
|
|
|
|
|
left: "좌측", right: "우측", centerX: "가로 중앙",
|
|
|
|
|
|
top: "상단", bottom: "하단", centerY: "세로 중앙",
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success(`${modeNames[mode]} 정렬 완료`);
|
|
|
|
|
|
},
|
|
|
|
|
|
[groupState.selectedComponents, layout, saveToHistory]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 균등 배분
|
|
|
|
|
|
const handleGroupDistribute = useCallback(
|
|
|
|
|
|
(direction: DistributeDirection) => {
|
|
|
|
|
|
if (groupState.selectedComponents.length < 3) {
|
|
|
|
|
|
toast.warning("3개 이상의 컴포넌트를 선택해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
saveToHistory(layout);
|
|
|
|
|
|
const newComponents = distributeComponents(layout.components, groupState.selectedComponents, direction);
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: newComponents }));
|
|
|
|
|
|
toast.success(`${direction === "horizontal" ? "가로" : "세로"} 균등 배분 완료`);
|
|
|
|
|
|
},
|
|
|
|
|
|
[groupState.selectedComponents, layout, saveToHistory]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 동일 크기 맞추기
|
|
|
|
|
|
const handleMatchSize = useCallback(
|
|
|
|
|
|
(mode: MatchSizeMode) => {
|
|
|
|
|
|
if (groupState.selectedComponents.length < 2) {
|
|
|
|
|
|
toast.warning("2개 이상의 컴포넌트를 선택해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
saveToHistory(layout);
|
|
|
|
|
|
const newComponents = matchComponentSize(
|
|
|
|
|
|
layout.components,
|
|
|
|
|
|
groupState.selectedComponents,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
selectedComponent?.id
|
|
|
|
|
|
);
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: newComponents }));
|
|
|
|
|
|
|
|
|
|
|
|
const modeNames: Record<MatchSizeMode, string> = {
|
|
|
|
|
|
width: "너비", height: "높이", both: "크기",
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success(`${modeNames[mode]} 맞추기 완료`);
|
|
|
|
|
|
},
|
|
|
|
|
|
[groupState.selectedComponents, layout, selectedComponent?.id, saveToHistory]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 라벨 일괄 토글
|
|
|
|
|
|
const handleToggleAllLabels = useCallback(() => {
|
|
|
|
|
|
saveToHistory(layout);
|
|
|
|
|
|
const newComponents = toggleAllLabels(layout.components);
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: newComponents }));
|
|
|
|
|
|
|
|
|
|
|
|
const hasHidden = layout.components.some(
|
|
|
|
|
|
(c) => c.type === "widget" && (c.style as any)?.labelDisplay === false
|
|
|
|
|
|
);
|
|
|
|
|
|
toast.success(hasHidden ? "모든 라벨 표시" : "모든 라벨 숨기기");
|
|
|
|
|
|
}, [layout, saveToHistory]);
|
|
|
|
|
|
|
|
|
|
|
|
// Nudge (화살표 키 이동)
|
|
|
|
|
|
const handleNudge = useCallback(
|
|
|
|
|
|
(direction: "up" | "down" | "left" | "right", distance: number) => {
|
|
|
|
|
|
const targetIds =
|
|
|
|
|
|
groupState.selectedComponents.length > 0
|
|
|
|
|
|
? groupState.selectedComponents
|
|
|
|
|
|
: selectedComponent
|
|
|
|
|
|
? [selectedComponent.id]
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
if (targetIds.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const newComponents = nudgeComponents(layout.components, targetIds, direction, distance);
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: newComponents }));
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 컴포넌트 업데이트
|
|
|
|
|
|
if (selectedComponent && targetIds.includes(selectedComponent.id)) {
|
|
|
|
|
|
const updated = newComponents.find((c) => c.id === selectedComponent.id);
|
|
|
|
|
|
if (updated) setSelectedComponent(updated);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[groupState.selectedComponents, selectedComponent, layout.components]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 저장
|
|
|
|
|
|
const handleSave = useCallback(async () => {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
if (!selectedScreen?.screenId) {
|
|
|
|
|
|
console.error("❌ 저장 실패: selectedScreen 또는 screenId가 없습니다.", selectedScreen);
|
|
|
|
|
|
toast.error("화면 정보가 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
try {
|
|
|
|
|
|
setIsSaving(true);
|
2025-10-15 17:25:38 +09:00
|
|
|
|
|
|
|
|
|
|
// 분할 패널 컴포넌트의 rightPanel.tableName 자동 설정
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp) => {
|
|
|
|
|
|
if (comp.type === "component" && comp.componentType === "split-panel-layout") {
|
|
|
|
|
|
const config = comp.componentConfig || {};
|
|
|
|
|
|
const rightPanel = config.rightPanel || {};
|
|
|
|
|
|
const leftPanel = config.leftPanel || {};
|
|
|
|
|
|
const relationshipType = rightPanel.relation?.type || "detail";
|
|
|
|
|
|
|
|
|
|
|
|
// 관계 타입이 detail이면 rightPanel.tableName을 leftPanel.tableName과 동일하게 설정
|
|
|
|
|
|
if (relationshipType === "detail" && leftPanel.tableName) {
|
|
|
|
|
|
console.log("🔧 분할 패널 자동 수정:", {
|
|
|
|
|
|
componentId: comp.id,
|
|
|
|
|
|
leftTableName: leftPanel.tableName,
|
|
|
|
|
|
rightTableName: leftPanel.tableName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
rightPanel: {
|
|
|
|
|
|
...rightPanel,
|
|
|
|
|
|
tableName: leftPanel.tableName,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return comp;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-04 15:20:26 +09:00
|
|
|
|
// 해상도 정보를 포함한 레이아웃 데이터 생성
|
2026-01-27 09:44:26 +09:00
|
|
|
|
// 현재 선택된 테이블을 화면의 기본 테이블로 저장
|
|
|
|
|
|
const currentMainTableName = tables.length > 0 ? tables[0].tableName : null;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-09-04 15:20:26 +09:00
|
|
|
|
const layoutWithResolution = {
|
|
|
|
|
|
...layout,
|
2025-10-15 17:25:38 +09:00
|
|
|
|
components: updatedComponents,
|
2025-09-04 15:20:26 +09:00
|
|
|
|
screenResolution: screenResolution,
|
2026-01-27 09:44:26 +09:00
|
|
|
|
mainTableName: currentMainTableName, // 화면의 기본 테이블
|
2025-09-04 15:20:26 +09:00
|
|
|
|
};
|
2026-02-02 17:11:00 +09:00
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
|
// V2/POP API 사용 여부에 따라 분기
|
|
|
|
|
|
const v2Layout = convertLegacyToV2(layoutWithResolution);
|
|
|
|
|
|
if (USE_POP_API) {
|
|
|
|
|
|
// POP 모드: screen_layouts_pop 테이블에 저장
|
|
|
|
|
|
await screenApi.saveLayoutPop(selectedScreen.screenId, v2Layout);
|
|
|
|
|
|
} else if (USE_V2_API) {
|
2026-02-09 13:27:59 +09:00
|
|
|
|
// 레이어 기반 저장: 현재 활성 레이어의 layout만 저장
|
2026-02-09 13:21:56 +09:00
|
|
|
|
const currentLayerId = activeLayerIdRef.current || 1;
|
|
|
|
|
|
await screenApi.saveLayoutV2(selectedScreen.screenId, {
|
|
|
|
|
|
...v2Layout,
|
|
|
|
|
|
layerId: currentLayerId,
|
|
|
|
|
|
});
|
2026-01-28 11:24:25 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
|
|
|
|
|
|
}
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// console.log("✅ 저장 성공!");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.success("화면이 저장되었습니다.");
|
2025-09-05 10:25:40 +09:00
|
|
|
|
|
2026-01-27 10:06:40 +09:00
|
|
|
|
// 저장 성공 후 부모에게 화면 정보 업데이트 알림 (테이블명 즉시 반영)
|
|
|
|
|
|
if (onScreenUpdate && currentMainTableName) {
|
|
|
|
|
|
onScreenUpdate({ tableName: currentMainTableName });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 10:25:40 +09:00
|
|
|
|
// 저장 성공 후 메뉴 할당 모달 열기
|
|
|
|
|
|
setShowMenuAssignmentModal(true);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
} catch (error) {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
console.error("❌ 저장 실패:", error);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.error("저장 중 오류가 발생했습니다.");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
|
}
|
2026-01-27 10:06:40 +09:00
|
|
|
|
}, [selectedScreen, layout, screenResolution, tables, onScreenUpdate]);
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
|
// POP 미리보기 핸들러 (새 창에서 열기)
|
|
|
|
|
|
const handlePopPreview = useCallback(() => {
|
|
|
|
|
|
if (!selectedScreen?.screenId) {
|
|
|
|
|
|
toast.error("화면 정보가 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deviceType = defaultDevicePreview || "tablet";
|
|
|
|
|
|
const previewUrl = `/pop/screens/${selectedScreen.screenId}?preview=true&device=${deviceType}`;
|
|
|
|
|
|
window.open(previewUrl, "_blank", "width=800,height=900");
|
|
|
|
|
|
}, [selectedScreen, defaultDevicePreview]);
|
|
|
|
|
|
|
2026-01-14 10:20:27 +09:00
|
|
|
|
// 다국어 자동 생성 핸들러
|
|
|
|
|
|
const handleGenerateMultilang = useCallback(async () => {
|
|
|
|
|
|
if (!selectedScreen?.screenId) {
|
|
|
|
|
|
toast.error("화면 정보가 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsGeneratingMultilang(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-14 13:08:44 +09:00
|
|
|
|
// 공통 유틸 사용하여 라벨 추출
|
|
|
|
|
|
const { extractMultilangLabels, extractTableNames, applyMultilangMappings } = await import(
|
|
|
|
|
|
"@/lib/utils/multilangLabelExtractor"
|
|
|
|
|
|
);
|
|
|
|
|
|
const { apiClient } = await import("@/lib/api/client");
|
2026-01-14 11:05:57 +09:00
|
|
|
|
|
2026-01-14 13:08:44 +09:00
|
|
|
|
// 테이블별 컬럼 라벨 로드
|
|
|
|
|
|
const tableNames = extractTableNames(layout.components);
|
|
|
|
|
|
const columnLabelMap: Record<string, Record<string, string>> = {};
|
2026-01-14 11:05:57 +09:00
|
|
|
|
|
2026-01-14 13:08:44 +09:00
|
|
|
|
for (const tableName of tableNames) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
|
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
|
|
|
|
const columns = response.data.data.columns || response.data.data;
|
|
|
|
|
|
if (Array.isArray(columns)) {
|
|
|
|
|
|
columnLabelMap[tableName] = {};
|
|
|
|
|
|
columns.forEach((col: any) => {
|
|
|
|
|
|
const colName = col.columnName || col.column_name || col.name;
|
|
|
|
|
|
const colLabel = col.displayName || col.columnLabel || col.column_label || colName;
|
|
|
|
|
|
if (colName) {
|
|
|
|
|
|
columnLabelMap[tableName][colName] = colLabel;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-14 10:20:27 +09:00
|
|
|
|
}
|
2026-01-14 13:08:44 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`컬럼 라벨 조회 실패 (${tableName}):`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 10:20:27 +09:00
|
|
|
|
|
2026-01-14 13:08:44 +09:00
|
|
|
|
// 라벨 추출 (다국어 설정과 동일한 로직)
|
|
|
|
|
|
const extractedLabels = extractMultilangLabels(layout.components, columnLabelMap);
|
|
|
|
|
|
const labels = extractedLabels.map((l) => ({
|
|
|
|
|
|
componentId: l.componentId,
|
|
|
|
|
|
label: l.label,
|
|
|
|
|
|
type: l.type,
|
|
|
|
|
|
}));
|
2026-01-14 10:20:27 +09:00
|
|
|
|
|
|
|
|
|
|
if (labels.length === 0) {
|
|
|
|
|
|
toast.info("다국어로 변환할 라벨이 없습니다.");
|
|
|
|
|
|
setIsGeneratingMultilang(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// API 호출
|
|
|
|
|
|
const { generateScreenLabelKeys } = await import("@/lib/api/multilang");
|
|
|
|
|
|
const response = await generateScreenLabelKeys({
|
|
|
|
|
|
screenId: selectedScreen.screenId,
|
|
|
|
|
|
menuObjId: menuObjid?.toString(),
|
|
|
|
|
|
labels,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.success && response.data) {
|
2026-01-14 13:08:44 +09:00
|
|
|
|
// 자동 매핑 적용
|
|
|
|
|
|
const updatedComponents = applyMultilangMappings(layout.components, response.data);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-14 13:08:44 +09:00
|
|
|
|
// 레이아웃 업데이트
|
2026-01-14 13:26:41 +09:00
|
|
|
|
const updatedLayout = {
|
|
|
|
|
|
...layout,
|
2026-01-14 13:08:44 +09:00
|
|
|
|
components: updatedComponents,
|
2026-01-14 13:26:41 +09:00
|
|
|
|
screenResolution: screenResolution,
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-14 13:26:41 +09:00
|
|
|
|
setLayout(updatedLayout);
|
2026-01-14 13:08:44 +09:00
|
|
|
|
|
2026-01-14 13:26:41 +09:00
|
|
|
|
// 자동 저장 (매핑 정보가 손실되지 않도록)
|
|
|
|
|
|
try {
|
2026-02-02 15:15:01 +09:00
|
|
|
|
const v2Layout = convertLegacyToV2(updatedLayout);
|
|
|
|
|
|
if (USE_POP_API) {
|
|
|
|
|
|
await screenApi.saveLayoutPop(selectedScreen.screenId, v2Layout);
|
|
|
|
|
|
} else if (USE_V2_API) {
|
2026-01-28 11:24:25 +09:00
|
|
|
|
await screenApi.saveLayoutV2(selectedScreen.screenId, v2Layout);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await screenApi.saveLayout(selectedScreen.screenId, updatedLayout);
|
|
|
|
|
|
}
|
2026-01-14 13:26:41 +09:00
|
|
|
|
toast.success(`${response.data.length}개의 다국어 키가 생성되고 자동 저장되었습니다.`);
|
|
|
|
|
|
} catch (saveError) {
|
|
|
|
|
|
console.error("다국어 매핑 저장 실패:", saveError);
|
|
|
|
|
|
toast.warning(`${response.data.length}개의 다국어 키가 생성되었습니다. 저장 버튼을 눌러 매핑을 저장하세요.`);
|
|
|
|
|
|
}
|
2026-01-14 10:20:27 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
toast.error(response.error?.details || "다국어 키 생성에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-14 13:08:44 +09:00
|
|
|
|
console.error("다국어 생성 실패:", error);
|
2026-01-14 10:20:27 +09:00
|
|
|
|
toast.error("다국어 키 생성 중 오류가 발생했습니다.");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsGeneratingMultilang(false);
|
|
|
|
|
|
}
|
2026-01-14 13:26:41 +09:00
|
|
|
|
}, [selectedScreen, layout, screenResolution, menuObjid]);
|
2026-01-14 10:20:27 +09:00
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 템플릿 드래그 처리
|
|
|
|
|
|
const handleTemplateDrop = useCallback(
|
|
|
|
|
|
(e: React.DragEvent, template: TemplateComponent) => {
|
|
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
|
|
|
|
|
|
|
|
|
|
|
const dropX = e.clientX - rect.left;
|
|
|
|
|
|
const dropY = e.clientY - rect.top;
|
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
// 현재 해상도에 맞는 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = layout.gridSettings
|
|
|
|
|
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 격자 스냅 적용
|
|
|
|
|
|
const snappedPosition =
|
2025-09-04 17:01:07 +09:00
|
|
|
|
layout.gridSettings?.snapToGrid && currentGridInfo
|
2025-11-10 15:45:51 +09:00
|
|
|
|
? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings)
|
2025-09-03 15:23:12 +09:00
|
|
|
|
: { x: dropX, y: dropY, z: 1 };
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎨 템플릿 드롭:", {
|
|
|
|
|
|
templateName: template.name,
|
|
|
|
|
|
componentsCount: template.components.length,
|
|
|
|
|
|
dropPosition: { x: dropX, y: dropY },
|
|
|
|
|
|
snappedPosition,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 템플릿의 모든 컴포넌트들을 생성
|
2025-09-04 11:33:52 +09:00
|
|
|
|
// 먼저 ID 매핑을 생성 (parentId 참조를 위해)
|
|
|
|
|
|
const idMapping: Record<string, string> = {};
|
|
|
|
|
|
template.components.forEach((templateComp, index) => {
|
|
|
|
|
|
const newId = generateComponentId();
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
|
// 첫 번째 컴포넌트(컨테이너)는 "form-container"로 매핑
|
|
|
|
|
|
idMapping["form-container"] = newId;
|
|
|
|
|
|
}
|
|
|
|
|
|
idMapping[templateComp.parentId || `temp_${index}`] = newId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
const newComponents: ComponentData[] = template.components.map((templateComp, index) => {
|
2025-09-04 11:33:52 +09:00
|
|
|
|
const componentId = index === 0 ? idMapping["form-container"] : generateComponentId();
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
// 템플릿 컴포넌트의 상대 위치를 드롭 위치 기준으로 조정
|
|
|
|
|
|
const absoluteX = snappedPosition.x + templateComp.position.x;
|
|
|
|
|
|
const absoluteY = snappedPosition.y + templateComp.position.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 격자 스냅 적용
|
|
|
|
|
|
const finalPosition =
|
2025-09-04 17:01:07 +09:00
|
|
|
|
layout.gridSettings?.snapToGrid && currentGridInfo
|
2025-11-10 14:46:30 +09:00
|
|
|
|
? snapPositionTo10px(
|
|
|
|
|
|
{ x: absoluteX, y: absoluteY, z: 1 },
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
)
|
2025-09-03 15:23:12 +09:00
|
|
|
|
: { x: absoluteX, y: absoluteY, z: 1 };
|
|
|
|
|
|
|
|
|
|
|
|
if (templateComp.type === "container") {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// 그리드 컬럼 기반 크기 계산
|
|
|
|
|
|
const gridColumns =
|
|
|
|
|
|
typeof templateComp.size.width === "number" && templateComp.size.width <= 12 ? templateComp.size.width : 4; // 기본 4컬럼
|
|
|
|
|
|
|
|
|
|
|
|
const calculatedSize =
|
|
|
|
|
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
|
|
|
|
|
? (() => {
|
|
|
|
|
|
const newWidth = calculateWidthFromColumns(
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: newWidth,
|
|
|
|
|
|
height: templateComp.size.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
})()
|
|
|
|
|
|
: { width: 400, height: templateComp.size.height }; // 폴백 크기
|
2025-09-04 11:33:52 +09:00
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
return {
|
|
|
|
|
|
id: componentId,
|
|
|
|
|
|
type: "container",
|
|
|
|
|
|
label: templateComp.label,
|
|
|
|
|
|
tableName: selectedScreen?.tableName || "",
|
2025-09-04 11:33:52 +09:00
|
|
|
|
title: templateComp.title || templateComp.label,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
position: finalPosition,
|
2025-09-04 11:33:52 +09:00
|
|
|
|
size: calculatedSize,
|
|
|
|
|
|
gridColumns,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
...templateComp.style,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (templateComp.type === "datatable") {
|
|
|
|
|
|
// 데이터 테이블 컴포넌트 생성
|
|
|
|
|
|
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
|
|
|
|
|
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// gridColumns에 맞는 크기 계산
|
|
|
|
|
|
const calculatedSize =
|
|
|
|
|
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
|
|
|
|
|
? (() => {
|
|
|
|
|
|
const newWidth = calculateWidthFromColumns(
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: newWidth,
|
|
|
|
|
|
height: templateComp.size.height, // 높이는 템플릿 값 유지
|
|
|
|
|
|
};
|
|
|
|
|
|
})()
|
|
|
|
|
|
: templateComp.size;
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("📊 데이터 테이블 생성 시 크기 계산:", {
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
templateSize: templateComp.size,
|
|
|
|
|
|
calculatedSize,
|
2025-09-04 17:01:07 +09:00
|
|
|
|
hasGridInfo: !!currentGridInfo,
|
2025-11-10 14:54:53 +09:00
|
|
|
|
hasGridSettings: !!layout.gridSettings,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: componentId,
|
|
|
|
|
|
type: "datatable",
|
|
|
|
|
|
label: templateComp.label,
|
|
|
|
|
|
tableName: selectedScreen?.tableName || "",
|
|
|
|
|
|
position: finalPosition,
|
|
|
|
|
|
size: calculatedSize,
|
|
|
|
|
|
title: templateComp.label,
|
|
|
|
|
|
columns: [], // 초기에는 빈 배열, 나중에 설정
|
|
|
|
|
|
filters: [], // 초기에는 빈 배열, 나중에 설정
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
pageSizeOptions: [5, 10, 20, 50],
|
|
|
|
|
|
showPageSizeSelector: true,
|
|
|
|
|
|
showPageInfo: true,
|
|
|
|
|
|
showFirstLast: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
showSearchButton: true,
|
|
|
|
|
|
searchButtonText: "검색",
|
|
|
|
|
|
enableExport: true,
|
|
|
|
|
|
enableRefresh: true,
|
2025-09-03 16:38:10 +09:00
|
|
|
|
enableAdd: true,
|
|
|
|
|
|
enableEdit: true,
|
|
|
|
|
|
enableDelete: true,
|
|
|
|
|
|
addButtonText: "추가",
|
|
|
|
|
|
editButtonText: "수정",
|
|
|
|
|
|
deleteButtonText: "삭제",
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
title: "새 데이터 추가",
|
|
|
|
|
|
description: `${templateComp.label}에 새로운 데이터를 추가합니다.`,
|
|
|
|
|
|
width: "lg",
|
|
|
|
|
|
layout: "two-column",
|
|
|
|
|
|
gridColumns: 2,
|
|
|
|
|
|
fieldOrder: [], // 초기에는 빈 배열, 나중에 컬럼 추가 시 설정
|
|
|
|
|
|
requiredFields: [],
|
|
|
|
|
|
hiddenFields: [],
|
|
|
|
|
|
advancedFieldConfigs: {}, // 초기에는 빈 객체, 나중에 컬럼별 설정
|
|
|
|
|
|
submitButtonText: "추가",
|
|
|
|
|
|
cancelButtonText: "취소",
|
|
|
|
|
|
},
|
2025-09-03 15:23:12 +09:00
|
|
|
|
gridColumns,
|
|
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
...templateComp.style,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as ComponentData;
|
2025-09-05 21:52:19 +09:00
|
|
|
|
} else if (templateComp.type === "file") {
|
|
|
|
|
|
// 파일 첨부 컴포넌트 생성
|
|
|
|
|
|
const gridColumns = 6; // 기본값: 6컬럼
|
|
|
|
|
|
|
2025-11-10 14:21:29 +09:00
|
|
|
|
const calculatedSize =
|
|
|
|
|
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
|
|
|
|
|
? (() => {
|
|
|
|
|
|
const newWidth = calculateWidthFromColumns(
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: newWidth,
|
|
|
|
|
|
height: templateComp.size.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
})()
|
|
|
|
|
|
: templateComp.size;
|
2025-09-05 21:52:19 +09:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: componentId,
|
|
|
|
|
|
type: "file",
|
|
|
|
|
|
label: templateComp.label,
|
|
|
|
|
|
position: finalPosition,
|
|
|
|
|
|
size: calculatedSize,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
fileConfig: {
|
|
|
|
|
|
accept: ["image/*", ".pdf", ".doc", ".docx", ".xls", ".xlsx"],
|
|
|
|
|
|
multiple: true,
|
|
|
|
|
|
maxSize: 10, // 10MB
|
|
|
|
|
|
maxFiles: 5,
|
|
|
|
|
|
docType: "DOCUMENT",
|
|
|
|
|
|
docTypeName: "일반 문서",
|
|
|
|
|
|
targetObjid: selectedScreen?.screenId || "",
|
|
|
|
|
|
showPreview: true,
|
|
|
|
|
|
showProgress: true,
|
|
|
|
|
|
dragDropText: "파일을 드래그하여 업로드하세요",
|
|
|
|
|
|
uploadButtonText: "파일 선택",
|
|
|
|
|
|
autoUpload: true,
|
|
|
|
|
|
chunkedUpload: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
uploadedFiles: [],
|
|
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-05 21:52:19 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
...templateComp.style,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as ComponentData;
|
2025-09-08 13:10:09 +09:00
|
|
|
|
} else if (templateComp.type === "area") {
|
|
|
|
|
|
// 영역 컴포넌트 생성
|
|
|
|
|
|
const gridColumns = 6; // 기본값: 6컬럼 (50% 너비)
|
|
|
|
|
|
|
2025-11-10 14:21:29 +09:00
|
|
|
|
const calculatedSize =
|
|
|
|
|
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
|
|
|
|
|
? (() => {
|
|
|
|
|
|
const newWidth = calculateWidthFromColumns(
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: newWidth,
|
|
|
|
|
|
height: templateComp.size.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
})()
|
|
|
|
|
|
: templateComp.size;
|
2025-09-08 13:10:09 +09:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: componentId,
|
|
|
|
|
|
type: "area",
|
|
|
|
|
|
label: templateComp.label,
|
|
|
|
|
|
position: finalPosition,
|
|
|
|
|
|
size: calculatedSize,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
layoutType: (templateComp as any).layoutType || "box",
|
|
|
|
|
|
title: (templateComp as any).title || templateComp.label,
|
|
|
|
|
|
description: (templateComp as any).description,
|
|
|
|
|
|
layoutConfig: (templateComp as any).layoutConfig || {},
|
|
|
|
|
|
areaStyle: {
|
|
|
|
|
|
backgroundColor: "#ffffff",
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderStyle: "solid",
|
|
|
|
|
|
borderColor: "#e5e7eb",
|
|
|
|
|
|
borderRadius: 8,
|
2025-10-14 18:07:38 +09:00
|
|
|
|
padding: 0,
|
2025-09-08 13:10:09 +09:00
|
|
|
|
margin: 0,
|
|
|
|
|
|
shadow: "sm",
|
|
|
|
|
|
...(templateComp as any).areaStyle,
|
|
|
|
|
|
},
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-08 13:10:09 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
...templateComp.style,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as ComponentData;
|
2025-09-03 15:23:12 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
// 위젯 컴포넌트
|
|
|
|
|
|
const widgetType = templateComp.widgetType || "text";
|
|
|
|
|
|
|
2025-10-14 16:45:30 +09:00
|
|
|
|
// 웹타입별 기본 그리드 컬럼 수 계산
|
|
|
|
|
|
const getDefaultGridColumnsForTemplate = (wType: string): number => {
|
|
|
|
|
|
const widthMap: Record<string, number> = {
|
|
|
|
|
|
text: 4,
|
|
|
|
|
|
email: 4,
|
|
|
|
|
|
tel: 3,
|
|
|
|
|
|
url: 4,
|
|
|
|
|
|
textarea: 6,
|
|
|
|
|
|
number: 2,
|
|
|
|
|
|
decimal: 2,
|
|
|
|
|
|
date: 3,
|
|
|
|
|
|
datetime: 3,
|
|
|
|
|
|
time: 2,
|
|
|
|
|
|
select: 3,
|
|
|
|
|
|
radio: 3,
|
|
|
|
|
|
checkbox: 2,
|
|
|
|
|
|
boolean: 2,
|
|
|
|
|
|
code: 3,
|
|
|
|
|
|
entity: 4,
|
|
|
|
|
|
file: 4,
|
|
|
|
|
|
image: 3,
|
|
|
|
|
|
button: 2,
|
|
|
|
|
|
label: 2,
|
|
|
|
|
|
};
|
|
|
|
|
|
return widthMap[wType] || 3;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 웹타입별 기본 설정 생성
|
|
|
|
|
|
const getDefaultWebTypeConfig = (wType: string) => {
|
|
|
|
|
|
switch (wType) {
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "YYYY-MM-DD" as const,
|
|
|
|
|
|
showTime: false,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "날짜를 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
case "dropdown":
|
|
|
|
|
|
return {
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ label: "옵션 1", value: "option1" },
|
|
|
|
|
|
{ label: "옵션 2", value: "option2" },
|
|
|
|
|
|
{ label: "옵션 3", value: "option3" },
|
|
|
|
|
|
],
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
searchable: false,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "옵션을 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "none" as const,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "텍스트를 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "email" as const,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "이메일을 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "tel":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "phone" as const,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "전화번호를 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
|
return {
|
|
|
|
|
|
rows: 3,
|
|
|
|
|
|
placeholder: templateComp.placeholder || "텍스트를 입력하세요",
|
|
|
|
|
|
resizable: true,
|
|
|
|
|
|
wordWrap: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
default:
|
|
|
|
|
|
return {
|
|
|
|
|
|
placeholder: templateComp.placeholder || "입력하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
|
// 위젯 크기도 격자에 맞게 조정
|
|
|
|
|
|
const widgetSize =
|
|
|
|
|
|
currentGridInfo && layout.gridSettings?.snapToGrid
|
|
|
|
|
|
? {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
width: calculateWidthFromColumns(1, currentGridInfo, layout.gridSettings as GridUtilSettings),
|
2025-09-04 17:01:07 +09:00
|
|
|
|
height: templateComp.size.height,
|
|
|
|
|
|
}
|
|
|
|
|
|
: templateComp.size;
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
return {
|
|
|
|
|
|
id: componentId,
|
|
|
|
|
|
type: "widget",
|
|
|
|
|
|
widgetType: widgetType as any,
|
|
|
|
|
|
label: templateComp.label,
|
|
|
|
|
|
placeholder: templateComp.placeholder,
|
|
|
|
|
|
columnName: `field_${index + 1}`,
|
2025-09-04 11:33:52 +09:00
|
|
|
|
parentId: templateComp.parentId ? idMapping[templateComp.parentId] : undefined,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
position: finalPosition,
|
2025-09-04 17:01:07 +09:00
|
|
|
|
size: widgetSize,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
required: templateComp.required || false,
|
|
|
|
|
|
readonly: templateComp.readonly || false,
|
2025-10-14 16:45:30 +09:00
|
|
|
|
gridColumns: getDefaultGridColumnsForTemplate(widgetType),
|
2025-09-03 15:23:12 +09:00
|
|
|
|
webTypeConfig: getDefaultWebTypeConfig(widgetType),
|
|
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
...templateComp.style,
|
|
|
|
|
|
},
|
|
|
|
|
|
} as ComponentData;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 🆕 현재 활성 레이어에 컴포넌트 추가 (ref 사용으로 클로저 문제 방지)
|
2026-02-06 09:51:29 +09:00
|
|
|
|
const componentsWithLayerId = newComponents.map((comp) => ({
|
|
|
|
|
|
...comp,
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1,
|
2026-02-06 09:51:29 +09:00
|
|
|
|
}));
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 레이아웃에 새 컴포넌트들 추가
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
2026-02-06 09:51:29 +09:00
|
|
|
|
components: [...layout.components, ...componentsWithLayerId],
|
2025-09-03 15:23:12 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 첫 번째 컴포넌트 선택
|
2026-02-06 09:51:29 +09:00
|
|
|
|
if (componentsWithLayerId.length > 0) {
|
|
|
|
|
|
setSelectedComponent(componentsWithLayerId[0]);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${template.name} 템플릿이 추가되었습니다.`);
|
|
|
|
|
|
},
|
2026-02-09 13:21:56 +09:00
|
|
|
|
[layout, selectedScreen, saveToHistory],
|
2025-09-03 15:23:12 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-10 18:36:28 +09:00
|
|
|
|
// 레이아웃 드래그 처리
|
|
|
|
|
|
const handleLayoutDrop = useCallback(
|
|
|
|
|
|
(e: React.DragEvent, layoutData: any) => {
|
|
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
|
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
|
|
|
|
|
const dropX = (e.clientX - rect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (e.clientY - rect.top) / zoomLevel;
|
2025-09-10 18:36:28 +09:00
|
|
|
|
|
|
|
|
|
|
// 현재 해상도에 맞는 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = layout.gridSettings
|
|
|
|
|
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
// 격자 스냅 적용
|
|
|
|
|
|
const snappedPosition =
|
|
|
|
|
|
layout.gridSettings?.snapToGrid && currentGridInfo
|
2025-11-10 15:45:51 +09:00
|
|
|
|
? snapToGrid({ x: dropX, y: dropY, z: 1 }, currentGridInfo, layout.gridSettings as GridUtilSettings)
|
2025-09-10 18:36:28 +09:00
|
|
|
|
: { x: dropX, y: dropY, z: 1 };
|
|
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
console.log("🏗️ 레이아웃 드롭 (줌 보정):", {
|
|
|
|
|
|
zoomLevel,
|
2025-09-10 18:36:28 +09:00
|
|
|
|
layoutType: layoutData.layoutType,
|
|
|
|
|
|
zonesCount: layoutData.zones.length,
|
2025-11-07 11:36:58 +09:00
|
|
|
|
mouseRaw: { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
2025-09-10 18:36:28 +09:00
|
|
|
|
dropPosition: { x: dropX, y: dropY },
|
|
|
|
|
|
snappedPosition,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃 컴포넌트 생성
|
|
|
|
|
|
const newLayoutComponent: ComponentData = {
|
|
|
|
|
|
id: layoutData.id,
|
|
|
|
|
|
type: "layout",
|
|
|
|
|
|
layoutType: layoutData.layoutType,
|
|
|
|
|
|
layoutConfig: layoutData.layoutConfig,
|
|
|
|
|
|
zones: layoutData.zones.map((zone: any) => ({
|
|
|
|
|
|
...zone,
|
|
|
|
|
|
id: `${layoutData.id}_${zone.id}`, // 레이아웃 ID를 접두사로 추가
|
|
|
|
|
|
})),
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
position: snappedPosition,
|
|
|
|
|
|
size: layoutData.size,
|
|
|
|
|
|
label: layoutData.label,
|
|
|
|
|
|
allowedComponentTypes: layoutData.allowedComponentTypes,
|
|
|
|
|
|
dropZoneConfig: layoutData.dropZoneConfig,
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 추가 (ref 사용)
|
2025-09-10 18:36:28 +09:00
|
|
|
|
} as ComponentData;
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃에 새 컴포넌트 추가
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: [...layout.components, newLayoutComponent],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃 컴포넌트 선택
|
|
|
|
|
|
setSelectedComponent(newLayoutComponent);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${layoutData.label} 레이아웃이 추가되었습니다.`);
|
|
|
|
|
|
},
|
2026-02-09 13:21:56 +09:00
|
|
|
|
[layout, screenResolution, saveToHistory, zoomLevel],
|
2025-09-10 18:36:28 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-11 16:21:00 +09:00
|
|
|
|
// handleZoneComponentDrop은 handleComponentDrop으로 대체됨
|
|
|
|
|
|
|
|
|
|
|
|
// 존 클릭 핸들러
|
|
|
|
|
|
const handleZoneClick = useCallback((zoneId: string) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🎯 존 클릭:", zoneId);
|
2025-09-11 16:21:00 +09:00
|
|
|
|
// 필요시 존 선택 로직 추가
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 웹타입별 기본 설정 생성 함수를 상위로 이동
|
|
|
|
|
|
const getDefaultWebTypeConfig = useCallback((webType: string) => {
|
|
|
|
|
|
switch (webType) {
|
|
|
|
|
|
case "button":
|
|
|
|
|
|
return {
|
|
|
|
|
|
actionType: "custom",
|
|
|
|
|
|
variant: "default",
|
|
|
|
|
|
confirmationMessage: "",
|
|
|
|
|
|
popupTitle: "",
|
|
|
|
|
|
popupContent: "",
|
|
|
|
|
|
navigateUrl: "",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "YYYY-MM-DD",
|
|
|
|
|
|
showTime: false,
|
|
|
|
|
|
placeholder: "날짜를 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "number":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "integer",
|
|
|
|
|
|
placeholder: "숫자를 입력하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
return {
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ label: "옵션 1", value: "option1" },
|
|
|
|
|
|
{ label: "옵션 2", value: "option2" },
|
|
|
|
|
|
{ label: "옵션 3", value: "option3" },
|
|
|
|
|
|
],
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
searchable: false,
|
|
|
|
|
|
placeholder: "옵션을 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "file":
|
|
|
|
|
|
return {
|
|
|
|
|
|
accept: ["*/*"],
|
|
|
|
|
|
maxSize: 10485760, // 10MB
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
showPreview: true,
|
|
|
|
|
|
autoUpload: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
default:
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 드래그 처리 (캔버스 레벨 드롭)
|
2025-09-09 17:42:23 +09:00
|
|
|
|
const handleComponentDrop = useCallback(
|
2025-09-11 16:21:00 +09:00
|
|
|
|
(e: React.DragEvent, component?: any, zoneId?: string, layoutId?: string) => {
|
|
|
|
|
|
// 존별 드롭인 경우 dragData에서 컴포넌트 정보 추출
|
|
|
|
|
|
if (!component) {
|
|
|
|
|
|
const dragData = e.dataTransfer.getData("application/json");
|
|
|
|
|
|
if (!dragData) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsedData = JSON.parse(dragData);
|
|
|
|
|
|
if (parsedData.type === "component") {
|
|
|
|
|
|
component = parsedData.component;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("드래그 데이터 파싱 오류:", error);
|
2025-09-11 16:21:00 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-16 15:12:22 +09:00
|
|
|
|
|
|
|
|
|
|
// 🎯 리피터 컨테이너 내부 드롭 처리
|
|
|
|
|
|
const dropTarget = e.target as HTMLElement;
|
|
|
|
|
|
const repeatContainer = dropTarget.closest('[data-repeat-container="true"]');
|
|
|
|
|
|
if (repeatContainer) {
|
|
|
|
|
|
const containerId = repeatContainer.getAttribute("data-component-id");
|
|
|
|
|
|
if (containerId) {
|
|
|
|
|
|
// 해당 리피터 컨테이너 찾기
|
|
|
|
|
|
const targetComponent = layout.components.find((c) => c.id === containerId);
|
2026-01-19 15:52:59 +09:00
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
// v2-repeat-container 또는 repeat-container 모두 지원
|
|
|
|
|
|
if (targetComponent && (compType === "repeat-container" || compType === "v2-repeat-container")) {
|
2026-01-16 15:12:22 +09:00
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const currentChildren = currentConfig.children || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
// 새 자식 컴포넌트 생성
|
|
|
|
|
|
const newChild = {
|
|
|
|
|
|
id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
|
componentType: component.id || component.componentType || "text-display",
|
|
|
|
|
|
label: component.name || component.label || "새 컴포넌트",
|
|
|
|
|
|
fieldName: "",
|
|
|
|
|
|
position: { x: 0, y: currentChildren.length * 40 },
|
|
|
|
|
|
size: component.defaultSize || { width: 200, height: 32 },
|
|
|
|
|
|
componentConfig: component.defaultConfig || {},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
// 컴포넌트 업데이트
|
|
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
children: [...currentChildren, newChild],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)),
|
2026-01-16 15:12:22 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
return; // 리피터 컨테이너 처리 완료
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🎯 탭 컨테이너 내부 드롭 처리 (중첩 구조 지원)
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
|
|
|
|
|
if (tabsContainer) {
|
|
|
|
|
|
const containerId = tabsContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
|
|
|
|
|
if (containerId && activeTabId) {
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기
|
|
|
|
|
|
let targetComponent = layout.components.find((c) => c.id === containerId);
|
|
|
|
|
|
let parentSplitPanelId: string | null = null;
|
|
|
|
|
|
let parentPanelSide: "left" | "right" | null = null;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기
|
|
|
|
|
|
if (!targetComponent) {
|
|
|
|
|
|
for (const comp of layout.components) {
|
|
|
|
|
|
const compType = (comp as any)?.componentType;
|
|
|
|
|
|
if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") {
|
|
|
|
|
|
const config = (comp as any).componentConfig || {};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 좌측 패널에서 찾기
|
|
|
|
|
|
const leftComponents = config.leftPanel?.components || [];
|
|
|
|
|
|
const foundInLeft = leftComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInLeft) {
|
|
|
|
|
|
targetComponent = foundInLeft;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "left";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 우측 패널에서 찾기
|
|
|
|
|
|
const rightComponents = config.rightPanel?.components || [];
|
|
|
|
|
|
const foundInRight = rightComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInRight) {
|
|
|
|
|
|
targetComponent = foundInRight;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "right";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) {
|
|
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const tabs = currentConfig.tabs || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 활성 탭의 드롭 위치 계산
|
|
|
|
|
|
const tabContentRect = tabsContainer.getBoundingClientRect();
|
|
|
|
|
|
const dropX = (e.clientX - tabContentRect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (e.clientY - tabContentRect.top) / zoomLevel;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 새 컴포넌트 생성
|
2026-01-20 14:01:35 +09:00
|
|
|
|
const componentType = component.id || component.componentType || "v2-text-display";
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 14:01:35 +09:00
|
|
|
|
console.log("🎯 탭에 컴포넌트 드롭:", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
|
componentName: component.name,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
isNested: !!parentSplitPanelId,
|
|
|
|
|
|
parentSplitPanelId,
|
|
|
|
|
|
parentPanelSide,
|
|
|
|
|
|
// 🆕 위치 디버깅
|
|
|
|
|
|
clientX: e.clientX,
|
|
|
|
|
|
clientY: e.clientY,
|
|
|
|
|
|
tabContentRect: { left: tabContentRect.left, top: tabContentRect.top },
|
|
|
|
|
|
zoomLevel,
|
|
|
|
|
|
calculatedPosition: { x: dropX, y: dropY },
|
2026-01-20 14:01:35 +09:00
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const newTabComponent = {
|
|
|
|
|
|
id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
2026-01-20 14:01:35 +09:00
|
|
|
|
componentType: componentType,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
label: component.name || component.label || "새 컴포넌트",
|
|
|
|
|
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
|
|
|
|
|
size: component.defaultSize || { width: 200, height: 100 },
|
|
|
|
|
|
componentConfig: component.defaultConfig || {},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 해당 탭에 컴포넌트 추가
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === activeTabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: [...(tab.components || []), newTabComponent],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = {
|
2026-01-20 10:46:34 +09:00
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
tabs: updatedTabs,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
let newLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: layout.components.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
pc.id === containerId ? updatedTabsComponent : pc,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success("컴포넌트가 중첩된 탭에 추가되었습니다");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)),
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
|
|
|
|
|
toast.success("컴포넌트가 탭에 추가되었습니다");
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
return; // 탭 컨테이너 처리 완료
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 🎯 분할 패널 커스텀 모드 컨테이너 내부 드롭 처리
|
|
|
|
|
|
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
|
|
|
|
|
if (splitPanelContainer) {
|
|
|
|
|
|
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
|
|
|
|
|
if (containerId && panelSide) {
|
|
|
|
|
|
const targetComponent = layout.components.find((c) => c.id === containerId);
|
|
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
|
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = currentConfig[panelKey] || {};
|
|
|
|
|
|
const currentComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 드롭 위치 계산
|
|
|
|
|
|
const panelRect = splitPanelContainer.getBoundingClientRect();
|
|
|
|
|
|
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 새 컴포넌트 생성
|
|
|
|
|
|
const componentType = component.id || component.componentType || "v2-text-display";
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
console.log("🎯 분할 패널에 컴포넌트 드롭:", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
|
panelSide: panelSide,
|
|
|
|
|
|
dropPosition: { x: dropX, y: dropY },
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const newPanelComponent = {
|
|
|
|
|
|
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
|
label: component.name || component.label || "새 컴포넌트",
|
|
|
|
|
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
|
|
|
|
|
size: component.defaultSize || { width: 200, height: 100 },
|
|
|
|
|
|
componentConfig: component.defaultConfig || {},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const updatedPanelConfig = {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: [...currentComponents, newPanelComponent],
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
[panelKey]: updatedPanelConfig,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)),
|
2026-01-30 16:34:05 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
|
|
|
|
|
return; // 분할 패널 처리 완료
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 컴포넌트 크기 정보
|
|
|
|
|
|
const componentWidth = component.defaultSize?.width || 120;
|
|
|
|
|
|
const componentHeight = component.defaultSize?.height || 36;
|
|
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 🔥 중요: 줌 레벨과 transform-origin을 고려한 마우스 위치 계산
|
|
|
|
|
|
// 1. 캔버스가 scale() 변환되어 있음 (transform-origin: top center)
|
|
|
|
|
|
// 2. 캔버스가 justify-center로 중앙 정렬되어 있음
|
2025-11-10 14:33:15 +09:00
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 실제 캔버스 논리적 크기
|
|
|
|
|
|
const canvasLogicalWidth = screenResolution.width;
|
2025-11-10 14:33:15 +09:00
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 화면상 캔버스 실제 크기 (스케일 적용 후)
|
|
|
|
|
|
const canvasVisualWidth = canvasLogicalWidth * zoomLevel;
|
2025-11-10 14:33:15 +09:00
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 중앙 정렬로 인한 왼쪽 오프셋 계산
|
|
|
|
|
|
// rect.left는 이미 중앙 정렬된 위치를 반영하고 있음
|
2025-11-10 14:33:15 +09:00
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 마우스의 캔버스 내 상대 위치 (스케일 보정)
|
|
|
|
|
|
const mouseXInCanvas = (e.clientX - rect.left) / zoomLevel;
|
|
|
|
|
|
const mouseYInCanvas = (e.clientY - rect.top) / zoomLevel;
|
|
|
|
|
|
|
|
|
|
|
|
// 방법 1: 마우스 포인터를 컴포넌트 중심으로
|
|
|
|
|
|
const dropX_centered = mouseXInCanvas - componentWidth / 2;
|
|
|
|
|
|
const dropY_centered = mouseYInCanvas - componentHeight / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 방법 2: 마우스 포인터를 컴포넌트 좌상단으로
|
|
|
|
|
|
const dropX_topleft = mouseXInCanvas;
|
|
|
|
|
|
const dropY_topleft = mouseYInCanvas;
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
|
|
|
|
|
// 사용자가 원하는 방식으로 변경: 마우스 포인터가 좌상단에 오도록
|
|
|
|
|
|
const dropX = dropX_topleft;
|
|
|
|
|
|
const dropY = dropY_topleft;
|
|
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
console.log("🎯 위치 계산 디버깅 (줌 레벨 + 중앙정렬 반영):", {
|
|
|
|
|
|
"1. 줌 레벨": zoomLevel,
|
|
|
|
|
|
"2. 마우스 위치 (화면)": { clientX: e.clientX, clientY: e.clientY },
|
|
|
|
|
|
"3. 캔버스 위치 (rect)": { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
|
|
|
|
"4. 캔버스 논리적 크기": { width: canvasLogicalWidth, height: screenResolution.height },
|
|
|
|
|
|
"5. 캔버스 시각적 크기": { width: canvasVisualWidth, height: screenResolution.height * zoomLevel },
|
|
|
|
|
|
"6. 마우스 캔버스 내 상대위치 (줌 전)": { x: e.clientX - rect.left, y: e.clientY - rect.top },
|
|
|
|
|
|
"7. 마우스 캔버스 내 상대위치 (줌 보정)": { x: mouseXInCanvas, y: mouseYInCanvas },
|
|
|
|
|
|
"8. 컴포넌트 크기": { width: componentWidth, height: componentHeight },
|
|
|
|
|
|
"9a. 중심 방식": { x: dropX_centered, y: dropY_centered },
|
|
|
|
|
|
"9b. 좌상단 방식": { x: dropX_topleft, y: dropY_topleft },
|
|
|
|
|
|
"10. 최종 선택": { dropX, dropY },
|
2025-09-11 18:38:28 +09:00
|
|
|
|
});
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
|
|
|
|
|
// 현재 해상도에 맞는 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = layout.gridSettings
|
|
|
|
|
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 캔버스 경계 내로 위치 제한
|
|
|
|
|
|
const boundedX = Math.max(0, Math.min(dropX, screenResolution.width - componentWidth));
|
|
|
|
|
|
const boundedY = Math.max(0, Math.min(dropY, screenResolution.height - componentHeight));
|
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
|
// 격자 스냅 적용
|
|
|
|
|
|
const snappedPosition =
|
|
|
|
|
|
layout.gridSettings?.snapToGrid && currentGridInfo
|
2025-11-10 14:46:30 +09:00
|
|
|
|
? snapPositionTo10px(
|
|
|
|
|
|
{ x: boundedX, y: boundedY, z: 1 },
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
layout.gridSettings as GridUtilSettings,
|
|
|
|
|
|
)
|
2025-09-11 18:38:28 +09:00
|
|
|
|
: { x: boundedX, y: boundedY, z: 1 };
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("🧩 컴포넌트 드롭:", {
|
|
|
|
|
|
componentName: component.name,
|
|
|
|
|
|
webType: component.webType,
|
2025-09-11 18:38:28 +09:00
|
|
|
|
rawPosition: { x: dropX, y: dropY },
|
|
|
|
|
|
boundedPosition: { x: boundedX, y: boundedY },
|
2025-09-09 17:42:23 +09:00
|
|
|
|
snappedPosition,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
|
// 새 컴포넌트 생성 (새 컴포넌트 시스템 지원)
|
2025-09-10 14:09:32 +09:00
|
|
|
|
console.log("🔍 ScreenDesigner handleComponentDrop:", {
|
|
|
|
|
|
componentName: component.name,
|
2025-09-11 18:38:28 +09:00
|
|
|
|
componentId: component.id,
|
2025-09-10 14:09:32 +09:00
|
|
|
|
webType: component.webType,
|
2025-09-11 18:38:28 +09:00
|
|
|
|
category: component.category,
|
|
|
|
|
|
defaultConfig: component.defaultConfig,
|
2025-10-15 13:30:11 +09:00
|
|
|
|
defaultSize: component.defaultSize,
|
2025-09-10 14:09:32 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-25 16:22:02 +09:00
|
|
|
|
// 컴포넌트별 gridColumns 설정 및 크기 계산
|
2025-09-15 17:10:46 +09:00
|
|
|
|
let componentSize = component.defaultSize;
|
|
|
|
|
|
const isCardDisplay = component.id === "card-display";
|
2025-09-25 16:22:02 +09:00
|
|
|
|
const isTableList = component.id === "table-list";
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-10-14 16:45:30 +09:00
|
|
|
|
// 컴포넌트 타입별 기본 그리드 컬럼 수 설정
|
2025-11-04 16:17:19 +09:00
|
|
|
|
const currentGridColumns = layout.gridSettings.columns; // 현재 격자 컬럼 수
|
2025-10-14 16:45:30 +09:00
|
|
|
|
let gridColumns = 1; // 기본값
|
|
|
|
|
|
|
|
|
|
|
|
// 특수 컴포넌트
|
|
|
|
|
|
if (isCardDisplay) {
|
2025-11-04 16:17:19 +09:00
|
|
|
|
gridColumns = Math.round(currentGridColumns * 0.667); // 약 66.67%
|
2025-10-14 16:45:30 +09:00
|
|
|
|
} else if (isTableList) {
|
2025-11-04 16:17:19 +09:00
|
|
|
|
gridColumns = currentGridColumns; // 테이블은 전체 너비
|
2025-10-14 16:45:30 +09:00
|
|
|
|
} else {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// 웹타입별 적절한 그리드 컬럼 수 설정
|
|
|
|
|
|
const webType = component.webType;
|
|
|
|
|
|
const componentId = component.id;
|
|
|
|
|
|
|
2025-11-04 16:17:19 +09:00
|
|
|
|
// 웹타입별 기본 비율 매핑 (12컬럼 기준 비율)
|
|
|
|
|
|
const gridColumnsRatioMap: Record<string, number> = {
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// 입력 컴포넌트 (INPUT 카테고리)
|
2025-11-04 16:17:19 +09:00
|
|
|
|
"text-input": 4 / 12, // 텍스트 입력 (33%)
|
|
|
|
|
|
"number-input": 2 / 12, // 숫자 입력 (16.67%)
|
|
|
|
|
|
"email-input": 4 / 12, // 이메일 입력 (33%)
|
|
|
|
|
|
"tel-input": 3 / 12, // 전화번호 입력 (25%)
|
|
|
|
|
|
"date-input": 3 / 12, // 날짜 입력 (25%)
|
|
|
|
|
|
"datetime-input": 4 / 12, // 날짜시간 입력 (33%)
|
|
|
|
|
|
"time-input": 2 / 12, // 시간 입력 (16.67%)
|
|
|
|
|
|
"textarea-basic": 6 / 12, // 텍스트 영역 (50%)
|
|
|
|
|
|
"select-basic": 3 / 12, // 셀렉트 (25%)
|
|
|
|
|
|
"checkbox-basic": 2 / 12, // 체크박스 (16.67%)
|
|
|
|
|
|
"radio-basic": 3 / 12, // 라디오 (25%)
|
|
|
|
|
|
"file-basic": 4 / 12, // 파일 (33%)
|
|
|
|
|
|
"file-upload": 4 / 12, // 파일 업로드 (33%)
|
|
|
|
|
|
"slider-basic": 3 / 12, // 슬라이더 (25%)
|
|
|
|
|
|
"toggle-switch": 2 / 12, // 토글 스위치 (16.67%)
|
|
|
|
|
|
"repeater-field-group": 6 / 12, // 반복 필드 그룹 (50%)
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
|
|
|
|
|
// 표시 컴포넌트 (DISPLAY 카테고리)
|
2025-11-04 16:17:19 +09:00
|
|
|
|
"label-basic": 2 / 12, // 라벨 (16.67%)
|
|
|
|
|
|
"text-display": 3 / 12, // 텍스트 표시 (25%)
|
|
|
|
|
|
"card-display": 8 / 12, // 카드 (66.67%)
|
|
|
|
|
|
"badge-basic": 1 / 12, // 배지 (8.33%)
|
|
|
|
|
|
"alert-basic": 6 / 12, // 알림 (50%)
|
|
|
|
|
|
"divider-basic": 1, // 구분선 (100%)
|
|
|
|
|
|
"divider-line": 1, // 구분선 (100%)
|
|
|
|
|
|
"accordion-basic": 1, // 아코디언 (100%)
|
|
|
|
|
|
"table-list": 1, // 테이블 리스트 (100%)
|
|
|
|
|
|
"image-display": 4 / 12, // 이미지 표시 (33%)
|
|
|
|
|
|
"split-panel-layout": 6 / 12, // 분할 패널 레이아웃 (50%)
|
|
|
|
|
|
"flow-widget": 1, // 플로우 위젯 (100%)
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
|
|
|
|
|
// 액션 컴포넌트 (ACTION 카테고리)
|
2025-11-04 16:17:19 +09:00
|
|
|
|
"button-basic": 1 / 12, // 버튼 (8.33%)
|
|
|
|
|
|
"button-primary": 1 / 12, // 프라이머리 버튼 (8.33%)
|
|
|
|
|
|
"button-secondary": 1 / 12, // 세컨더리 버튼 (8.33%)
|
|
|
|
|
|
"icon-button": 1 / 12, // 아이콘 버튼 (8.33%)
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
|
|
|
|
|
// 레이아웃 컴포넌트
|
2025-11-04 16:17:19 +09:00
|
|
|
|
"container-basic": 6 / 12, // 컨테이너 (50%)
|
|
|
|
|
|
"section-basic": 1, // 섹션 (100%)
|
|
|
|
|
|
"panel-basic": 6 / 12, // 패널 (50%)
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
|
|
|
|
|
// 기타
|
2025-11-04 16:17:19 +09:00
|
|
|
|
"image-basic": 4 / 12, // 이미지 (33%)
|
|
|
|
|
|
"icon-basic": 1 / 12, // 아이콘 (8.33%)
|
|
|
|
|
|
"progress-bar": 4 / 12, // 프로그레스 바 (33%)
|
|
|
|
|
|
"chart-basic": 6 / 12, // 차트 (50%)
|
2025-10-15 13:30:11 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-04 16:17:19 +09:00
|
|
|
|
// defaultSize에 gridColumnSpan이 "full"이면 전체 컬럼 사용
|
2025-10-20 10:55:33 +09:00
|
|
|
|
if (component.defaultSize?.gridColumnSpan === "full") {
|
2025-11-04 16:17:19 +09:00
|
|
|
|
gridColumns = currentGridColumns;
|
2025-10-20 10:55:33 +09:00
|
|
|
|
} else {
|
2025-11-04 16:17:19 +09:00
|
|
|
|
// componentId 또는 webType으로 비율 찾기, 없으면 기본값 25%
|
|
|
|
|
|
const ratio = gridColumnsRatioMap[componentId] || gridColumnsRatioMap[webType] || 0.25;
|
|
|
|
|
|
// 현재 격자 컬럼 수에 비율을 곱하여 계산 (최소 1, 최대 currentGridColumns)
|
|
|
|
|
|
gridColumns = Math.max(1, Math.min(currentGridColumns, Math.round(ratio * currentGridColumns)));
|
2025-10-20 10:55:33 +09:00
|
|
|
|
}
|
2025-10-15 13:30:11 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("🎯 컴포넌트 타입별 gridColumns 설정:", {
|
|
|
|
|
|
componentId,
|
|
|
|
|
|
webType,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
});
|
2025-10-14 16:45:30 +09:00
|
|
|
|
}
|
2025-09-15 17:10:46 +09:00
|
|
|
|
|
2025-11-10 15:10:42 +09:00
|
|
|
|
// 10px 단위로 너비 스냅
|
|
|
|
|
|
if (layout.gridSettings?.snapToGrid) {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
componentSize = {
|
|
|
|
|
|
...component.defaultSize,
|
2025-11-10 15:10:42 +09:00
|
|
|
|
width: snapTo10px(component.defaultSize.width),
|
|
|
|
|
|
height: snapTo10px(component.defaultSize.height),
|
2025-11-10 14:21:29 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-15 17:10:46 +09:00
|
|
|
|
|
2025-10-15 13:30:11 +09:00
|
|
|
|
console.log("🎨 최종 컴포넌트 크기:", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
componentName: component.name,
|
|
|
|
|
|
defaultSize: component.defaultSize,
|
|
|
|
|
|
finalSize: componentSize,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
|
// 반복 필드 그룹인 경우 테이블의 첫 번째 컬럼을 기본 필드로 추가
|
|
|
|
|
|
let enhancedDefaultConfig = { ...component.defaultConfig };
|
|
|
|
|
|
if (
|
|
|
|
|
|
component.id === "repeater-field-group" &&
|
|
|
|
|
|
tables &&
|
|
|
|
|
|
tables.length > 0 &&
|
|
|
|
|
|
tables[0].columns &&
|
|
|
|
|
|
tables[0].columns.length > 0
|
|
|
|
|
|
) {
|
|
|
|
|
|
const firstColumn = tables[0].columns[0];
|
|
|
|
|
|
enhancedDefaultConfig = {
|
|
|
|
|
|
...enhancedDefaultConfig,
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: firstColumn.columnName,
|
|
|
|
|
|
label: firstColumn.columnLabel || firstColumn.columnName,
|
|
|
|
|
|
type: (firstColumn.widgetType as any) || "text",
|
|
|
|
|
|
required: firstColumn.required || false,
|
|
|
|
|
|
placeholder: `${firstColumn.columnLabel || firstColumn.columnName}을(를) 입력하세요`,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 16:17:19 +09:00
|
|
|
|
// gridColumns에 맞춰 width를 퍼센트로 계산
|
|
|
|
|
|
const widthPercent = (gridColumns / currentGridColumns) * 100;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎨 [컴포넌트 생성] 너비 계산:", {
|
|
|
|
|
|
componentName: component.name,
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
currentGridColumns,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
widthPercent: `${widthPercent}%`,
|
|
|
|
|
|
calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-08 15:50:58 +09:00
|
|
|
|
// 🆕 라벨을 기반으로 기본 columnName 생성 (한글 → 스네이크 케이스)
|
|
|
|
|
|
// 예: "창고코드" → "warehouse_code" 또는 그대로 유지
|
|
|
|
|
|
const generateDefaultColumnName = (label: string): string => {
|
|
|
|
|
|
// 한글 라벨의 경우 그대로 사용 (나중에 사용자가 수정 가능)
|
|
|
|
|
|
// 영문의 경우 스네이크 케이스로 변환
|
|
|
|
|
|
if (/[가-힣]/.test(label)) {
|
|
|
|
|
|
// 한글이 포함된 경우: 공백을 언더스코어로, 소문자로 변환
|
|
|
|
|
|
return label.replace(/\s+/g, "_").toLowerCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 영문의 경우: 카멜케이스/파스칼케이스를 스네이크 케이스로 변환
|
|
|
|
|
|
return label
|
|
|
|
|
|
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
|
|
|
|
.replace(/\s+/g, "_")
|
|
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
|
const newComponent: ComponentData = {
|
|
|
|
|
|
id: generateComponentId(),
|
2025-09-12 14:24:25 +09:00
|
|
|
|
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
2025-09-09 17:42:23 +09:00
|
|
|
|
label: component.name,
|
2025-12-08 15:50:58 +09:00
|
|
|
|
columnName: generateDefaultColumnName(component.name), // 🆕 기본 columnName 자동 생성
|
2025-09-09 17:42:23 +09:00
|
|
|
|
widgetType: component.webType,
|
2025-09-11 18:38:28 +09:00
|
|
|
|
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
2025-09-09 17:42:23 +09:00
|
|
|
|
position: snappedPosition,
|
2025-09-15 17:10:46 +09:00
|
|
|
|
size: componentSize,
|
2025-09-25 16:22:02 +09:00
|
|
|
|
gridColumns: gridColumns, // 컴포넌트별 그리드 컬럼 수 적용
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 추가 (ref 사용)
|
2025-09-11 18:38:28 +09:00
|
|
|
|
componentConfig: {
|
|
|
|
|
|
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
2025-09-12 14:24:25 +09:00
|
|
|
|
webType: component.webType, // 웹타입 정보 추가
|
2025-10-16 15:05:24 +09:00
|
|
|
|
...enhancedDefaultConfig,
|
2025-09-11 18:38:28 +09:00
|
|
|
|
},
|
2025-09-09 17:42:23 +09:00
|
|
|
|
webTypeConfig: getDefaultWebTypeConfig(component.webType),
|
|
|
|
|
|
style: {
|
2026-02-04 18:01:20 +09:00
|
|
|
|
labelDisplay: true, // 🆕 라벨 기본 표시 (사용자가 끄고 싶으면 체크 해제)
|
2025-09-09 17:42:23 +09:00
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-09 17:42:23 +09:00
|
|
|
|
labelFontWeight: "500",
|
|
|
|
|
|
labelMarginBottom: "4px",
|
2025-11-17 10:01:09 +09:00
|
|
|
|
width: `${componentSize.width}px`, // size와 동기화 (픽셀 단위)
|
|
|
|
|
|
height: `${componentSize.height}px`, // size와 동기화 (픽셀 단위)
|
2025-09-09 17:42:23 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃에 컴포넌트 추가
|
|
|
|
|
|
const newLayout: LayoutData = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: [...layout.components, newComponent],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 새 컴포넌트 선택
|
|
|
|
|
|
setSelectedComponent(newComponent);
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
|
|
|
|
|
// openPanel("properties");
|
2025-09-09 17:42:23 +09:00
|
|
|
|
|
|
|
|
|
|
toast.success(`${component.name} 컴포넌트가 추가되었습니다.`);
|
|
|
|
|
|
},
|
2026-02-09 13:21:56 +09:00
|
|
|
|
[layout, selectedScreen, saveToHistory],
|
2025-09-09 17:42:23 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그 앤 드롭 처리
|
|
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}, []);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const handleDrop = useCallback(
|
2026-02-09 13:21:56 +09:00
|
|
|
|
async (e: React.DragEvent) => {
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
2025-09-01 14:00:31 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const dragData = e.dataTransfer.getData("application/json");
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// console.log("🎯 드롭 이벤트:", { dragData });
|
2025-09-12 14:24:25 +09:00
|
|
|
|
if (!dragData) {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// console.log("❌ 드래그 데이터가 없습니다");
|
2025-09-12 14:24:25 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
try {
|
2025-09-03 15:23:12 +09:00
|
|
|
|
const parsedData = JSON.parse(dragData);
|
2025-11-10 14:21:29 +09:00
|
|
|
|
// console.log("📋 파싱된 데이터:", parsedData);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
// 템플릿 드래그인 경우
|
|
|
|
|
|
if (parsedData.type === "template") {
|
|
|
|
|
|
handleTemplateDrop(e, parsedData.template);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 18:36:28 +09:00
|
|
|
|
// 레이아웃 드래그인 경우
|
|
|
|
|
|
if (parsedData.type === "layout") {
|
|
|
|
|
|
handleLayoutDrop(e, parsedData.layout);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
|
// 컴포넌트 드래그인 경우
|
|
|
|
|
|
if (parsedData.type === "component") {
|
|
|
|
|
|
handleComponentDrop(e, parsedData.component);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 🆕 조건부 레이어 영역 드래그인 경우 → DB condition_config에 displayRegion 저장
|
|
|
|
|
|
if (parsedData.type === "layer-region" && parsedData.layerId && selectedScreen?.screenId) {
|
|
|
|
|
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!canvasRect) return;
|
|
|
|
|
|
const dropX = Math.round((e.clientX - canvasRect.left) / zoomLevel);
|
|
|
|
|
|
const dropY = Math.round((e.clientY - canvasRect.top) / zoomLevel);
|
|
|
|
|
|
const newRegion = {
|
|
|
|
|
|
x: Math.max(0, dropX - 400),
|
|
|
|
|
|
y: Math.max(0, dropY),
|
|
|
|
|
|
width: Math.min(800, screenResolution.width),
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
};
|
|
|
|
|
|
// DB에 displayRegion 저장 (condition_config에 포함)
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 기존 condition_config를 가져와서 displayRegion만 추가/업데이트
|
|
|
|
|
|
const layerData = await screenApi.getLayerLayout(selectedScreen.screenId, parsedData.layerId);
|
|
|
|
|
|
const existingCondition = layerData?.conditionConfig || {};
|
|
|
|
|
|
await screenApi.updateLayerCondition(
|
|
|
|
|
|
selectedScreen.screenId,
|
|
|
|
|
|
parsedData.layerId,
|
|
|
|
|
|
{ ...existingCondition, displayRegion: newRegion }
|
|
|
|
|
|
);
|
|
|
|
|
|
// 레이어 영역 state에 반영 (캔버스에 즉시 표시)
|
|
|
|
|
|
setLayerRegions((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[parsedData.layerId]: { ...newRegion, layerName: parsedData.layerName },
|
|
|
|
|
|
}));
|
|
|
|
|
|
toast.success(`"${parsedData.layerName}" 영역이 배치되었습니다.`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레이어 영역 저장 실패:", error);
|
|
|
|
|
|
toast.error("레이어 영역 저장에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 기존 테이블/컬럼 드래그 처리
|
|
|
|
|
|
const { type, table, column } = parsedData;
|
2025-09-04 11:33:52 +09:00
|
|
|
|
|
|
|
|
|
|
// 드롭 대상이 폼 컨테이너인지 확인
|
|
|
|
|
|
const dropTarget = e.target as HTMLElement;
|
|
|
|
|
|
const formContainer = dropTarget.closest('[data-form-container="true"]');
|
|
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
// 🎯 리피터 컨테이너 내부에 컬럼 드롭 시 처리
|
|
|
|
|
|
const repeatContainer = dropTarget.closest('[data-repeat-container="true"]');
|
|
|
|
|
|
if (repeatContainer && type === "column" && column) {
|
|
|
|
|
|
const containerId = repeatContainer.getAttribute("data-component-id");
|
|
|
|
|
|
if (containerId) {
|
|
|
|
|
|
const targetComponent = layout.components.find((c) => c.id === containerId);
|
2026-01-19 15:52:59 +09:00
|
|
|
|
const rcType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
if (targetComponent && (rcType === "repeat-container" || rcType === "v2-repeat-container")) {
|
2026-01-16 15:12:22 +09:00
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const currentChildren = currentConfig.children || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
// 새 자식 컴포넌트 생성 (컬럼 기반)
|
|
|
|
|
|
const newChild = {
|
|
|
|
|
|
id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
|
componentType: column.widgetType || "text-display",
|
|
|
|
|
|
label: column.columnLabel || column.columnName,
|
|
|
|
|
|
fieldName: column.columnName,
|
|
|
|
|
|
position: { x: 0, y: currentChildren.length * 40 },
|
|
|
|
|
|
size: { width: 200, height: 32 },
|
|
|
|
|
|
componentConfig: {},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
children: [...currentChildren, newChild],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)),
|
2026-01-16 15:12:22 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-16 15:12:22 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🎯 탭 컨테이너 내부에 컬럼 드롭 시 처리 (중첩 구조 지원)
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const tabsContainer = dropTarget.closest('[data-tabs-container="true"]');
|
|
|
|
|
|
if (tabsContainer && type === "column" && column) {
|
|
|
|
|
|
const containerId = tabsContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
|
|
|
|
|
if (containerId && activeTabId) {
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기
|
|
|
|
|
|
let targetComponent = layout.components.find((c) => c.id === containerId);
|
|
|
|
|
|
let parentSplitPanelId: string | null = null;
|
|
|
|
|
|
let parentPanelSide: "left" | "right" | null = null;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기
|
|
|
|
|
|
if (!targetComponent) {
|
|
|
|
|
|
for (const comp of layout.components) {
|
|
|
|
|
|
const compType = (comp as any)?.componentType;
|
|
|
|
|
|
if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") {
|
|
|
|
|
|
const config = (comp as any).componentConfig || {};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 좌측 패널에서 찾기
|
|
|
|
|
|
const leftComponents = config.leftPanel?.components || [];
|
|
|
|
|
|
const foundInLeft = leftComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInLeft) {
|
|
|
|
|
|
targetComponent = foundInLeft;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "left";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 우측 패널에서 찾기
|
|
|
|
|
|
const rightComponents = config.rightPanel?.components || [];
|
|
|
|
|
|
const foundInRight = rightComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInRight) {
|
|
|
|
|
|
targetComponent = foundInRight;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "right";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) {
|
|
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const tabs = currentConfig.tabs || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 드롭 위치 계산
|
|
|
|
|
|
const tabContentRect = tabsContainer.getBoundingClientRect();
|
|
|
|
|
|
const dropX = (e.clientX - tabContentRect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (e.clientY - tabContentRect.top) / zoomLevel;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
|
// 🆕 V2 컴포넌트 매핑 사용 (일반 캔버스와 동일)
|
|
|
|
|
|
const v2Mapping = createV2ConfigFromColumn({
|
2026-01-20 14:01:35 +09:00
|
|
|
|
widgetType: column.widgetType,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
columnLabel: column.columnLabel,
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
inputType: column.inputType,
|
|
|
|
|
|
required: column.required,
|
|
|
|
|
|
detailSettings: column.detailSettings,
|
|
|
|
|
|
referenceTable: column.referenceTable,
|
|
|
|
|
|
referenceColumn: column.referenceColumn,
|
|
|
|
|
|
displayColumn: column.displayColumn,
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 14:01:35 +09:00
|
|
|
|
// 웹타입별 기본 크기 계산
|
|
|
|
|
|
const getTabComponentSize = (widgetType: string) => {
|
|
|
|
|
|
const sizeMap: Record<string, { width: number; height: number }> = {
|
|
|
|
|
|
text: { width: 200, height: 36 },
|
|
|
|
|
|
number: { width: 150, height: 36 },
|
|
|
|
|
|
decimal: { width: 150, height: 36 },
|
|
|
|
|
|
date: { width: 180, height: 36 },
|
|
|
|
|
|
datetime: { width: 200, height: 36 },
|
|
|
|
|
|
select: { width: 200, height: 36 },
|
|
|
|
|
|
category: { width: 200, height: 36 },
|
|
|
|
|
|
code: { width: 200, height: 36 },
|
|
|
|
|
|
entity: { width: 220, height: 36 },
|
|
|
|
|
|
boolean: { width: 120, height: 36 },
|
|
|
|
|
|
checkbox: { width: 120, height: 36 },
|
|
|
|
|
|
textarea: { width: 300, height: 100 },
|
|
|
|
|
|
file: { width: 250, height: 80 },
|
|
|
|
|
|
};
|
|
|
|
|
|
return sizeMap[widgetType] || { width: 200, height: 36 };
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 14:01:35 +09:00
|
|
|
|
const componentSize = getTabComponentSize(column.widgetType);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
const newTabComponent = {
|
|
|
|
|
|
id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
componentType: v2Mapping.componentType,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
label: column.columnLabel || column.columnName,
|
|
|
|
|
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
2026-01-20 14:01:35 +09:00
|
|
|
|
size: componentSize,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
inputType: column.inputType || column.widgetType,
|
|
|
|
|
|
widgetType: column.widgetType,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
componentConfig: {
|
2026-02-02 17:11:00 +09:00
|
|
|
|
...v2Mapping.componentConfig,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
tableName: column.tableName,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
inputType: column.inputType || column.widgetType,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 해당 탭에 컴포넌트 추가
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === activeTabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: [...(tab.components || []), newTabComponent],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = {
|
2026-01-20 10:46:34 +09:00
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
tabs: updatedTabs,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
let newLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: layout.components.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
pc.id === containerId ? updatedTabsComponent : pc,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success("컬럼이 중첩된 탭에 추가되었습니다");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)),
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
|
|
|
|
|
toast.success("컬럼이 탭에 추가되었습니다");
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-20 10:46:34 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 🎯 분할 패널 커스텀 모드 컨테이너 내부에 컬럼 드롭 시 처리
|
|
|
|
|
|
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
|
|
|
|
|
if (splitPanelContainer && type === "column" && column) {
|
|
|
|
|
|
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
|
|
|
|
|
if (containerId && panelSide) {
|
|
|
|
|
|
const targetComponent = layout.components.find((c) => c.id === containerId);
|
|
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
|
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = currentConfig[panelKey] || {};
|
|
|
|
|
|
const currentComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 드롭 위치 계산
|
|
|
|
|
|
const panelRect = splitPanelContainer.getBoundingClientRect();
|
|
|
|
|
|
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// V2 컴포넌트 매핑 사용
|
|
|
|
|
|
const v2Mapping = createV2ConfigFromColumn({
|
|
|
|
|
|
widgetType: column.widgetType,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
columnLabel: column.columnLabel,
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
inputType: column.inputType,
|
|
|
|
|
|
required: column.required,
|
|
|
|
|
|
detailSettings: column.detailSettings,
|
|
|
|
|
|
referenceTable: column.referenceTable,
|
|
|
|
|
|
referenceColumn: column.referenceColumn,
|
|
|
|
|
|
displayColumn: column.displayColumn,
|
|
|
|
|
|
});
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 웹타입별 기본 크기 계산
|
|
|
|
|
|
const getPanelComponentSize = (widgetType: string) => {
|
|
|
|
|
|
const sizeMap: Record<string, { width: number; height: number }> = {
|
|
|
|
|
|
text: { width: 200, height: 36 },
|
|
|
|
|
|
number: { width: 150, height: 36 },
|
|
|
|
|
|
decimal: { width: 150, height: 36 },
|
|
|
|
|
|
date: { width: 180, height: 36 },
|
|
|
|
|
|
datetime: { width: 200, height: 36 },
|
|
|
|
|
|
select: { width: 200, height: 36 },
|
|
|
|
|
|
category: { width: 200, height: 36 },
|
|
|
|
|
|
code: { width: 200, height: 36 },
|
|
|
|
|
|
entity: { width: 220, height: 36 },
|
|
|
|
|
|
boolean: { width: 120, height: 36 },
|
|
|
|
|
|
checkbox: { width: 120, height: 36 },
|
|
|
|
|
|
textarea: { width: 300, height: 100 },
|
|
|
|
|
|
file: { width: 250, height: 80 },
|
|
|
|
|
|
};
|
|
|
|
|
|
return sizeMap[widgetType] || { width: 200, height: 36 };
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const componentSize = getPanelComponentSize(column.widgetType);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const newPanelComponent = {
|
|
|
|
|
|
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
|
componentType: v2Mapping.componentType,
|
|
|
|
|
|
label: column.columnLabel || column.columnName,
|
|
|
|
|
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
|
|
|
|
|
size: componentSize,
|
|
|
|
|
|
inputType: column.inputType || column.widgetType,
|
|
|
|
|
|
widgetType: column.widgetType,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...v2Mapping.componentConfig,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
tableName: column.tableName,
|
|
|
|
|
|
inputType: column.inputType || column.widgetType,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const updatedPanelConfig = {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: [...currentComponents, newPanelComponent],
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
[panelKey]: updatedPanelConfig,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)),
|
2026-01-30 16:34:05 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
toast.success(`컬럼이 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const x = e.clientX - rect.left;
|
|
|
|
|
|
const y = e.clientY - rect.top;
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
let newComponent: ComponentData;
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (type === "table") {
|
|
|
|
|
|
// 테이블 컨테이너 생성
|
|
|
|
|
|
newComponent = {
|
|
|
|
|
|
id: generateComponentId(),
|
|
|
|
|
|
type: "container",
|
2025-09-08 14:23:55 +09:00
|
|
|
|
label: table.tableLabel || table.tableName, // 테이블 라벨 우선, 없으면 테이블명
|
2025-09-02 16:18:38 +09:00
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
position: { x, y, z: 1 } as Position,
|
|
|
|
|
|
size: { width: 300, height: 200 },
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 추가 (ref 사용)
|
2025-09-02 16:46:54 +09:00
|
|
|
|
style: {
|
|
|
|
|
|
labelDisplay: true,
|
|
|
|
|
|
labelFontSize: "14px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-02 16:46:54 +09:00
|
|
|
|
labelFontWeight: "600",
|
|
|
|
|
|
labelMarginBottom: "8px",
|
|
|
|
|
|
},
|
2025-09-02 16:18:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
} else if (type === "column") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 드롭 처리:", { webType: column.widgetType, columnName: column.columnName });
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-11-10 14:41:58 +09:00
|
|
|
|
// 웹타입별 기본 너비 계산 (10px 단위 고정)
|
|
|
|
|
|
const getDefaultWidth = (widgetType: string): number => {
|
2025-10-14 16:45:30 +09:00
|
|
|
|
const widthMap: Record<string, number> = {
|
2025-11-10 14:41:58 +09:00
|
|
|
|
// 텍스트 입력 계열
|
|
|
|
|
|
text: 200,
|
|
|
|
|
|
email: 200,
|
|
|
|
|
|
tel: 150,
|
|
|
|
|
|
url: 250,
|
|
|
|
|
|
textarea: 300,
|
|
|
|
|
|
|
|
|
|
|
|
// 숫자/날짜 입력
|
|
|
|
|
|
number: 120,
|
|
|
|
|
|
decimal: 120,
|
|
|
|
|
|
date: 150,
|
|
|
|
|
|
datetime: 180,
|
|
|
|
|
|
time: 120,
|
|
|
|
|
|
|
|
|
|
|
|
// 선택 입력
|
|
|
|
|
|
select: 180,
|
|
|
|
|
|
radio: 180,
|
|
|
|
|
|
checkbox: 120,
|
|
|
|
|
|
boolean: 120,
|
|
|
|
|
|
|
|
|
|
|
|
// 코드/참조
|
|
|
|
|
|
code: 180,
|
|
|
|
|
|
entity: 200,
|
|
|
|
|
|
|
|
|
|
|
|
// 파일/이미지
|
|
|
|
|
|
file: 250,
|
|
|
|
|
|
image: 200,
|
2025-10-14 16:45:30 +09:00
|
|
|
|
|
|
|
|
|
|
// 기타
|
2025-11-10 14:41:58 +09:00
|
|
|
|
button: 100,
|
|
|
|
|
|
label: 100,
|
2025-10-14 16:45:30 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 14:41:58 +09:00
|
|
|
|
return widthMap[widgetType] || 200; // 기본값 200px
|
2025-10-14 16:45:30 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-15 13:30:11 +09:00
|
|
|
|
// 웹타입별 기본 높이 계산
|
|
|
|
|
|
const getDefaultHeight = (widgetType: string): number => {
|
|
|
|
|
|
const heightMap: Record<string, number> = {
|
|
|
|
|
|
textarea: 120, // 텍스트 영역은 3줄 (40 * 3)
|
|
|
|
|
|
checkbox: 80, // 체크박스 그룹 (40 * 2)
|
|
|
|
|
|
radio: 80, // 라디오 버튼 (40 * 2)
|
|
|
|
|
|
file: 240, // 파일 업로드 (40 * 6)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 14:33:15 +09:00
|
|
|
|
return heightMap[widgetType] || 30; // 기본값 30px로 변경
|
2025-10-15 13:30:11 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
// 웹타입별 기본 설정 생성
|
|
|
|
|
|
const getDefaultWebTypeConfig = (widgetType: string) => {
|
|
|
|
|
|
switch (widgetType) {
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "YYYY-MM-DD" as const,
|
|
|
|
|
|
showTime: false,
|
|
|
|
|
|
placeholder: "날짜를 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "datetime":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "YYYY-MM-DD HH:mm" as const,
|
|
|
|
|
|
showTime: true,
|
|
|
|
|
|
placeholder: "날짜와 시간을 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "number":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "integer" as const,
|
|
|
|
|
|
placeholder: "숫자를 입력하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "decimal":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "decimal" as const,
|
|
|
|
|
|
step: 0.01,
|
|
|
|
|
|
decimalPlaces: 2,
|
|
|
|
|
|
placeholder: "소수를 입력하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
case "dropdown":
|
|
|
|
|
|
return {
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ label: "옵션 1", value: "option1" },
|
|
|
|
|
|
{ label: "옵션 2", value: "option2" },
|
|
|
|
|
|
{ label: "옵션 3", value: "option3" },
|
|
|
|
|
|
],
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
searchable: false,
|
|
|
|
|
|
placeholder: "옵션을 선택하세요",
|
|
|
|
|
|
};
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "none" as const,
|
|
|
|
|
|
placeholder: "텍스트를 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "email" as const,
|
|
|
|
|
|
placeholder: "이메일을 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "tel":
|
|
|
|
|
|
return {
|
|
|
|
|
|
format: "phone" as const,
|
|
|
|
|
|
placeholder: "전화번호를 입력하세요",
|
|
|
|
|
|
multiline: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
|
return {
|
|
|
|
|
|
rows: 3,
|
|
|
|
|
|
placeholder: "텍스트를 입력하세요",
|
|
|
|
|
|
resizable: true,
|
|
|
|
|
|
autoResize: false,
|
|
|
|
|
|
wordWrap: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "checkbox":
|
|
|
|
|
|
case "boolean":
|
|
|
|
|
|
return {
|
|
|
|
|
|
defaultChecked: false,
|
|
|
|
|
|
labelPosition: "right" as const,
|
|
|
|
|
|
checkboxText: "",
|
|
|
|
|
|
trueValue: true,
|
|
|
|
|
|
falseValue: false,
|
|
|
|
|
|
indeterminate: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "radio":
|
|
|
|
|
|
return {
|
|
|
|
|
|
options: [
|
|
|
|
|
|
{ label: "옵션 1", value: "option1" },
|
|
|
|
|
|
{ label: "옵션 2", value: "option2" },
|
|
|
|
|
|
],
|
|
|
|
|
|
layout: "vertical" as const,
|
|
|
|
|
|
defaultValue: "",
|
|
|
|
|
|
allowNone: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
case "file":
|
|
|
|
|
|
return {
|
|
|
|
|
|
accept: "",
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
maxSize: 10,
|
|
|
|
|
|
maxFiles: 1,
|
|
|
|
|
|
preview: true,
|
|
|
|
|
|
dragDrop: true,
|
|
|
|
|
|
allowedExtensions: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
case "code":
|
|
|
|
|
|
return {
|
2025-09-15 15:38:48 +09:00
|
|
|
|
codeCategory: "", // 기본값, 실제로는 컬럼 정보에서 가져옴
|
|
|
|
|
|
placeholder: "선택하세요",
|
|
|
|
|
|
options: [], // 기본 빈 배열, 실제로는 API에서 로드
|
2025-09-03 11:32:09 +09:00
|
|
|
|
};
|
|
|
|
|
|
case "entity":
|
|
|
|
|
|
return {
|
|
|
|
|
|
entityName: "",
|
|
|
|
|
|
displayField: "name",
|
|
|
|
|
|
valueField: "id",
|
|
|
|
|
|
searchable: true,
|
|
|
|
|
|
multiple: false,
|
|
|
|
|
|
allowClear: true,
|
|
|
|
|
|
placeholder: "엔터티를 선택하세요",
|
|
|
|
|
|
apiEndpoint: "",
|
|
|
|
|
|
filters: [],
|
|
|
|
|
|
displayFormat: "simple" as const,
|
|
|
|
|
|
};
|
2025-09-25 18:54:25 +09:00
|
|
|
|
case "table":
|
|
|
|
|
|
return {
|
|
|
|
|
|
tableName: "",
|
|
|
|
|
|
displayMode: "table" as const,
|
|
|
|
|
|
showHeader: true,
|
|
|
|
|
|
showFooter: true,
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
showPageSizeSelector: true,
|
|
|
|
|
|
showPageInfo: true,
|
|
|
|
|
|
showFirstLast: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
columns: [],
|
|
|
|
|
|
searchable: true,
|
|
|
|
|
|
sortable: true,
|
|
|
|
|
|
filterable: true,
|
|
|
|
|
|
exportable: true,
|
|
|
|
|
|
};
|
2025-09-03 11:32:09 +09:00
|
|
|
|
default:
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-04 11:33:52 +09:00
|
|
|
|
// 폼 컨테이너에 드롭한 경우
|
|
|
|
|
|
if (formContainer) {
|
|
|
|
|
|
const formContainerId = formContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const formContainerComponent = layout.components.find((c) => c.id === formContainerId);
|
|
|
|
|
|
|
|
|
|
|
|
if (formContainerComponent) {
|
|
|
|
|
|
// 폼 내부에서의 상대적 위치 계산
|
|
|
|
|
|
const containerRect = formContainer.getBoundingClientRect();
|
|
|
|
|
|
const relativeX = e.clientX - containerRect.left;
|
|
|
|
|
|
const relativeY = e.clientY - containerRect.top;
|
|
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
|
// 🆕 V2 컴포넌트 매핑 사용
|
|
|
|
|
|
const v2Mapping = createV2ConfigFromColumn({
|
2025-12-19 15:44:38 +09:00
|
|
|
|
widgetType: column.widgetType,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
columnLabel: column.columnLabel,
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
inputType: column.inputType,
|
|
|
|
|
|
required: column.required,
|
|
|
|
|
|
detailSettings: column.detailSettings, // 엔티티 참조 정보 전달
|
|
|
|
|
|
// column_labels 직접 필드도 전달
|
|
|
|
|
|
referenceTable: column.referenceTable,
|
|
|
|
|
|
referenceColumn: column.referenceColumn,
|
|
|
|
|
|
displayColumn: column.displayColumn,
|
|
|
|
|
|
});
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
2025-11-10 14:41:58 +09:00
|
|
|
|
// 웹타입별 기본 너비 계산 (10px 단위 고정)
|
|
|
|
|
|
const componentWidth = getDefaultWidth(column.widgetType);
|
2025-11-10 14:21:29 +09:00
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
|
console.log("🎯 폼 컨테이너 V2 컴포넌트 생성:", {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
widgetType: column.widgetType,
|
2026-01-28 17:36:19 +09:00
|
|
|
|
v2Type: v2Mapping.componentType,
|
2025-11-10 14:21:29 +09:00
|
|
|
|
componentWidth,
|
|
|
|
|
|
});
|
2025-10-14 16:45:30 +09:00
|
|
|
|
|
2026-01-15 10:39:23 +09:00
|
|
|
|
// 엔티티 조인 컬럼인 경우 읽기 전용으로 설정
|
|
|
|
|
|
const isEntityJoinColumn = column.isEntityJoin === true;
|
|
|
|
|
|
|
2025-09-04 11:33:52 +09:00
|
|
|
|
newComponent = {
|
|
|
|
|
|
id: generateComponentId(),
|
2026-01-28 17:36:19 +09:00
|
|
|
|
type: "component", // ✅ V2 컴포넌트 시스템 사용
|
2025-09-12 14:24:25 +09:00
|
|
|
|
label: column.columnLabel || column.columnName,
|
2025-09-04 11:33:52 +09:00
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
columnName: column.columnName,
|
2026-01-15 10:39:23 +09:00
|
|
|
|
required: isEntityJoinColumn ? false : column.required, // 조인 컬럼은 필수 아님
|
|
|
|
|
|
readonly: isEntityJoinColumn, // 🆕 엔티티 조인 컬럼은 읽기 전용
|
2025-09-04 11:33:52 +09:00
|
|
|
|
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
2026-01-28 17:36:19 +09:00
|
|
|
|
componentType: v2Mapping.componentType, // v2-input, v2-select 등
|
2025-09-04 11:33:52 +09:00
|
|
|
|
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
2025-10-15 13:30:11 +09:00
|
|
|
|
size: { width: componentWidth, height: getDefaultHeight(column.widgetType) },
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 추가 (ref 사용)
|
2025-09-15 15:38:48 +09:00
|
|
|
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
|
|
|
|
|
...(column.widgetType === "code" &&
|
|
|
|
|
|
column.codeCategory && {
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
}),
|
2026-01-15 10:39:23 +09:00
|
|
|
|
// 엔티티 조인 정보 저장
|
|
|
|
|
|
...(isEntityJoinColumn && {
|
|
|
|
|
|
isEntityJoin: true,
|
|
|
|
|
|
entityJoinTable: column.entityJoinTable,
|
|
|
|
|
|
entityJoinColumn: column.entityJoinColumn,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
}),
|
2025-09-04 11:33:52 +09:00
|
|
|
|
style: {
|
2026-02-04 18:01:20 +09:00
|
|
|
|
labelDisplay: true, // 🆕 라벨 기본 표시
|
2025-09-04 11:33:52 +09:00
|
|
|
|
labelFontSize: "12px",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
labelColor: "#212121",
|
2025-09-04 11:33:52 +09:00
|
|
|
|
labelFontWeight: "500",
|
|
|
|
|
|
labelMarginBottom: "6px",
|
|
|
|
|
|
},
|
2025-09-12 14:24:25 +09:00
|
|
|
|
componentConfig: {
|
2026-01-28 17:36:19 +09:00
|
|
|
|
type: v2Mapping.componentType, // v2-input, v2-select 등
|
|
|
|
|
|
...v2Mapping.componentConfig, // V2 컴포넌트 기본 설정
|
2026-01-15 10:39:23 +09:00
|
|
|
|
...(isEntityJoinColumn && { disabled: true }), // 🆕 조인 컬럼은 비활성화
|
2025-09-12 14:24:25 +09:00
|
|
|
|
},
|
2025-09-04 11:33:52 +09:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return; // 폼 컨테이너를 찾을 수 없으면 드롭 취소
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-01-28 17:36:19 +09:00
|
|
|
|
// 일반 캔버스에 드롭한 경우 - 🆕 V2 컴포넌트 시스템 사용
|
|
|
|
|
|
const v2Mapping = createV2ConfigFromColumn({
|
2025-12-19 15:44:38 +09:00
|
|
|
|
widgetType: column.widgetType,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
columnLabel: column.columnLabel,
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
inputType: column.inputType,
|
|
|
|
|
|
required: column.required,
|
|
|
|
|
|
detailSettings: column.detailSettings, // 엔티티 참조 정보 전달
|
|
|
|
|
|
// column_labels 직접 필드도 전달
|
|
|
|
|
|
referenceTable: column.referenceTable,
|
|
|
|
|
|
referenceColumn: column.referenceColumn,
|
|
|
|
|
|
displayColumn: column.displayColumn,
|
|
|
|
|
|
});
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
2025-11-10 14:41:58 +09:00
|
|
|
|
// 웹타입별 기본 너비 계산 (10px 단위 고정)
|
|
|
|
|
|
const componentWidth = getDefaultWidth(column.widgetType);
|
2025-11-10 14:21:29 +09:00
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
|
console.log("🎯 캔버스 V2 컴포넌트 생성:", {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
widgetType: column.widgetType,
|
2026-01-28 17:36:19 +09:00
|
|
|
|
v2Type: v2Mapping.componentType,
|
2025-11-10 14:21:29 +09:00
|
|
|
|
componentWidth,
|
|
|
|
|
|
});
|
2025-10-14 16:45:30 +09:00
|
|
|
|
|
2026-01-15 10:39:23 +09:00
|
|
|
|
// 엔티티 조인 컬럼인 경우 읽기 전용으로 설정
|
|
|
|
|
|
const isEntityJoinColumn = column.isEntityJoin === true;
|
|
|
|
|
|
|
2025-09-04 11:33:52 +09:00
|
|
|
|
newComponent = {
|
|
|
|
|
|
id: generateComponentId(),
|
2026-01-28 17:36:19 +09:00
|
|
|
|
type: "component", // ✅ V2 컴포넌트 시스템 사용
|
2025-09-08 14:23:55 +09:00
|
|
|
|
label: column.columnLabel || column.columnName, // 컬럼 라벨 우선, 없으면 컬럼명
|
2025-09-04 11:33:52 +09:00
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
columnName: column.columnName,
|
2026-01-15 10:39:23 +09:00
|
|
|
|
required: isEntityJoinColumn ? false : column.required, // 조인 컬럼은 필수 아님
|
|
|
|
|
|
readonly: isEntityJoinColumn, // 🆕 엔티티 조인 컬럼은 읽기 전용
|
2026-01-28 17:36:19 +09:00
|
|
|
|
componentType: v2Mapping.componentType, // v2-input, v2-select 등
|
2025-09-04 11:33:52 +09:00
|
|
|
|
position: { x, y, z: 1 } as Position,
|
2025-10-15 13:30:11 +09:00
|
|
|
|
size: { width: componentWidth, height: getDefaultHeight(column.widgetType) },
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 추가 (ref 사용)
|
2025-09-15 15:38:48 +09:00
|
|
|
|
// 코드 타입인 경우 코드 카테고리 정보 추가
|
|
|
|
|
|
...(column.widgetType === "code" &&
|
|
|
|
|
|
column.codeCategory && {
|
|
|
|
|
|
codeCategory: column.codeCategory,
|
|
|
|
|
|
}),
|
2026-01-15 10:39:23 +09:00
|
|
|
|
// 엔티티 조인 정보 저장
|
|
|
|
|
|
...(isEntityJoinColumn && {
|
|
|
|
|
|
isEntityJoin: true,
|
|
|
|
|
|
entityJoinTable: column.entityJoinTable,
|
|
|
|
|
|
entityJoinColumn: column.entityJoinColumn,
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
}),
|
2025-09-04 11:33:52 +09:00
|
|
|
|
style: {
|
2026-02-04 18:01:20 +09:00
|
|
|
|
labelDisplay: true, // 🆕 라벨 기본 표시
|
2025-10-30 12:03:50 +09:00
|
|
|
|
labelFontSize: "14px",
|
|
|
|
|
|
labelColor: "#000000", // 순수한 검정
|
2025-09-04 11:33:52 +09:00
|
|
|
|
labelFontWeight: "500",
|
2025-10-30 12:03:50 +09:00
|
|
|
|
labelMarginBottom: "8px",
|
2025-09-04 11:33:52 +09:00
|
|
|
|
},
|
2025-09-12 14:24:25 +09:00
|
|
|
|
componentConfig: {
|
2026-01-28 17:36:19 +09:00
|
|
|
|
type: v2Mapping.componentType, // v2-input, v2-select 등
|
|
|
|
|
|
...v2Mapping.componentConfig, // V2 컴포넌트 기본 설정
|
2026-01-15 10:39:23 +09:00
|
|
|
|
...(isEntityJoinColumn && { disabled: true }), // 🆕 조인 컬럼은 비활성화
|
2025-09-12 14:24:25 +09:00
|
|
|
|
},
|
2025-09-04 11:33:52 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-11-10 15:09:27 +09:00
|
|
|
|
// 10px 단위 스냅 적용 (그룹 컴포넌트 제외)
|
2025-09-04 17:01:07 +09:00
|
|
|
|
if (layout.gridSettings?.snapToGrid && newComponent.type !== "group") {
|
2025-11-10 15:09:27 +09:00
|
|
|
|
newComponent.position = snapPositionTo10px(newComponent.position);
|
|
|
|
|
|
newComponent.size = snapSizeTo10px(newComponent.size);
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
2025-11-10 15:09:27 +09:00
|
|
|
|
console.log("🧲 새 컴포넌트 10px 스냅 적용:", {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
type: newComponent.type,
|
|
|
|
|
|
snappedPosition: newComponent.position,
|
|
|
|
|
|
snappedSize: newComponent.size,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: [...layout.components, newComponent],
|
|
|
|
|
|
};
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
setSelectedComponent(newComponent);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 🔧 테이블 패널 유지를 위해 자동 속성 패널 열기 비활성화
|
|
|
|
|
|
// openPanel("properties");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("드롭 처리 실패:", error);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-10 14:46:30 +09:00
|
|
|
|
[layout, saveToHistory],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// 파일 컴포넌트 업데이트 처리
|
|
|
|
|
|
const handleFileComponentUpdate = useCallback(
|
|
|
|
|
|
(updates: Partial<ComponentData>) => {
|
|
|
|
|
|
if (!selectedFileComponent) return;
|
|
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const updatedComponents = layout.components.map((comp) =>
|
|
|
|
|
|
comp.id === selectedFileComponent.id ? { ...comp, ...updates } : comp,
|
2025-09-26 13:11:34 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = { ...layout, components: updatedComponents };
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-10-13 18:28:03 +09:00
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// selectedFileComponent도 업데이트
|
2025-10-13 18:28:03 +09:00
|
|
|
|
setSelectedFileComponent((prev) => (prev ? { ...prev, ...updates } : null));
|
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
|
// selectedComponent가 같은 컴포넌트라면 업데이트
|
|
|
|
|
|
if (selectedComponent?.id === selectedFileComponent.id) {
|
2025-10-13 18:28:03 +09:00
|
|
|
|
setSelectedComponent((prev) => (prev ? { ...prev, ...updates } : null));
|
2025-09-26 13:11:34 +09:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[selectedFileComponent, layout, saveToHistory, selectedComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 파일첨부 모달 닫기
|
|
|
|
|
|
const handleFileAttachmentModalClose = useCallback(() => {
|
|
|
|
|
|
setShowFileAttachmentModal(false);
|
|
|
|
|
|
setSelectedFileComponent(null);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 더블클릭 처리
|
2025-10-13 18:28:03 +09:00
|
|
|
|
const handleComponentDoubleClick = useCallback((component: ComponentData, event?: React.MouseEvent) => {
|
|
|
|
|
|
event?.stopPropagation();
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
// 파일 컴포넌트인 경우 상세 모달 열기
|
|
|
|
|
|
if (component.type === "file") {
|
|
|
|
|
|
setSelectedFileComponent(component);
|
|
|
|
|
|
setShowFileAttachmentModal(true);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
2025-10-13 18:28:03 +09:00
|
|
|
|
// 다른 컴포넌트 타입의 더블클릭 처리는 여기에 추가
|
|
|
|
|
|
// console.log("더블클릭된 컴포넌트:", component.type, component.id);
|
|
|
|
|
|
}, []);
|
2025-09-26 13:11:34 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 컴포넌트 클릭 처리 (다중선택 지원)
|
|
|
|
|
|
const handleComponentClick = useCallback(
|
|
|
|
|
|
(component: ComponentData, event?: React.MouseEvent) => {
|
|
|
|
|
|
event?.stopPropagation();
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그가 끝난 직후라면 클릭을 무시 (다중 선택 유지)
|
|
|
|
|
|
if (dragState.justFinishedDrag) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// 🔧 layout.components에서 최신 버전의 컴포넌트 찾기
|
|
|
|
|
|
const latestComponent = layout.components.find((c) => c.id === component.id);
|
|
|
|
|
|
if (!latestComponent) {
|
|
|
|
|
|
console.warn("⚠️ 컴포넌트를 찾을 수 없습니다:", component.id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const isShiftPressed = event?.shiftKey || false;
|
|
|
|
|
|
const isCtrlPressed = event?.ctrlKey || event?.metaKey || false;
|
2025-10-21 17:32:54 +09:00
|
|
|
|
const isGroupContainer = latestComponent.type === "group";
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (isShiftPressed || isCtrlPressed || groupState.isGrouping) {
|
|
|
|
|
|
// 다중 선택 모드
|
|
|
|
|
|
if (isGroupContainer) {
|
|
|
|
|
|
// 그룹 컨테이너는 단일 선택으로 처리
|
2025-10-21 17:32:54 +09:00
|
|
|
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
2025-10-21 17:32:54 +09:00
|
|
|
|
selectedComponents: [latestComponent.id],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
isGrouping: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-10-21 17:32:54 +09:00
|
|
|
|
const isSelected = groupState.selectedComponents.includes(latestComponent.id);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
selectedComponents: isSelected
|
2025-10-21 17:32:54 +09:00
|
|
|
|
? prev.selectedComponents.filter((id) => id !== latestComponent.id)
|
|
|
|
|
|
: [...prev.selectedComponents, latestComponent.id],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}));
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 마지막 선택된 컴포넌트를 selectedComponent로 설정
|
|
|
|
|
|
if (!isSelected) {
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// console.log("🎯 컴포넌트 선택 (다중 모드):", latestComponent.id);
|
|
|
|
|
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 단일 선택 모드
|
2025-10-21 17:32:54 +09:00
|
|
|
|
// console.log("🎯 컴포넌트 선택 (단일 모드):", latestComponent.id);
|
|
|
|
|
|
handleComponentSelect(latestComponent); // 🔧 최신 버전 사용
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
2025-10-21 17:32:54 +09:00
|
|
|
|
selectedComponents: [latestComponent.id],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-10-22 14:54:50 +09:00
|
|
|
|
[
|
|
|
|
|
|
handleComponentSelect,
|
|
|
|
|
|
groupState.isGrouping,
|
|
|
|
|
|
groupState.selectedComponents,
|
|
|
|
|
|
dragState.justFinishedDrag,
|
|
|
|
|
|
layout.components,
|
|
|
|
|
|
],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 컴포넌트 드래그 시작
|
|
|
|
|
|
const startComponentDrag = useCallback(
|
2025-11-07 11:36:58 +09:00
|
|
|
|
(component: ComponentData, event: React.MouseEvent | React.DragEvent) => {
|
2025-09-02 16:18:38 +09:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 새로운 드래그 시작 시 justFinishedDrag 플래그 해제
|
|
|
|
|
|
if (dragState.justFinishedDrag) {
|
|
|
|
|
|
setDragState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
justFinishedDrag: false,
|
|
|
|
|
|
}));
|
2025-09-01 17:57:52 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
|
|
|
|
|
// 캔버스가 scale() 변환되어 있기 때문에 마우스 위치도 역변환 필요
|
|
|
|
|
|
const relativeMouseX = (event.clientX - rect.left) / zoomLevel;
|
|
|
|
|
|
const relativeMouseY = (event.clientY - rect.top) / zoomLevel;
|
2025-09-09 18:02:07 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 다중 선택된 컴포넌트들 확인
|
|
|
|
|
|
const isDraggedComponentSelected = groupState.selectedComponents.includes(component.id);
|
2025-09-11 12:22:39 +09:00
|
|
|
|
let componentsToMove = isDraggedComponentSelected
|
2025-09-02 16:18:38 +09:00
|
|
|
|
? layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id))
|
|
|
|
|
|
: [component];
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-11 12:22:39 +09:00
|
|
|
|
// 레이아웃 컴포넌트인 경우 존에 속한 컴포넌트들도 함께 이동
|
|
|
|
|
|
if (component.type === "layout") {
|
|
|
|
|
|
const zoneComponents = layout.components.filter((comp) => comp.parentId === component.id && comp.zoneId);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🏗️ 레이아웃 드래그 - 존 컴포넌트들 포함:", {
|
|
|
|
|
|
layoutId: component.id,
|
|
|
|
|
|
zoneComponentsCount: zoneComponents.length,
|
|
|
|
|
|
zoneComponents: zoneComponents.map((c) => ({ id: c.id, zoneId: c.zoneId })),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 중복 제거하여 추가
|
|
|
|
|
|
const allComponentIds = new Set(componentsToMove.map((c) => c.id));
|
|
|
|
|
|
const additionalComponents = zoneComponents.filter((c) => !allComponentIds.has(c.id));
|
|
|
|
|
|
componentsToMove = [...componentsToMove, ...additionalComponents];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setDragState({
|
|
|
|
|
|
isDragging: true,
|
|
|
|
|
|
draggedComponent: component, // 주 드래그 컴포넌트 (마우스 위치 기준)
|
|
|
|
|
|
draggedComponents: componentsToMove, // 함께 이동할 모든 컴포넌트들
|
|
|
|
|
|
originalPosition: {
|
|
|
|
|
|
x: component.position.x,
|
|
|
|
|
|
y: component.position.y,
|
|
|
|
|
|
z: (component.position as Position).z || 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
currentPosition: {
|
|
|
|
|
|
x: component.position.x,
|
|
|
|
|
|
y: component.position.y,
|
|
|
|
|
|
z: (component.position as Position).z || 1,
|
|
|
|
|
|
},
|
2025-11-10 14:21:29 +09:00
|
|
|
|
grabOffset: {
|
|
|
|
|
|
x: relativeMouseX - component.position.x,
|
|
|
|
|
|
y: relativeMouseY - component.position.y,
|
|
|
|
|
|
},
|
2025-09-02 16:18:38 +09:00
|
|
|
|
justFinishedDrag: false,
|
2025-09-01 17:57:52 +09:00
|
|
|
|
});
|
|
|
|
|
|
},
|
2025-11-07 11:36:58 +09:00
|
|
|
|
[groupState.selectedComponents, layout.components, dragState.justFinishedDrag, zoomLevel],
|
2025-09-01 17:57:52 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그 중 위치 업데이트 (성능 최적화 + 실시간 업데이트)
|
|
|
|
|
|
const updateDragPosition = useCallback(
|
|
|
|
|
|
(event: MouseEvent) => {
|
|
|
|
|
|
if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const rect = canvasRef.current.getBoundingClientRect();
|
2025-09-09 18:02:07 +09:00
|
|
|
|
|
2025-11-07 11:36:58 +09:00
|
|
|
|
// 🔥 중요: 줌 레벨을 고려한 마우스 위치 계산
|
|
|
|
|
|
// 캔버스가 scale() 변환되어 있기 때문에 마우스 위치도 역변환 필요
|
|
|
|
|
|
const relativeMouseX = (event.clientX - rect.left) / zoomLevel;
|
|
|
|
|
|
const relativeMouseY = (event.clientY - rect.top) / zoomLevel;
|
2025-09-09 18:02:07 +09:00
|
|
|
|
|
2025-10-23 15:14:45 +09:00
|
|
|
|
// 컴포넌트 크기 가져오기
|
|
|
|
|
|
const draggedComp = layout.components.find((c) => c.id === dragState.draggedComponent.id);
|
|
|
|
|
|
const componentWidth = draggedComp?.size?.width || 100;
|
|
|
|
|
|
const componentHeight = draggedComp?.size?.height || 40;
|
|
|
|
|
|
|
|
|
|
|
|
// 경계 제한 적용
|
|
|
|
|
|
const rawX = relativeMouseX - dragState.grabOffset.x;
|
|
|
|
|
|
const rawY = relativeMouseY - dragState.grabOffset.y;
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const newPosition = {
|
2025-11-10 14:21:29 +09:00
|
|
|
|
x: Math.max(0, Math.min(rawX, screenResolution.width - componentWidth)),
|
|
|
|
|
|
y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)),
|
2025-09-02 16:18:38 +09:00
|
|
|
|
z: (dragState.draggedComponent.position as Position).z || 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 드래그 상태 업데이트
|
2025-09-10 09:50:20 +09:00
|
|
|
|
setDragState((prev) => {
|
|
|
|
|
|
const newState = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
currentPosition: { ...newPosition }, // 새로운 객체 생성
|
|
|
|
|
|
};
|
|
|
|
|
|
return newState;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 성능 최적화: 드래그 중에는 상태 업데이트만 하고,
|
|
|
|
|
|
// 실제 레이아웃 업데이트는 endDrag에서 처리
|
|
|
|
|
|
// 속성 패널에서는 dragState.currentPosition을 참조하여 실시간 표시
|
2025-09-01 14:00:31 +09:00
|
|
|
|
},
|
2025-11-07 11:36:58 +09:00
|
|
|
|
[dragState.isDragging, dragState.draggedComponent, dragState.grabOffset, zoomLevel],
|
2025-09-01 14:00:31 +09:00
|
|
|
|
);
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 드래그 종료
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const endDrag = useCallback(
|
|
|
|
|
|
(mouseEvent?: MouseEvent) => {
|
|
|
|
|
|
if (dragState.isDragging && dragState.draggedComponent) {
|
|
|
|
|
|
// 🎯 탭 컨테이너로의 드롭 처리 (기존 컴포넌트 이동, 중첩 구조 지원)
|
|
|
|
|
|
if (mouseEvent) {
|
|
|
|
|
|
const dropTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY) as HTMLElement;
|
|
|
|
|
|
const tabsContainer = dropTarget?.closest('[data-tabs-container="true"]');
|
|
|
|
|
|
|
|
|
|
|
|
if (tabsContainer) {
|
|
|
|
|
|
const containerId = tabsContainer.getAttribute("data-component-id");
|
|
|
|
|
|
const activeTabId = tabsContainer.getAttribute("data-active-tab-id");
|
|
|
|
|
|
|
|
|
|
|
|
if (containerId && activeTabId) {
|
|
|
|
|
|
// 1. 먼저 최상위 레이아웃에서 탭 컴포넌트 찾기
|
|
|
|
|
|
let targetComponent = layout.components.find((c) => c.id === containerId);
|
|
|
|
|
|
let parentSplitPanelId: string | null = null;
|
|
|
|
|
|
let parentPanelSide: "left" | "right" | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 최상위에 없으면 분할 패널 안에서 중첩된 탭 찾기
|
|
|
|
|
|
if (!targetComponent) {
|
|
|
|
|
|
for (const comp of layout.components) {
|
|
|
|
|
|
const compType = (comp as any)?.componentType;
|
|
|
|
|
|
if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") {
|
|
|
|
|
|
const config = (comp as any).componentConfig || {};
|
|
|
|
|
|
|
|
|
|
|
|
// 좌측 패널에서 찾기
|
|
|
|
|
|
const leftComponents = config.leftPanel?.components || [];
|
|
|
|
|
|
const foundInLeft = leftComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInLeft) {
|
|
|
|
|
|
targetComponent = foundInLeft;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "left";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 우측 패널에서 찾기
|
|
|
|
|
|
const rightComponents = config.rightPanel?.components || [];
|
|
|
|
|
|
const foundInRight = rightComponents.find((c: any) => c.id === containerId);
|
|
|
|
|
|
if (foundInRight) {
|
|
|
|
|
|
targetComponent = foundInRight;
|
|
|
|
|
|
parentSplitPanelId = comp.id;
|
|
|
|
|
|
parentPanelSide = "right";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-02-02 17:11:00 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
const compType = (targetComponent as any)?.componentType;
|
|
|
|
|
|
|
|
|
|
|
|
// 자기 자신을 자신에게 드롭하는 것 방지
|
|
|
|
|
|
if (
|
|
|
|
|
|
targetComponent &&
|
2026-01-26 11:04:39 +09:00
|
|
|
|
(compType === "tabs-widget" || compType === "v2-tabs-widget") &&
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
dragState.draggedComponent !== containerId
|
|
|
|
|
|
) {
|
|
|
|
|
|
const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent);
|
|
|
|
|
|
if (draggedComponent) {
|
|
|
|
|
|
const currentConfig = (targetComponent as any).componentConfig || {};
|
|
|
|
|
|
const tabs = currentConfig.tabs || [];
|
|
|
|
|
|
|
|
|
|
|
|
// 탭 컨텐츠 영역 기준 드롭 위치 계산
|
|
|
|
|
|
const tabContentRect = tabsContainer.getBoundingClientRect();
|
|
|
|
|
|
const dropX = (mouseEvent.clientX - tabContentRect.left) / zoomLevel;
|
|
|
|
|
|
const dropY = (mouseEvent.clientY - tabContentRect.top) / zoomLevel;
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 컴포넌트를 탭 내부 컴포넌트로 변환
|
|
|
|
|
|
const newTabComponent = {
|
|
|
|
|
|
id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
|
componentType: (draggedComponent as any).componentType || draggedComponent.type,
|
|
|
|
|
|
label: (draggedComponent as any).label || "컴포넌트",
|
|
|
|
|
|
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
|
|
|
|
|
size: draggedComponent.size || { width: 200, height: 100 },
|
|
|
|
|
|
componentConfig: (draggedComponent as any).componentConfig || {},
|
|
|
|
|
|
style: (draggedComponent as any).style || {},
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
// 해당 탭에 컴포넌트 추가
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === activeTabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: [...(tab.components || []), newTabComponent],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const updatedTabsComponent = {
|
|
|
|
|
|
...targetComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
tabs: updatedTabs,
|
|
|
|
|
|
},
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
let newLayout;
|
|
|
|
|
|
|
|
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: layout.components
|
|
|
|
|
|
.filter((c) => c.id !== dragState.draggedComponent)
|
|
|
|
|
|
.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
|
|
|
|
|
pc.id === containerId ? updatedTabsComponent : pc,
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success("컴포넌트가 중첩된 탭으로 이동되었습니다");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: layout.components
|
|
|
|
|
|
.filter((c) => c.id !== dragState.draggedComponent)
|
|
|
|
|
|
.map((c) => {
|
|
|
|
|
|
if (c.id === containerId) {
|
|
|
|
|
|
return updatedTabsComponent;
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
toast.success("컴포넌트가 탭으로 이동되었습니다");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 드래그 상태 초기화 후 종료
|
|
|
|
|
|
setDragState({
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
draggedComponent: null,
|
|
|
|
|
|
draggedComponents: [],
|
|
|
|
|
|
originalPosition: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
currentPosition: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
grabOffset: { x: 0, y: 0 },
|
|
|
|
|
|
justFinishedDrag: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, justFinishedDrag: false }));
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
return; // 탭으로 이동 완료, 일반 드래그 종료 로직 스킵
|
2026-02-02 17:11:00 +09:00
|
|
|
|
}
|
2026-01-26 11:04:39 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 주 드래그 컴포넌트의 최종 위치 계산
|
|
|
|
|
|
const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent);
|
|
|
|
|
|
let finalPosition = dragState.currentPosition;
|
2025-09-04 17:01:07 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 현재 해상도에 맞는 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo = layout.gridSettings
|
|
|
|
|
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
})
|
|
|
|
|
|
: null;
|
2025-09-04 17:01:07 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 일반 컴포넌트 및 플로우 버튼 그룹에 격자 스냅 적용 (일반 그룹 제외)
|
|
|
|
|
|
if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) {
|
|
|
|
|
|
finalPosition = snapPositionTo10px(
|
|
|
|
|
|
{
|
|
|
|
|
|
x: dragState.currentPosition.x,
|
|
|
|
|
|
y: dragState.currentPosition.y,
|
|
|
|
|
|
z: dragState.currentPosition.z ?? 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
currentGridInfo,
|
|
|
|
|
|
{
|
|
|
|
|
|
columns: layout.gridSettings.columns,
|
|
|
|
|
|
gap: layout.gridSettings.gap,
|
|
|
|
|
|
padding: layout.gridSettings.padding,
|
|
|
|
|
|
snapToGrid: layout.gridSettings.snapToGrid || false,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 스냅으로 인한 추가 이동 거리 계산
|
|
|
|
|
|
const snapDeltaX = finalPosition.x - dragState.currentPosition.x;
|
|
|
|
|
|
const snapDeltaY = finalPosition.y - dragState.currentPosition.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 원래 이동 거리 + 스냅 조정 거리
|
|
|
|
|
|
const totalDeltaX = dragState.currentPosition.x - dragState.originalPosition.x + snapDeltaX;
|
|
|
|
|
|
const totalDeltaY = dragState.currentPosition.y - dragState.originalPosition.y + snapDeltaY;
|
|
|
|
|
|
|
|
|
|
|
|
// 다중 컴포넌트들의 최종 위치 업데이트
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp) => {
|
|
|
|
|
|
const isDraggedComponent = dragState.draggedComponents.some((dragComp) => dragComp.id === comp.id);
|
|
|
|
|
|
if (isDraggedComponent) {
|
|
|
|
|
|
const originalComponent = dragState.draggedComponents.find((dragComp) => dragComp.id === comp.id)!;
|
|
|
|
|
|
let newPosition = {
|
|
|
|
|
|
x: originalComponent.position.x + totalDeltaX,
|
|
|
|
|
|
y: originalComponent.position.y + totalDeltaY,
|
|
|
|
|
|
z: originalComponent.position.z || 1,
|
|
|
|
|
|
};
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 캔버스 경계 제한 (컴포넌트가 화면 밖으로 나가지 않도록)
|
|
|
|
|
|
const componentWidth = comp.size?.width || 100;
|
|
|
|
|
|
const componentHeight = comp.size?.height || 40;
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 최소 위치: 0, 최대 위치: 캔버스 크기 - 컴포넌트 크기
|
|
|
|
|
|
newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth));
|
|
|
|
|
|
newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight));
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용
|
|
|
|
|
|
if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) {
|
|
|
|
|
|
const { columnWidth } = gridInfo;
|
|
|
|
|
|
const { gap } = layout.gridSettings;
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 그룹 내부 패딩 고려한 격자 정렬
|
|
|
|
|
|
const padding = 16;
|
|
|
|
|
|
const effectiveX = newPosition.x - padding;
|
|
|
|
|
|
const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16)));
|
|
|
|
|
|
const snappedX = padding + columnIndex * (columnWidth + (gap || 16));
|
|
|
|
|
|
|
|
|
|
|
|
// Y 좌표는 20px 단위로 스냅
|
|
|
|
|
|
const effectiveY = newPosition.y - padding;
|
|
|
|
|
|
const rowIndex = Math.round(effectiveY / 20);
|
|
|
|
|
|
const snappedY = padding + rowIndex * 20;
|
|
|
|
|
|
|
|
|
|
|
|
// 크기도 외부 격자와 동일하게 스냅
|
|
|
|
|
|
const fullColumnWidth = columnWidth + (gap || 16); // 외부 격자와 동일한 크기
|
|
|
|
|
|
const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth));
|
|
|
|
|
|
const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap 제거하여 실제 컴포넌트 크기
|
|
|
|
|
|
// 높이는 사용자가 입력한 값 그대로 사용 (스냅 제거)
|
|
|
|
|
|
const snappedHeight = Math.max(40, comp.size.height);
|
|
|
|
|
|
|
|
|
|
|
|
newPosition = {
|
|
|
|
|
|
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
|
|
|
|
|
|
y: Math.max(padding, snappedY),
|
|
|
|
|
|
z: newPosition.z,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 크기도 업데이트
|
|
|
|
|
|
const newSize = {
|
|
|
|
|
|
width: snappedWidth,
|
|
|
|
|
|
height: snappedHeight,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
position: newPosition as Position,
|
|
|
|
|
|
size: newSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
position: newPosition as Position,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
return comp;
|
|
|
|
|
|
});
|
2025-09-03 11:32:09 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const newLayout = { ...layout, components: updatedComponents };
|
|
|
|
|
|
setLayout(newLayout);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 선택된 컴포넌트도 업데이트 (PropertiesPanel 동기화용)
|
|
|
|
|
|
if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) {
|
|
|
|
|
|
const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id);
|
|
|
|
|
|
if (updatedSelectedComponent) {
|
|
|
|
|
|
console.log("🔄 ScreenDesigner: 선택된 컴포넌트 위치 업데이트", {
|
|
|
|
|
|
componentId: selectedComponent.id,
|
|
|
|
|
|
oldPosition: selectedComponent.position,
|
|
|
|
|
|
newPosition: updatedSelectedComponent.position,
|
|
|
|
|
|
});
|
|
|
|
|
|
setSelectedComponent(updatedSelectedComponent);
|
|
|
|
|
|
}
|
2025-09-10 09:50:20 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 히스토리에 저장
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
setDragState({
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
draggedComponent: null,
|
|
|
|
|
|
draggedComponents: [],
|
|
|
|
|
|
originalPosition: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
currentPosition: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
grabOffset: { x: 0, y: 0 },
|
|
|
|
|
|
justFinishedDrag: true,
|
|
|
|
|
|
});
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
// 짧은 시간 후 justFinishedDrag 플래그 해제
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setDragState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
justFinishedDrag: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
},
|
|
|
|
|
|
[dragState, layout, saveToHistory],
|
|
|
|
|
|
);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
|
|
// 드래그 선택 시작
|
|
|
|
|
|
const startSelectionDrag = useCallback(
|
|
|
|
|
|
(event: React.MouseEvent) => {
|
|
|
|
|
|
if (dragState.isDragging) return; // 컴포넌트 드래그 중이면 무시
|
|
|
|
|
|
|
|
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!rect) return;
|
|
|
|
|
|
|
2025-10-24 10:40:12 +09:00
|
|
|
|
// zoom 스케일을 고려한 좌표 변환
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const startPoint = {
|
2025-10-24 10:40:12 +09:00
|
|
|
|
x: (event.clientX - rect.left) / zoomLevel,
|
|
|
|
|
|
y: (event.clientY - rect.top) / zoomLevel,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
z: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setSelectionDrag({
|
|
|
|
|
|
isSelecting: true,
|
|
|
|
|
|
startPoint,
|
|
|
|
|
|
currentPoint: startPoint,
|
|
|
|
|
|
wasSelecting: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2025-10-24 10:40:12 +09:00
|
|
|
|
[dragState.isDragging, zoomLevel],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 드래그 선택 업데이트
|
|
|
|
|
|
const updateSelectionDrag = useCallback(
|
|
|
|
|
|
(event: MouseEvent) => {
|
|
|
|
|
|
if (!selectionDrag.isSelecting || !canvasRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const rect = canvasRef.current.getBoundingClientRect();
|
2025-10-24 10:40:12 +09:00
|
|
|
|
// zoom 스케일을 고려한 좌표 변환
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const currentPoint = {
|
2025-10-24 10:40:12 +09:00
|
|
|
|
x: (event.clientX - rect.left) / zoomLevel,
|
|
|
|
|
|
y: (event.clientY - rect.top) / zoomLevel,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
z: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setSelectionDrag((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
currentPoint,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 선택 영역 내의 컴포넌트들 찾기
|
|
|
|
|
|
const selectionRect = {
|
|
|
|
|
|
left: Math.min(selectionDrag.startPoint.x, currentPoint.x),
|
|
|
|
|
|
top: Math.min(selectionDrag.startPoint.y, currentPoint.y),
|
|
|
|
|
|
right: Math.max(selectionDrag.startPoint.x, currentPoint.x),
|
|
|
|
|
|
bottom: Math.max(selectionDrag.startPoint.y, currentPoint.y),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-06 09:51:29 +09:00
|
|
|
|
// 🆕 visibleComponents만 선택 대상으로 (현재 활성 레이어의 컴포넌트만)
|
|
|
|
|
|
const selectedIds = visibleComponents
|
2025-09-02 16:18:38 +09:00
|
|
|
|
.filter((comp) => {
|
|
|
|
|
|
const compRect = {
|
|
|
|
|
|
left: comp.position.x,
|
|
|
|
|
|
top: comp.position.y,
|
|
|
|
|
|
right: comp.position.x + comp.size.width,
|
|
|
|
|
|
bottom: comp.position.y + comp.size.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
compRect.left < selectionRect.right &&
|
|
|
|
|
|
compRect.right > selectionRect.left &&
|
|
|
|
|
|
compRect.top < selectionRect.bottom &&
|
|
|
|
|
|
compRect.bottom > selectionRect.top
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((comp) => comp.id);
|
|
|
|
|
|
|
|
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
selectedComponents: selectedIds,
|
|
|
|
|
|
}));
|
|
|
|
|
|
},
|
2026-02-06 09:51:29 +09:00
|
|
|
|
[selectionDrag.isSelecting, selectionDrag.startPoint, visibleComponents, zoomLevel],
|
2025-09-02 16:18:38 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 드래그 선택 종료
|
|
|
|
|
|
const endSelectionDrag = useCallback(() => {
|
|
|
|
|
|
// 최소 드래그 거리 확인 (5픽셀)
|
|
|
|
|
|
const minDragDistance = 5;
|
|
|
|
|
|
const dragDistance = Math.sqrt(
|
|
|
|
|
|
Math.pow(selectionDrag.currentPoint.x - selectionDrag.startPoint.x, 2) +
|
|
|
|
|
|
Math.pow(selectionDrag.currentPoint.y - selectionDrag.startPoint.y, 2),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const wasActualDrag = dragDistance > minDragDistance;
|
|
|
|
|
|
|
|
|
|
|
|
setSelectionDrag({
|
|
|
|
|
|
isSelecting: false,
|
|
|
|
|
|
startPoint: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
currentPoint: { x: 0, y: 0, z: 1 },
|
|
|
|
|
|
wasSelecting: wasActualDrag, // 실제 드래그였을 때만 클릭 이벤트 무시
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 짧은 시간 후 wasSelecting을 false로 리셋
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setSelectionDrag((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
wasSelecting: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}, [selectionDrag.currentPoint, selectionDrag.startPoint]);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 삭제 (단일/다중 선택 지원)
|
|
|
|
|
|
const deleteComponent = useCallback(() => {
|
|
|
|
|
|
// 다중 선택된 컴포넌트가 있는 경우
|
|
|
|
|
|
if (groupState.selectedComponents.length > 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ 다중 컴포넌트 삭제:", groupState.selectedComponents.length, "개");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
|
|
let newComponents = [...layout.components];
|
|
|
|
|
|
|
|
|
|
|
|
// 각 선택된 컴포넌트를 삭제 처리
|
|
|
|
|
|
groupState.selectedComponents.forEach((componentId) => {
|
|
|
|
|
|
const component = layout.components.find((comp) => comp.id === componentId);
|
|
|
|
|
|
if (!component) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (component.type === "group") {
|
|
|
|
|
|
// 그룹 삭제 시: 자식 컴포넌트들의 절대 위치 복원
|
|
|
|
|
|
const childComponents = newComponents.filter((comp) => comp.parentId === component.id);
|
|
|
|
|
|
const restoredChildren = restoreAbsolutePositions(childComponents, component.position);
|
|
|
|
|
|
|
|
|
|
|
|
newComponents = newComponents
|
|
|
|
|
|
.map((comp) => {
|
|
|
|
|
|
if (comp.parentId === component.id) {
|
|
|
|
|
|
// 복원된 절대 위치로 업데이트
|
|
|
|
|
|
const restoredChild = restoredChildren.find((restored) => restored.id === comp.id);
|
|
|
|
|
|
return restoredChild || { ...comp, parentId: undefined };
|
2025-09-02 10:33:41 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
return comp;
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((comp) => comp.id !== component.id); // 그룹 컴포넌트 제거
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 컴포넌트 삭제
|
|
|
|
|
|
newComponents = newComponents.filter((comp) => comp.id !== component.id);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
});
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const newLayout = { ...layout, components: newComponents };
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-09-01 17:57:52 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 선택 상태 초기화
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: [] }));
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${groupState.selectedComponents.length}개 컴포넌트가 삭제되었습니다.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 단일 선택된 컴포넌트 삭제
|
|
|
|
|
|
if (!selectedComponent) return;
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ 단일 컴포넌트 삭제:", selectedComponent.id);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
|
|
let newComponents;
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedComponent.type === "group") {
|
|
|
|
|
|
// 그룹 삭제 시: 자식 컴포넌트들의 절대 위치 복원 후 그룹 삭제
|
|
|
|
|
|
const childComponents = layout.components.filter((comp) => comp.parentId === selectedComponent.id);
|
|
|
|
|
|
const restoredChildren = restoreAbsolutePositions(childComponents, selectedComponent.position);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
newComponents = layout.components
|
|
|
|
|
|
.map((comp) => {
|
|
|
|
|
|
if (comp.parentId === selectedComponent.id) {
|
|
|
|
|
|
// 복원된 절대 위치로 업데이트
|
|
|
|
|
|
const restoredChild = restoredChildren.find((restored) => restored.id === comp.id);
|
|
|
|
|
|
return restoredChild || { ...comp, parentId: undefined };
|
2025-09-01 11:48:12 +09:00
|
|
|
|
}
|
2025-09-01 15:22:47 +09:00
|
|
|
|
return comp;
|
2025-09-02 16:18:38 +09:00
|
|
|
|
})
|
|
|
|
|
|
.filter((comp) => comp.id !== selectedComponent.id); // 그룹 컴포넌트 제거
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 컴포넌트 삭제
|
|
|
|
|
|
newComponents = layout.components.filter((comp) => comp.id !== selectedComponent.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = { ...layout, components: newComponents };
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
toast.success("컴포넌트가 삭제되었습니다.");
|
|
|
|
|
|
}, [selectedComponent, groupState.selectedComponents, layout, saveToHistory]);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 복사
|
|
|
|
|
|
const copyComponent = useCallback(() => {
|
|
|
|
|
|
if (groupState.selectedComponents.length > 0) {
|
|
|
|
|
|
// 다중 선택된 컴포넌트들 복사
|
|
|
|
|
|
const componentsToCopy = layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id));
|
|
|
|
|
|
setClipboard(componentsToCopy);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("다중 컴포넌트 복사:", componentsToCopy.length, "개");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.success(`${componentsToCopy.length}개 컴포넌트가 복사되었습니다.`);
|
|
|
|
|
|
} else if (selectedComponent) {
|
|
|
|
|
|
// 단일 컴포넌트 복사
|
|
|
|
|
|
setClipboard([selectedComponent]);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("단일 컴포넌트 복사:", selectedComponent.id);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.success("컴포넌트가 복사되었습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedComponent, groupState.selectedComponents, layout.components]);
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 붙여넣기
|
|
|
|
|
|
const pasteComponent = useCallback(() => {
|
|
|
|
|
|
if (clipboard.length === 0) {
|
|
|
|
|
|
toast.warning("복사된 컴포넌트가 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newComponents: ComponentData[] = [];
|
|
|
|
|
|
const offset = 20; // 붙여넣기 시 위치 오프셋
|
|
|
|
|
|
|
|
|
|
|
|
clipboard.forEach((clipComponent, index) => {
|
|
|
|
|
|
const newComponent: ComponentData = {
|
|
|
|
|
|
...clipComponent,
|
|
|
|
|
|
id: generateComponentId(),
|
|
|
|
|
|
position: {
|
|
|
|
|
|
x: clipComponent.position.x + offset + index * 10,
|
|
|
|
|
|
y: clipComponent.position.y + offset + index * 10,
|
|
|
|
|
|
z: clipComponent.position.z || 1,
|
|
|
|
|
|
} as Position,
|
|
|
|
|
|
parentId: undefined, // 붙여넣기 시 부모 관계 해제
|
2026-02-09 13:21:56 +09:00
|
|
|
|
layerId: activeLayerIdRef.current || 1, // 🆕 현재 활성 레이어에 붙여넣기 (ref 사용)
|
2025-09-01 15:22:47 +09:00
|
|
|
|
};
|
2025-09-02 16:18:38 +09:00
|
|
|
|
newComponents.push(newComponent);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: [...layout.components, ...newComponents],
|
|
|
|
|
|
};
|
2025-09-01 14:00:31 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 붙여넣은 컴포넌트들을 선택 상태로 만들기
|
|
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
selectedComponents: newComponents.map((comp) => comp.id),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("컴포넌트 붙여넣기 완료:", newComponents.length, "개");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.success(`${newComponents.length}개 컴포넌트가 붙여넣어졌습니다.`);
|
2026-02-09 13:21:56 +09:00
|
|
|
|
}, [clipboard, layout, saveToHistory]);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
2025-10-24 10:37:02 +09:00
|
|
|
|
// 🆕 플로우 버튼 그룹 생성 (다중 선택된 버튼들을 한 번에 그룹으로)
|
|
|
|
|
|
// 🆕 플로우 버튼 그룹 다이얼로그 상태
|
|
|
|
|
|
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const handleFlowButtonGroup = useCallback(() => {
|
|
|
|
|
|
const selectedComponents = layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id));
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 컴포넌트가 없거나 1개 이하면 그룹화 불가
|
|
|
|
|
|
if (selectedComponents.length < 2) {
|
|
|
|
|
|
toast.error("그룹으로 묶을 버튼을 2개 이상 선택해주세요");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 모두 버튼인지 확인
|
|
|
|
|
|
if (!areAllButtons(selectedComponents)) {
|
|
|
|
|
|
toast.error("버튼 컴포넌트만 그룹으로 묶을 수 있습니다");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 다이얼로그 열기
|
|
|
|
|
|
setGroupDialogOpen(true);
|
|
|
|
|
|
}, [layout, groupState.selectedComponents]);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 그룹 생성 확인 핸들러
|
|
|
|
|
|
const handleGroupConfirm = useCallback(
|
|
|
|
|
|
(settings: {
|
|
|
|
|
|
direction: "horizontal" | "vertical";
|
|
|
|
|
|
gap: number;
|
|
|
|
|
|
align: "start" | "center" | "end" | "space-between" | "space-around";
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const selectedComponents = layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id));
|
|
|
|
|
|
|
|
|
|
|
|
// 고유한 그룹 ID 생성
|
|
|
|
|
|
const newGroupId = generateGroupId();
|
|
|
|
|
|
|
2025-10-24 17:27:22 +09:00
|
|
|
|
// 🔧 그룹 위치 및 버튼 재배치 계산
|
|
|
|
|
|
const align = settings.align;
|
|
|
|
|
|
const direction = settings.direction;
|
|
|
|
|
|
const gap = settings.gap;
|
|
|
|
|
|
|
|
|
|
|
|
const groupY = Math.min(...selectedComponents.map((b) => b.position.y));
|
|
|
|
|
|
let anchorButton; // 기준이 되는 버튼
|
|
|
|
|
|
let groupX: number;
|
|
|
|
|
|
|
|
|
|
|
|
// align에 따라 기준 버튼과 그룹 시작점 결정
|
|
|
|
|
|
if (direction === "horizontal") {
|
|
|
|
|
|
if (align === "end") {
|
|
|
|
|
|
// 끝점 정렬: 가장 오른쪽 버튼이 기준
|
|
|
|
|
|
anchorButton = selectedComponents.reduce((max, btn) => {
|
|
|
|
|
|
const rightEdge = btn.position.x + (btn.size?.width || 100);
|
|
|
|
|
|
const maxRightEdge = max.position.x + (max.size?.width || 100);
|
|
|
|
|
|
return rightEdge > maxRightEdge ? btn : max;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 전체 그룹 너비 계산
|
|
|
|
|
|
const totalWidth = selectedComponents.reduce((total, btn, index) => {
|
|
|
|
|
|
const buttonWidth = btn.size?.width || 100;
|
|
|
|
|
|
const gapWidth = index < selectedComponents.length - 1 ? gap : 0;
|
|
|
|
|
|
return total + buttonWidth + gapWidth;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 시작점 = 기준 버튼의 오른쪽 끝 - 전체 그룹 너비
|
|
|
|
|
|
groupX = anchorButton.position.x + (anchorButton.size?.width || 100) - totalWidth;
|
|
|
|
|
|
} else if (align === "center") {
|
|
|
|
|
|
// 중앙 정렬: 버튼들의 중심점을 기준으로
|
|
|
|
|
|
const minX = Math.min(...selectedComponents.map((b) => b.position.x));
|
|
|
|
|
|
const maxX = Math.max(...selectedComponents.map((b) => b.position.x + (b.size?.width || 100)));
|
|
|
|
|
|
const centerX = (minX + maxX) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
const totalWidth = selectedComponents.reduce((total, btn, index) => {
|
|
|
|
|
|
const buttonWidth = btn.size?.width || 100;
|
|
|
|
|
|
const gapWidth = index < selectedComponents.length - 1 ? gap : 0;
|
|
|
|
|
|
return total + buttonWidth + gapWidth;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
|
|
groupX = centerX - totalWidth / 2;
|
|
|
|
|
|
anchorButton = selectedComponents[0]; // 중앙 정렬은 첫 번째 버튼 기준
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 시작점 정렬: 가장 왼쪽 버튼이 기준
|
|
|
|
|
|
anchorButton = selectedComponents.reduce((min, btn) => {
|
|
|
|
|
|
return btn.position.x < min.position.x ? btn : min;
|
|
|
|
|
|
});
|
|
|
|
|
|
groupX = anchorButton.position.x;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 세로 정렬: 가장 위쪽 버튼이 기준
|
|
|
|
|
|
anchorButton = selectedComponents.reduce((min, btn) => {
|
|
|
|
|
|
return btn.position.y < min.position.y ? btn : min;
|
|
|
|
|
|
});
|
|
|
|
|
|
groupX = Math.min(...selectedComponents.map((b) => b.position.x));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 버튼들의 위치를 그룹 기준으로 재배치
|
|
|
|
|
|
// 기준 버튼의 절대 위치를 유지하고, FlexBox가 나머지를 자동 정렬
|
2025-10-24 10:37:02 +09:00
|
|
|
|
const groupedButtons = selectedComponents.map((button) => {
|
|
|
|
|
|
const currentConfig = (button as any).webTypeConfig?.flowVisibilityConfig || {};
|
|
|
|
|
|
|
2025-10-24 17:27:22 +09:00
|
|
|
|
// 모든 버튼을 그룹 시작점에 배치
|
|
|
|
|
|
// FlexBox가 자동으로 정렬하여 기준 버튼의 위치가 유지됨
|
|
|
|
|
|
const newPosition = {
|
|
|
|
|
|
x: groupX,
|
|
|
|
|
|
y: groupY,
|
|
|
|
|
|
z: button.position.z || 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-24 10:37:02 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...button,
|
2025-10-24 17:27:22 +09:00
|
|
|
|
position: newPosition,
|
2025-10-24 10:37:02 +09:00
|
|
|
|
webTypeConfig: {
|
|
|
|
|
|
...(button as any).webTypeConfig,
|
|
|
|
|
|
flowVisibilityConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
layoutBehavior: "auto-compact",
|
|
|
|
|
|
groupId: newGroupId,
|
|
|
|
|
|
groupDirection: settings.direction,
|
|
|
|
|
|
groupGap: settings.gap,
|
|
|
|
|
|
groupAlign: settings.align,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃 업데이트
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp) => {
|
|
|
|
|
|
const grouped = groupedButtons.find((gb) => gb.id === comp.id);
|
|
|
|
|
|
return grouped || comp;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${selectedComponents.length}개의 버튼이 플로우 그룹으로 묶였습니다`, {
|
|
|
|
|
|
description: `그룹 ID: ${newGroupId} / ${settings.direction === "horizontal" ? "가로" : "세로"} / ${settings.gap}px 간격`,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 플로우 버튼 그룹 생성 완료:", {
|
|
|
|
|
|
groupId: newGroupId,
|
|
|
|
|
|
buttonCount: selectedComponents.length,
|
2025-10-24 17:27:22 +09:00
|
|
|
|
buttons: selectedComponents.map((b) => ({ id: b.id, position: b.position })),
|
|
|
|
|
|
groupPosition: { x: groupX, y: groupY },
|
2025-10-24 10:37:02 +09:00
|
|
|
|
settings,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[layout, groupState.selectedComponents, saveToHistory],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 플로우 버튼 그룹 해제
|
|
|
|
|
|
const handleFlowButtonUngroup = useCallback(() => {
|
|
|
|
|
|
const selectedComponents = layout.components.filter((comp) => groupState.selectedComponents.includes(comp.id));
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedComponents.length === 0) {
|
|
|
|
|
|
toast.error("그룹 해제할 버튼을 선택해주세요");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 버튼이 아닌 것 필터링
|
|
|
|
|
|
const buttons = selectedComponents.filter((comp) => areAllButtons([comp]));
|
|
|
|
|
|
|
|
|
|
|
|
if (buttons.length === 0) {
|
|
|
|
|
|
toast.error("선택된 버튼 중 그룹화된 버튼이 없습니다");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 해제
|
|
|
|
|
|
const ungroupedButtons = ungroupButtons(buttons);
|
|
|
|
|
|
|
2025-10-29 11:26:00 +09:00
|
|
|
|
// 레이아웃 업데이트 + 플로우 표시 제어 초기화
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp, index) => {
|
2025-10-24 10:37:02 +09:00
|
|
|
|
const ungrouped = ungroupedButtons.find((ub) => ub.id === comp.id);
|
2025-10-29 11:26:00 +09:00
|
|
|
|
|
|
|
|
|
|
if (ungrouped) {
|
|
|
|
|
|
// 원래 위치 복원 또는 현재 위치 유지 + 간격 추가
|
|
|
|
|
|
const buttonIndex = buttons.findIndex((b) => b.id === comp.id);
|
|
|
|
|
|
const basePosition = comp.position;
|
|
|
|
|
|
|
|
|
|
|
|
// 버튼들을 오른쪽으로 조금씩 이동 (겹치지 않도록)
|
|
|
|
|
|
const offsetX = buttonIndex * 120; // 각 버튼당 120px 간격
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 해제된 버튼의 플로우 표시 제어를 끄고 설정 초기화
|
|
|
|
|
|
return {
|
|
|
|
|
|
...ungrouped,
|
|
|
|
|
|
position: {
|
|
|
|
|
|
x: basePosition.x + offsetX,
|
|
|
|
|
|
y: basePosition.y,
|
|
|
|
|
|
z: basePosition.z || 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
webTypeConfig: {
|
|
|
|
|
|
...ungrouped.webTypeConfig,
|
|
|
|
|
|
flowVisibilityConfig: {
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
targetFlowComponentId: null,
|
|
|
|
|
|
mode: "whitelist",
|
|
|
|
|
|
visibleSteps: [],
|
|
|
|
|
|
hiddenSteps: [],
|
|
|
|
|
|
layoutBehavior: "auto-compact",
|
|
|
|
|
|
groupId: null,
|
|
|
|
|
|
groupDirection: "horizontal",
|
|
|
|
|
|
groupGap: 8,
|
|
|
|
|
|
groupAlign: "start",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return comp;
|
2025-10-24 10:37:02 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
|
2025-10-29 11:26:00 +09:00
|
|
|
|
toast.success(`${buttons.length}개의 버튼 그룹이 해제되고 플로우 표시 제어가 비활성화되었습니다`);
|
2025-10-24 10:37:02 +09:00
|
|
|
|
}, [layout, groupState.selectedComponents, saveToHistory]);
|
|
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
// 그룹 생성 (임시 비활성화)
|
2025-09-01 15:22:47 +09:00
|
|
|
|
const handleGroupCreate = useCallback(
|
|
|
|
|
|
(componentIds: string[], title: string, style?: any) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("그룹 생성 기능이 임시 비활성화되었습니다.");
|
2025-09-03 11:32:09 +09:00
|
|
|
|
toast.info("그룹 기능이 임시 비활성화되었습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 격자 정보 계산
|
|
|
|
|
|
const currentGridInfo =
|
|
|
|
|
|
gridInfo ||
|
|
|
|
|
|
calculateGridInfo(
|
|
|
|
|
|
1200,
|
|
|
|
|
|
800,
|
|
|
|
|
|
layout.gridSettings || {
|
|
|
|
|
|
columns: 12,
|
|
|
|
|
|
gap: 16,
|
2025-10-14 18:07:38 +09:00
|
|
|
|
padding: 0,
|
2025-09-03 11:32:09 +09:00
|
|
|
|
snapToGrid: true,
|
2025-10-15 13:30:11 +09:00
|
|
|
|
showGrid: false,
|
2025-09-03 11:32:09 +09:00
|
|
|
|
gridColor: "#d1d5db",
|
|
|
|
|
|
gridOpacity: 0.5,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔧 그룹 생성 시작:", {
|
|
|
|
|
|
selectedCount: selectedComponents.length,
|
2025-11-10 14:51:36 +09:00
|
|
|
|
snapToGrid: true,
|
2025-09-03 11:32:09 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 크기 조정 기반 그룹 크기 계산
|
|
|
|
|
|
const calculateOptimalGroupSize = () => {
|
|
|
|
|
|
if (!currentGridInfo || !layout.gridSettings?.snapToGrid) {
|
|
|
|
|
|
// 격자 스냅이 비활성화된 경우 기본 패딩 사용
|
|
|
|
|
|
const boundingBox = calculateBoundingBox(selectedComponents);
|
|
|
|
|
|
const padding = 40;
|
|
|
|
|
|
return {
|
|
|
|
|
|
boundingBox,
|
|
|
|
|
|
groupPosition: { x: boundingBox.minX - padding, y: boundingBox.minY - padding, z: 1 },
|
|
|
|
|
|
groupSize: { width: boundingBox.width + padding * 2, height: boundingBox.height + padding * 2 },
|
|
|
|
|
|
gridColumns: 1,
|
|
|
|
|
|
scaledComponents: selectedComponents, // 크기 조정 없음
|
|
|
|
|
|
padding: padding,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { columnWidth } = currentGridInfo;
|
|
|
|
|
|
const gap = layout.gridSettings?.gap || 16;
|
|
|
|
|
|
const contentBoundingBox = calculateBoundingBox(selectedComponents);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 간단한 접근: 컴포넌트들의 시작점에서 가장 가까운 격자 시작점 찾기
|
|
|
|
|
|
const startColumn = Math.floor(contentBoundingBox.minX / (columnWidth + gap));
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 컴포넌트들의 끝점까지 포함할 수 있는 컬럼 수 계산
|
|
|
|
|
|
const groupStartX = startColumn * (columnWidth + gap);
|
|
|
|
|
|
const availableWidthFromStart = contentBoundingBox.maxX - groupStartX;
|
|
|
|
|
|
const currentWidthInColumns = Math.ceil(availableWidthFromStart / (columnWidth + gap));
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 그룹은 격자에 정확히 맞게 위치와 크기 설정
|
|
|
|
|
|
const padding = 20;
|
|
|
|
|
|
const groupX = startColumn * (columnWidth + gap); // 격자 시작점에 정확히 맞춤
|
|
|
|
|
|
const groupY = contentBoundingBox.minY - padding;
|
|
|
|
|
|
const groupWidth = currentWidthInColumns * columnWidth + (currentWidthInColumns - 1) * gap; // 컬럼 크기 + gap
|
|
|
|
|
|
const groupHeight = contentBoundingBox.height + padding * 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 내부 컴포넌트들을 그룹 크기에 맞게 스케일링
|
|
|
|
|
|
const availableWidth = groupWidth - padding * 2; // 패딩 제외한 실제 사용 가능 너비
|
|
|
|
|
|
const scaleFactorX = availableWidth / contentBoundingBox.width;
|
|
|
|
|
|
|
|
|
|
|
|
const scaledComponents = selectedComponents.map((comp) => {
|
|
|
|
|
|
// 컴포넌트의 원래 위치에서 컨텐츠 영역 시작점까지의 상대 위치 계산
|
|
|
|
|
|
const relativeX = comp.position.x - contentBoundingBox.minX;
|
|
|
|
|
|
const relativeY = comp.position.y - contentBoundingBox.minY;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
position: {
|
|
|
|
|
|
x: padding + relativeX * scaleFactorX, // 패딩 + 스케일된 상대 위치
|
|
|
|
|
|
y: padding + relativeY, // Y는 스케일링 없이 패딩만 적용
|
|
|
|
|
|
z: comp.position.z || 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
size: {
|
|
|
|
|
|
width: comp.size.width * scaleFactorX, // X 방향 스케일링
|
|
|
|
|
|
height: comp.size.height, // Y는 원본 크기 유지
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎯 컴포넌트 크기 조정 기반 그룹 생성:", {
|
|
|
|
|
|
originalBoundingBox: contentBoundingBox,
|
|
|
|
|
|
gridCalculation: {
|
|
|
|
|
|
columnWidthPlusGap: columnWidth + gap,
|
|
|
|
|
|
startColumn: `Math.floor(${contentBoundingBox.minX} / ${columnWidth + gap}) = ${startColumn}`,
|
|
|
|
|
|
groupStartX: `${startColumn} * ${columnWidth + gap} = ${groupStartX}`,
|
|
|
|
|
|
availableWidthFromStart: `${contentBoundingBox.maxX} - ${groupStartX} = ${availableWidthFromStart}`,
|
|
|
|
|
|
currentWidthInColumns: `Math.ceil(${availableWidthFromStart} / ${columnWidth + gap}) = ${currentWidthInColumns}`,
|
|
|
|
|
|
finalGroupX: `${startColumn} * ${columnWidth + gap} = ${groupX}`,
|
|
|
|
|
|
actualGroupWidth: `${currentWidthInColumns}컬럼 * ${columnWidth}px + ${currentWidthInColumns - 1}gap * ${gap}px = ${groupWidth}px`,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupPosition: { x: groupX, y: groupY },
|
|
|
|
|
|
groupSize: { width: groupWidth, height: groupHeight },
|
|
|
|
|
|
scaleFactorX,
|
|
|
|
|
|
availableWidth,
|
|
|
|
|
|
padding,
|
|
|
|
|
|
scaledComponentsCount: scaledComponents.length,
|
|
|
|
|
|
scaledComponentsDetails: scaledComponents.map((comp) => {
|
|
|
|
|
|
const original = selectedComponents.find((c) => c.id === comp.id);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: comp.id,
|
|
|
|
|
|
originalPos: original?.position,
|
|
|
|
|
|
scaledPos: comp.position,
|
|
|
|
|
|
originalSize: original?.size,
|
|
|
|
|
|
scaledSize: comp.size,
|
|
|
|
|
|
deltaX: comp.position.x - (original?.position.x || 0),
|
|
|
|
|
|
deltaY: comp.position.y - (original?.position.y || 0),
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
return {
|
|
|
|
|
|
boundingBox: contentBoundingBox,
|
|
|
|
|
|
groupPosition: { x: groupX, y: groupY, z: 1 },
|
|
|
|
|
|
groupSize: { width: groupWidth, height: groupHeight },
|
|
|
|
|
|
gridColumns: currentWidthInColumns,
|
|
|
|
|
|
scaledComponents: scaledComponents, // 스케일된 컴포넌트들
|
|
|
|
|
|
padding: padding,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
boundingBox,
|
|
|
|
|
|
groupPosition,
|
|
|
|
|
|
groupSize: optimizedGroupSize,
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
scaledComponents,
|
|
|
|
|
|
padding,
|
|
|
|
|
|
} = calculateOptimalGroupSize();
|
|
|
|
|
|
|
|
|
|
|
|
// 스케일된 컴포넌트들로 상대 위치 계산 (이미 최적화되어 추가 격자 정렬 불필요)
|
2025-09-01 15:57:49 +09:00
|
|
|
|
const relativeChildren = calculateRelativePositions(
|
2025-09-03 11:32:09 +09:00
|
|
|
|
scaledComponents,
|
|
|
|
|
|
groupPosition,
|
|
|
|
|
|
"temp", // 임시 그룹 ID
|
2025-09-01 15:57:49 +09:00
|
|
|
|
);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
console.log("📏 최적화된 그룹 생성 (컴포넌트 스케일링):", {
|
|
|
|
|
|
gridColumns,
|
|
|
|
|
|
groupSize: optimizedGroupSize,
|
|
|
|
|
|
groupPosition,
|
|
|
|
|
|
scaledComponentsCount: scaledComponents.length,
|
|
|
|
|
|
padding,
|
|
|
|
|
|
strategy: "내부 컴포넌트 크기 조정으로 격자 정확 맞춤",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 컴포넌트 생성 (gridColumns 속성 포함)
|
|
|
|
|
|
const groupComponent = createGroupComponent(componentIds, title, groupPosition, optimizedGroupSize, style);
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹에 계산된 gridColumns 속성 추가
|
|
|
|
|
|
groupComponent.gridColumns = gridColumns;
|
|
|
|
|
|
|
|
|
|
|
|
// 실제 그룹 ID로 자식들 업데이트
|
|
|
|
|
|
const finalChildren = relativeChildren.map((child) => ({
|
|
|
|
|
|
...child,
|
|
|
|
|
|
parentId: groupComponent.id,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-09-01 15:22:47 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: [
|
2025-09-01 15:57:49 +09:00
|
|
|
|
...layout.components.filter((comp) => !componentIds.includes(comp.id)),
|
2025-09-01 15:22:47 +09:00
|
|
|
|
groupComponent,
|
2025-09-03 11:32:09 +09:00
|
|
|
|
...finalChildren,
|
2025-09-01 15:22:47 +09:00
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-09-03 11:32:09 +09:00
|
|
|
|
setGroupState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
selectedComponents: [groupComponent.id],
|
|
|
|
|
|
isGrouping: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
setSelectedComponent(groupComponent);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎯 최적화된 그룹 생성 완료:", {
|
|
|
|
|
|
groupId: groupComponent.id,
|
|
|
|
|
|
childrenCount: finalChildren.length,
|
|
|
|
|
|
position: groupPosition,
|
|
|
|
|
|
size: optimizedGroupSize,
|
|
|
|
|
|
gridColumns: groupComponent.gridColumns,
|
|
|
|
|
|
componentsScaled: !!scaledComponents.length,
|
2025-11-10 14:52:20 +09:00
|
|
|
|
gridAligned: true,
|
2025-09-03 11:32:09 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`그룹이 생성되었습니다 (${finalChildren.length}개 컴포넌트)`);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
},
|
2025-11-10 14:46:30 +09:00
|
|
|
|
[layout, saveToHistory],
|
2025-09-01 15:22:47 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 그룹 생성 함수 (다이얼로그 표시)
|
|
|
|
|
|
const createGroup = useCallback(() => {
|
|
|
|
|
|
if (groupState.selectedComponents.length < 2) {
|
|
|
|
|
|
toast.warning("그룹을 만들려면 2개 이상의 컴포넌트를 선택해야 합니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 그룹 생성 다이얼로그 표시");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setShowGroupCreateDialog(true);
|
|
|
|
|
|
}, [groupState.selectedComponents]);
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
// 그룹 해제 함수 (임시 비활성화)
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const ungroupComponents = useCallback(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("그룹 해제 기능이 임시 비활성화되었습니다.");
|
2025-09-03 11:32:09 +09:00
|
|
|
|
toast.info("그룹 해제 기능이 임시 비활성화되었습니다.");
|
|
|
|
|
|
return;
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const groupId = selectedComponent.id;
|
2025-09-01 15:22:47 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 자식 컴포넌트들의 절대 위치 복원
|
|
|
|
|
|
const childComponents = layout.components.filter((comp) => comp.parentId === groupId);
|
|
|
|
|
|
const restoredChildren = restoreAbsolutePositions(childComponents, selectedComponent.position);
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 자식 컴포넌트들의 위치 복원 및 parentId 제거
|
|
|
|
|
|
const updatedComponents = layout.components
|
|
|
|
|
|
.map((comp) => {
|
|
|
|
|
|
if (comp.parentId === groupId) {
|
|
|
|
|
|
const restoredChild = restoredChildren.find((restored) => restored.id === comp.id);
|
|
|
|
|
|
return restoredChild || { ...comp, parentId: undefined };
|
|
|
|
|
|
}
|
|
|
|
|
|
return comp;
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((comp) => comp.id !== groupId); // 그룹 컴포넌트 제거
|
2025-09-01 18:42:59 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
const newLayout = { ...layout, components: updatedComponents };
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 선택 상태 초기화
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: [] }));
|
|
|
|
|
|
}, [selectedComponent, layout, saveToHistory]);
|
2025-09-01 18:42:59 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 마우스 이벤트 처리 (드래그 및 선택) - 성능 최적화
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let animationFrameId: number;
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
|
|
|
|
if (dragState.isDragging) {
|
|
|
|
|
|
// requestAnimationFrame으로 부드러운 애니메이션
|
|
|
|
|
|
if (animationFrameId) {
|
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
2025-09-02 11:16:40 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
animationFrameId = requestAnimationFrame(() => {
|
|
|
|
|
|
updateDragPosition(e);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (selectionDrag.isSelecting) {
|
|
|
|
|
|
updateSelectionDrag(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2026-01-26 11:04:39 +09:00
|
|
|
|
const handleMouseUp = (e: MouseEvent) => {
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (dragState.isDragging) {
|
|
|
|
|
|
if (animationFrameId) {
|
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
|
|
|
|
|
}
|
2026-01-26 11:04:39 +09:00
|
|
|
|
endDrag(e);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
} else if (selectionDrag.isSelecting) {
|
|
|
|
|
|
endSelectionDrag();
|
2025-09-01 18:42:59 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (dragState.isDragging || selectionDrag.isSelecting) {
|
|
|
|
|
|
document.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
if (animationFrameId) {
|
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
|
|
|
|
|
}
|
|
|
|
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
|
|
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
2025-09-01 18:42:59 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}, [
|
|
|
|
|
|
dragState.isDragging,
|
|
|
|
|
|
selectionDrag.isSelecting,
|
|
|
|
|
|
updateDragPosition,
|
|
|
|
|
|
endDrag,
|
|
|
|
|
|
updateSelectionDrag,
|
|
|
|
|
|
endSelectionDrag,
|
|
|
|
|
|
]);
|
2025-09-01 18:42:59 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 키보드 이벤트 처리 (브라우저 기본 기능 완전 차단)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🎯 키 입력 감지:", { key: e.key, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, metaKey: e.metaKey });
|
2025-09-02 16:18:38 +09:00
|
|
|
|
|
|
|
|
|
|
// 🚫 브라우저 기본 단축키 완전 차단 목록
|
|
|
|
|
|
const browserShortcuts = [
|
|
|
|
|
|
// 검색 관련
|
|
|
|
|
|
{ ctrl: true, key: "f" }, // 페이지 내 검색
|
|
|
|
|
|
{ ctrl: true, key: "g" }, // 다음 검색 결과
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "g" }, // 이전 검색 결과
|
|
|
|
|
|
{ ctrl: true, key: "h" }, // 검색 기록
|
|
|
|
|
|
|
|
|
|
|
|
// 탭/창 관리
|
|
|
|
|
|
{ ctrl: true, key: "t" }, // 새 탭
|
|
|
|
|
|
{ ctrl: true, key: "w" }, // 탭 닫기
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "t" }, // 닫힌 탭 복원
|
|
|
|
|
|
{ ctrl: true, key: "n" }, // 새 창
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "n" }, // 시크릿 창
|
|
|
|
|
|
|
|
|
|
|
|
// 페이지 관리
|
|
|
|
|
|
{ ctrl: true, key: "r" }, // 새로고침
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "r" }, // 강제 새로고침
|
|
|
|
|
|
{ ctrl: true, key: "d" }, // 북마크 추가
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "d" }, // 모든 탭 북마크
|
|
|
|
|
|
|
|
|
|
|
|
// 편집 관련 (필요시에만 허용)
|
|
|
|
|
|
{ ctrl: true, key: "s" }, // 저장 (필요시 차단 해제)
|
|
|
|
|
|
{ ctrl: true, key: "p" }, // 인쇄
|
|
|
|
|
|
{ ctrl: true, key: "o" }, // 파일 열기
|
|
|
|
|
|
{ ctrl: true, key: "v" }, // 붙여넣기 (브라우저 기본 동작 차단)
|
|
|
|
|
|
|
|
|
|
|
|
// 개발자 도구
|
|
|
|
|
|
{ key: "F12" }, // 개발자 도구
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "i" }, // 개발자 도구
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "c" }, // 요소 검사
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "j" }, // 콘솔
|
|
|
|
|
|
{ ctrl: true, key: "u" }, // 소스 보기
|
|
|
|
|
|
|
|
|
|
|
|
// 기타
|
|
|
|
|
|
{ ctrl: true, key: "j" }, // 다운로드
|
|
|
|
|
|
{ ctrl: true, shift: true, key: "delete" }, // 브라우징 데이터 삭제
|
|
|
|
|
|
{ ctrl: true, key: "+" }, // 확대
|
|
|
|
|
|
{ ctrl: true, key: "-" }, // 축소
|
|
|
|
|
|
{ ctrl: true, key: "0" }, // 확대/축소 초기화
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 브라우저 기본 단축키 체크 및 차단
|
|
|
|
|
|
const isBrowserShortcut = browserShortcuts.some((shortcut) => {
|
|
|
|
|
|
const ctrlMatch = shortcut.ctrl ? e.ctrlKey || e.metaKey : true;
|
|
|
|
|
|
const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
|
2025-09-03 11:32:09 +09:00
|
|
|
|
const keyMatch = e.key?.toLowerCase() === shortcut.key?.toLowerCase();
|
2025-09-02 16:18:38 +09:00
|
|
|
|
return ctrlMatch && shiftMatch && keyMatch;
|
|
|
|
|
|
});
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (isBrowserShortcut) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🚫 브라우저 기본 단축키 차단:", e.key);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// ✅ 애플리케이션 전용 단축키 처리
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 1. 그룹 관련 단축키
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "g" && !e.shiftKey) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 그룹 생성 단축키");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (groupState.selectedComponents.length >= 2) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 그룹 생성 실행");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
createGroup();
|
2025-09-01 15:22:47 +09:00
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("⚠️ 선택된 컴포넌트가 부족함 (2개 이상 필요)");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key?.toLowerCase() === "g") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 그룹 해제 단축키");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
2025-09-02 11:16:40 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (selectedComponent && selectedComponent.type === "group") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 그룹 해제 실행");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
ungroupComponents();
|
|
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("⚠️ 선택된 그룹이 없음");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 2. 전체 선택 (애플리케이션 내에서만)
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "a") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 전체 선택 (애플리케이션 내)");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
const allComponentIds = layout.components.map((comp) => comp.id);
|
|
|
|
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: allComponentIds }));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 3. 실행취소/다시실행
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "z" && !e.shiftKey) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 실행취소");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
undo();
|
|
|
|
|
|
return false;
|
2025-09-01 14:26:39 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
if (
|
2025-09-03 11:32:09 +09:00
|
|
|
|
((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "y") ||
|
|
|
|
|
|
((e.ctrlKey || e.metaKey) && e.shiftKey && e.key?.toLowerCase() === "z")
|
2025-09-02 16:18:38 +09:00
|
|
|
|
) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 다시실행");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
redo();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 4. 복사 (컴포넌트 복사)
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "c") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컴포넌트 복사");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
copyComponent();
|
|
|
|
|
|
return false;
|
2025-09-02 11:16:40 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 5. 붙여넣기 (컴포넌트 붙여넣기)
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "v") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컴포넌트 붙여넣기");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
pasteComponent();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 6. 삭제 (단일/다중 선택 지원)
|
|
|
|
|
|
if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ 컴포넌트 삭제 (단축키)");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
deleteComponent();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 7. 선택 해제
|
|
|
|
|
|
if (e.key === "Escape") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 선택 해제");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
setGroupState((prev) => ({ ...prev, selectedComponents: [], isGrouping: false }));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-01 16:40:24 +09:00
|
|
|
|
|
2025-09-02 16:18:38 +09:00
|
|
|
|
// 8. 저장 (Ctrl+S는 레이아웃 저장용으로 사용)
|
2025-09-03 11:32:09 +09:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key?.toLowerCase() === "s") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("💾 레이아웃 저장");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃 저장 실행
|
|
|
|
|
|
if (layout.components.length > 0 && selectedScreen?.screenId) {
|
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
|
try {
|
2025-09-04 15:20:26 +09:00
|
|
|
|
// 해상도 정보를 포함한 레이아웃 데이터 생성
|
|
|
|
|
|
const layoutWithResolution = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
screenResolution: screenResolution,
|
|
|
|
|
|
};
|
2025-09-04 17:01:07 +09:00
|
|
|
|
console.log("⚡ 자동 저장할 레이아웃 데이터:", {
|
|
|
|
|
|
componentsCount: layoutWithResolution.components.length,
|
|
|
|
|
|
gridSettings: layoutWithResolution.gridSettings,
|
|
|
|
|
|
screenResolution: layoutWithResolution.screenResolution,
|
|
|
|
|
|
});
|
2026-02-02 15:15:01 +09:00
|
|
|
|
// V2/POP API 사용 여부에 따라 분기
|
|
|
|
|
|
const v2Layout = convertLegacyToV2(layoutWithResolution);
|
|
|
|
|
|
if (USE_POP_API) {
|
|
|
|
|
|
await screenApi.saveLayoutPop(selectedScreen.screenId, v2Layout);
|
|
|
|
|
|
} else if (USE_V2_API) {
|
2026-01-28 11:24:25 +09:00
|
|
|
|
await screenApi.saveLayoutV2(selectedScreen.screenId, v2Layout);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution);
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.success("레이아웃이 저장되었습니다.");
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("레이아웃 저장 실패:", error);
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.error("레이아웃 저장에 실패했습니다.");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("⚠️ 저장할 컴포넌트가 없습니다");
|
2025-09-02 16:18:38 +09:00
|
|
|
|
toast.warning("저장할 컴포넌트가 없습니다.");
|
2025-09-01 16:40:24 +09:00
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
return false;
|
2025-09-01 14:26:39 +09:00
|
|
|
|
}
|
2026-02-06 15:18:27 +09:00
|
|
|
|
|
|
|
|
|
|
// === 9. 화살표 키 Nudge (컴포넌트 미세 이동) ===
|
|
|
|
|
|
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
|
|
|
|
// 입력 필드에서는 무시
|
|
|
|
|
|
const active = document.activeElement;
|
|
|
|
|
|
if (
|
|
|
|
|
|
active instanceof HTMLInputElement ||
|
|
|
|
|
|
active instanceof HTMLTextAreaElement ||
|
|
|
|
|
|
active?.getAttribute("contenteditable") === "true"
|
|
|
|
|
|
) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedComponent || groupState.selectedComponents.length > 0) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
const distance = e.shiftKey ? 10 : 1; // Shift 누르면 10px
|
|
|
|
|
|
const dirMap: Record<string, "up" | "down" | "left" | "right"> = {
|
|
|
|
|
|
ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right",
|
|
|
|
|
|
};
|
|
|
|
|
|
handleNudge(dirMap[e.key], distance);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 10. 정렬 단축키 (Alt + 키) - 다중 선택 시 ===
|
|
|
|
|
|
if (e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
|
|
|
|
const alignKey = e.key?.toLowerCase();
|
|
|
|
|
|
const alignMap: Record<string, AlignMode> = {
|
|
|
|
|
|
l: "left", r: "right", c: "centerX",
|
|
|
|
|
|
t: "top", b: "bottom", m: "centerY",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (alignMap[alignKey] && groupState.selectedComponents.length >= 2) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleGroupAlign(alignMap[alignKey]);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 균등 배분 (Alt+H: 가로, Alt+V: 세로)
|
|
|
|
|
|
if (alignKey === "h" && groupState.selectedComponents.length >= 3) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleGroupDistribute("horizontal");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (alignKey === "v" && groupState.selectedComponents.length >= 3) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleGroupDistribute("vertical");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 동일 크기 맞추기 (Alt+W: 너비, Alt+E: 높이)
|
|
|
|
|
|
if (alignKey === "w" && groupState.selectedComponents.length >= 2) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleMatchSize("width");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (alignKey === "e" && groupState.selectedComponents.length >= 2) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleMatchSize("height");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 11. 라벨 일괄 토글 (Alt+Shift+L) ===
|
|
|
|
|
|
if (e.altKey && e.shiftKey && e.key?.toLowerCase() === "l") {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
handleToggleAllLabels();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 12. 단축키 도움말 (? 키) ===
|
|
|
|
|
|
if (e.key === "?" && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
|
|
|
|
// 입력 필드에서는 무시
|
|
|
|
|
|
const active = document.activeElement;
|
|
|
|
|
|
if (
|
|
|
|
|
|
active instanceof HTMLInputElement ||
|
|
|
|
|
|
active instanceof HTMLTextAreaElement ||
|
|
|
|
|
|
active?.getAttribute("contenteditable") === "true"
|
|
|
|
|
|
) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setShowShortcutsModal(true);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-02 16:18:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// window 레벨에서 캡처 단계에서 가장 먼저 처리
|
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown, { capture: true, passive: false });
|
|
|
|
|
|
return () => window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
|
|
|
|
}, [
|
|
|
|
|
|
selectedComponent,
|
|
|
|
|
|
deleteComponent,
|
|
|
|
|
|
copyComponent,
|
|
|
|
|
|
pasteComponent,
|
|
|
|
|
|
undo,
|
|
|
|
|
|
redo,
|
|
|
|
|
|
createGroup,
|
|
|
|
|
|
ungroupComponents,
|
|
|
|
|
|
groupState.selectedComponents,
|
|
|
|
|
|
layout,
|
|
|
|
|
|
selectedScreen,
|
2026-02-06 15:18:27 +09:00
|
|
|
|
handleNudge,
|
|
|
|
|
|
handleGroupAlign,
|
|
|
|
|
|
handleGroupDistribute,
|
|
|
|
|
|
handleMatchSize,
|
|
|
|
|
|
handleToggleAllLabels,
|
2025-09-02 16:18:38 +09:00
|
|
|
|
]);
|
2025-09-01 14:26:39 +09:00
|
|
|
|
|
2025-10-23 15:06:00 +09:00
|
|
|
|
// 플로우 위젯 높이 자동 업데이트 이벤트 리스너
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleComponentSizeUpdate = (event: CustomEvent) => {
|
|
|
|
|
|
const { componentId, height } = event.detail;
|
|
|
|
|
|
|
|
|
|
|
|
// 해당 컴포넌트 찾기
|
|
|
|
|
|
const targetComponent = layout.components.find((c) => c.id === componentId);
|
|
|
|
|
|
if (!targetComponent) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 이미 같은 높이면 업데이트 안함
|
|
|
|
|
|
if (targetComponent.size?.height === height) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 높이 업데이트
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp) => {
|
|
|
|
|
|
if (comp.id === componentId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...comp,
|
|
|
|
|
|
size: {
|
|
|
|
|
|
...comp.size,
|
|
|
|
|
|
width: comp.size?.width || 100,
|
|
|
|
|
|
height: height,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return comp;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 컴포넌트도 업데이트
|
|
|
|
|
|
if (selectedComponent?.id === componentId) {
|
|
|
|
|
|
const updatedComponent = updatedComponents.find((c) => c.id === componentId);
|
|
|
|
|
|
if (updatedComponent) {
|
|
|
|
|
|
setSelectedComponent(updatedComponent);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [layout, selectedComponent]);
|
|
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 🆕 조건부 영역 드래그 핸들러 (이동/리사이즈, DB 기반)
|
|
|
|
|
|
const handleRegionMouseDown = useCallback((
|
|
|
|
|
|
e: React.MouseEvent,
|
|
|
|
|
|
layerId: string,
|
|
|
|
|
|
mode: "move" | "resize",
|
|
|
|
|
|
handle?: string,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const lid = Number(layerId);
|
|
|
|
|
|
const region = layerRegions[lid];
|
|
|
|
|
|
if (!region) return;
|
|
|
|
|
|
|
|
|
|
|
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!canvasRect) return;
|
|
|
|
|
|
|
|
|
|
|
|
const x = (e.clientX - canvasRect.left) / zoomLevel;
|
|
|
|
|
|
const y = (e.clientY - canvasRect.top) / zoomLevel;
|
|
|
|
|
|
|
|
|
|
|
|
setRegionDrag({
|
|
|
|
|
|
isDrawing: false,
|
|
|
|
|
|
isDragging: mode === "move",
|
|
|
|
|
|
isResizing: mode === "resize",
|
|
|
|
|
|
targetLayerId: layerId,
|
|
|
|
|
|
startX: x,
|
|
|
|
|
|
startY: y,
|
|
|
|
|
|
currentX: x,
|
|
|
|
|
|
currentY: y,
|
|
|
|
|
|
resizeHandle: handle || null,
|
|
|
|
|
|
originalRegion: { x: region.x, y: region.y, width: region.width, height: region.height },
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [layerRegions, zoomLevel]);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 캔버스 마우스 이벤트 (영역 이동/리사이즈)
|
|
|
|
|
|
const handleRegionCanvasMouseMove = useCallback((e: React.MouseEvent) => {
|
|
|
|
|
|
if (!regionDrag.isDragging && !regionDrag.isResizing) return;
|
|
|
|
|
|
if (!regionDrag.targetLayerId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
|
if (!canvasRect) return;
|
|
|
|
|
|
|
|
|
|
|
|
const x = (e.clientX - canvasRect.left) / zoomLevel;
|
|
|
|
|
|
const y = (e.clientY - canvasRect.top) / zoomLevel;
|
|
|
|
|
|
|
|
|
|
|
|
if (regionDrag.isDragging && regionDrag.originalRegion) {
|
|
|
|
|
|
const dx = x - regionDrag.startX;
|
|
|
|
|
|
const dy = y - regionDrag.startY;
|
|
|
|
|
|
const newRegion = {
|
|
|
|
|
|
x: Math.max(0, Math.round(regionDrag.originalRegion.x + dx)),
|
|
|
|
|
|
y: Math.max(0, Math.round(regionDrag.originalRegion.y + dy)),
|
|
|
|
|
|
width: regionDrag.originalRegion.width,
|
|
|
|
|
|
height: regionDrag.originalRegion.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
const lid = Number(regionDrag.targetLayerId);
|
|
|
|
|
|
setLayerRegions((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[lid]: { ...prev[lid], ...newRegion },
|
|
|
|
|
|
}));
|
|
|
|
|
|
} else if (regionDrag.isResizing && regionDrag.originalRegion) {
|
|
|
|
|
|
const dx = x - regionDrag.startX;
|
|
|
|
|
|
const dy = y - regionDrag.startY;
|
|
|
|
|
|
const orig = regionDrag.originalRegion;
|
|
|
|
|
|
const newRegion = { ...orig };
|
|
|
|
|
|
|
|
|
|
|
|
const handle = regionDrag.resizeHandle;
|
|
|
|
|
|
if (handle?.includes("e")) newRegion.width = Math.max(50, Math.round(orig.width + dx));
|
|
|
|
|
|
if (handle?.includes("s")) newRegion.height = Math.max(30, Math.round(orig.height + dy));
|
|
|
|
|
|
if (handle?.includes("w")) {
|
|
|
|
|
|
newRegion.x = Math.max(0, Math.round(orig.x + dx));
|
|
|
|
|
|
newRegion.width = Math.max(50, Math.round(orig.width - dx));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (handle?.includes("n")) {
|
|
|
|
|
|
newRegion.y = Math.max(0, Math.round(orig.y + dy));
|
|
|
|
|
|
newRegion.height = Math.max(30, Math.round(orig.height - dy));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const lid = Number(regionDrag.targetLayerId);
|
|
|
|
|
|
setLayerRegions((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[lid]: { ...prev[lid], ...newRegion },
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [regionDrag, zoomLevel]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleRegionCanvasMouseUp = useCallback(async () => {
|
|
|
|
|
|
// 드래그 완료 시 DB에 영역 저장
|
|
|
|
|
|
if ((regionDrag.isDragging || regionDrag.isResizing) && regionDrag.targetLayerId && selectedScreen?.screenId) {
|
|
|
|
|
|
const lid = Number(regionDrag.targetLayerId);
|
|
|
|
|
|
const region = layerRegions[lid];
|
|
|
|
|
|
if (region) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const layerData = await screenApi.getLayerLayout(selectedScreen.screenId, lid);
|
|
|
|
|
|
const existingCondition = layerData?.conditionConfig || {};
|
|
|
|
|
|
await screenApi.updateLayerCondition(
|
|
|
|
|
|
selectedScreen.screenId, lid,
|
|
|
|
|
|
{ ...existingCondition, displayRegion: { x: region.x, y: region.y, width: region.width, height: region.height } }
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.error("영역 저장 실패");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 드래그 상태 초기화
|
|
|
|
|
|
setRegionDrag({
|
|
|
|
|
|
isDrawing: false,
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
isResizing: false,
|
|
|
|
|
|
targetLayerId: null,
|
|
|
|
|
|
startX: 0, startY: 0, currentX: 0, currentY: 0,
|
|
|
|
|
|
resizeHandle: null,
|
|
|
|
|
|
originalRegion: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [regionDrag, layerRegions, selectedScreen]);
|
|
|
|
|
|
|
2026-02-06 09:51:29 +09:00
|
|
|
|
// 🆕 레이어 변경 핸들러 - 레이어 컨텍스트에서 레이어가 변경되면 layout에도 반영
|
2026-02-09 13:21:56 +09:00
|
|
|
|
// 주의: layout.layers에 직접 설정된 displayRegion 등 메타데이터를 보존
|
2026-02-06 09:51:29 +09:00
|
|
|
|
const handleLayersChange = useCallback((newLayers: LayerDefinition[]) => {
|
2026-02-09 13:21:56 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
// 기존 layout.layers의 메타데이터(displayRegion 등)를 보존하며 병합
|
|
|
|
|
|
const mergedLayers = newLayers.map((newLayer) => {
|
|
|
|
|
|
const existingLayer = prevLayout.layers?.find((l) => l.id === newLayer.id);
|
|
|
|
|
|
if (!existingLayer) return newLayer;
|
|
|
|
|
|
|
|
|
|
|
|
// LayerContext에서 온 데이터(condition 등)를 우선하되,
|
|
|
|
|
|
// layout.layers에만 있는 데이터(캔버스에서 직접 수정한 displayRegion)도 보존
|
|
|
|
|
|
return {
|
|
|
|
|
|
...existingLayer, // 기존 메타데이터 보존 (displayRegion 등)
|
|
|
|
|
|
...newLayer, // LayerContext 데이터 우선 (condition, name, isVisible 등)
|
|
|
|
|
|
// displayRegion: 양쪽 모두 있을 수 있으므로 최신 값 우선
|
|
|
|
|
|
displayRegion: newLayer.displayRegion !== undefined
|
|
|
|
|
|
? newLayer.displayRegion
|
|
|
|
|
|
: existingLayer.displayRegion,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
layers: mergedLayers,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-02-06 09:51:29 +09:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 활성 레이어 변경 핸들러
|
2026-02-09 13:21:56 +09:00
|
|
|
|
const handleActiveLayerChange = useCallback((newActiveLayerId: number) => {
|
|
|
|
|
|
setActiveLayerIdWithRef(newActiveLayerId);
|
|
|
|
|
|
}, [setActiveLayerIdWithRef]);
|
2026-02-06 09:51:29 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 초기 레이어 계산 - layout에서 layers가 있으면 사용, 없으면 기본 레이어 생성
|
|
|
|
|
|
// 주의: components는 layout.components에 layerId 속성으로 저장되므로, layer.components는 비워둠
|
|
|
|
|
|
const initialLayers = useMemo<LayerDefinition[]>(() => {
|
|
|
|
|
|
if (layout.layers && layout.layers.length > 0) {
|
|
|
|
|
|
// 기존 레이어 구조 사용 (layer.components는 무시하고 빈 배열로 설정)
|
|
|
|
|
|
return layout.layers.map(layer => ({
|
|
|
|
|
|
...layer,
|
|
|
|
|
|
components: [], // layout.components + layerId 방식 사용
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
// layers가 없으면 기본 레이어 생성 (components는 빈 배열)
|
|
|
|
|
|
return [createDefaultLayer()];
|
|
|
|
|
|
}, [layout.layers]);
|
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
|
if (!selectedScreen) {
|
|
|
|
|
|
return (
|
2025-10-22 17:19:47 +09:00
|
|
|
|
<div className="bg-background flex h-full items-center justify-center">
|
|
|
|
|
|
<div className="space-y-4 text-center">
|
|
|
|
|
|
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
|
|
|
|
|
<Database className="text-muted-foreground h-8 w-8" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 className="text-foreground text-lg font-semibold">화면을 선택하세요</h3>
|
|
|
|
|
|
<p className="text-muted-foreground max-w-sm text-sm">설계할 화면을 먼저 선택해주세요.</p>
|
2025-09-01 14:00:31 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 ScreenDesigner 렌더링 확인 (디버그 완료 - 주석 처리)
|
|
|
|
|
|
// console.log("🏠 ScreenDesigner 렌더!", Date.now());
|
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
|
return (
|
2025-11-10 15:36:18 +09:00
|
|
|
|
<ScreenPreviewProvider isPreviewMode={false}>
|
2026-02-06 09:51:29 +09:00
|
|
|
|
<LayerProvider
|
|
|
|
|
|
initialLayers={initialLayers}
|
|
|
|
|
|
onLayersChange={handleLayersChange}
|
|
|
|
|
|
onActiveLayerChange={handleActiveLayerChange}
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableOptionsProvider>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<div className="bg-background flex h-full w-full flex-col">
|
|
|
|
|
|
{/* 상단 슬림 툴바 */}
|
|
|
|
|
|
<SlimToolbar
|
|
|
|
|
|
screenName={selectedScreen?.screenName}
|
|
|
|
|
|
tableName={selectedScreen?.tableName}
|
|
|
|
|
|
screenResolution={screenResolution}
|
|
|
|
|
|
onBack={onBackToList}
|
|
|
|
|
|
onSave={handleSave}
|
|
|
|
|
|
isSaving={isSaving}
|
2026-02-02 15:15:01 +09:00
|
|
|
|
onPreview={isPop ? handlePopPreview : undefined}
|
2026-01-05 13:28:11 +09:00
|
|
|
|
onResolutionChange={setScreenResolution}
|
|
|
|
|
|
gridSettings={layout.gridSettings}
|
|
|
|
|
|
onGridSettingsChange={updateGridSettings}
|
2026-01-14 10:20:27 +09:00
|
|
|
|
onGenerateMultilang={handleGenerateMultilang}
|
|
|
|
|
|
isGeneratingMultilang={isGeneratingMultilang}
|
2026-01-14 11:51:24 +09:00
|
|
|
|
onOpenMultilangSettings={() => setShowMultilangSettingsModal(true)}
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
isPanelOpen={panelStates.v2?.isOpen || false}
|
|
|
|
|
|
onTogglePanel={() => togglePanel("v2")}
|
2026-02-06 15:18:27 +09:00
|
|
|
|
selectedCount={groupState.selectedComponents.length}
|
|
|
|
|
|
onAlign={handleGroupAlign}
|
|
|
|
|
|
onDistribute={handleGroupDistribute}
|
|
|
|
|
|
onMatchSize={handleMatchSize}
|
|
|
|
|
|
onToggleLabels={handleToggleAllLabels}
|
|
|
|
|
|
onShowShortcuts={() => setShowShortcutsModal(true)}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
/>
|
2026-01-16 09:59:16 +09:00
|
|
|
|
{/* 메인 컨테이너 (패널들 + 캔버스) */}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
2026-01-16 09:59:16 +09:00
|
|
|
|
{/* 통합 패널 - 좌측 사이드바 제거 후 너비 300px로 확장 */}
|
2026-01-28 17:36:19 +09:00
|
|
|
|
{panelStates.v2?.isOpen && (
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
<div className="border-border bg-card flex h-full w-[300px] flex-col overflow-hidden border-r shadow-sm">
|
|
|
|
|
|
<div className="border-border flex shrink-0 items-center justify-between border-b px-4 py-3">
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<h3 className="text-foreground text-sm font-semibold">패널</h3>
|
|
|
|
|
|
<button
|
2026-01-28 17:36:19 +09:00
|
|
|
|
onClick={() => closePanel("v2")}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
✕
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-01 18:50:26 +09:00
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
2026-02-09 13:21:56 +09:00
|
|
|
|
<Tabs value={leftPanelTab} onValueChange={setLeftPanelTab} className="flex min-h-0 flex-1 flex-col">
|
2026-02-06 09:51:29 +09:00
|
|
|
|
<TabsList className="mx-4 mt-2 grid h-8 w-auto grid-cols-3 gap-1">
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<TabsTrigger value="components" className="text-xs">
|
|
|
|
|
|
컴포넌트
|
|
|
|
|
|
</TabsTrigger>
|
2026-02-06 09:51:29 +09:00
|
|
|
|
<TabsTrigger value="layers" className="text-xs">
|
|
|
|
|
|
레이어
|
|
|
|
|
|
</TabsTrigger>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<TabsTrigger value="properties" className="text-xs">
|
|
|
|
|
|
편집
|
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="components" className="mt-0 flex-1 overflow-hidden">
|
|
|
|
|
|
<ComponentsPanel
|
|
|
|
|
|
tables={filteredTables}
|
|
|
|
|
|
searchTerm={searchTerm}
|
|
|
|
|
|
onSearchChange={setSearchTerm}
|
|
|
|
|
|
onTableDragStart={(e, table, column) => {
|
|
|
|
|
|
const dragData = {
|
|
|
|
|
|
type: column ? "column" : "table",
|
|
|
|
|
|
table,
|
|
|
|
|
|
column,
|
|
|
|
|
|
};
|
|
|
|
|
|
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
|
|
|
|
|
}}
|
2026-01-15 15:17:52 +09:00
|
|
|
|
selectedTableName={selectedScreen?.tableName}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
placedColumns={placedColumns}
|
2026-01-15 15:17:52 +09:00
|
|
|
|
onTableSelect={handleTableSelect}
|
|
|
|
|
|
showTableSelector={true}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2026-02-09 13:21:56 +09:00
|
|
|
|
{/* 🆕 레이어 관리 탭 (DB 기반) */}
|
2026-02-06 09:51:29 +09:00
|
|
|
|
<TabsContent value="layers" className="mt-0 flex-1 overflow-hidden">
|
2026-02-09 13:21:56 +09:00
|
|
|
|
<LayerManagerPanel
|
|
|
|
|
|
screenId={selectedScreen?.screenId || null}
|
|
|
|
|
|
activeLayerId={Number(activeLayerIdRef.current) || 1}
|
|
|
|
|
|
onLayerChange={async (layerId) => {
|
|
|
|
|
|
if (!selectedScreen?.screenId) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 현재 레이어 저장
|
|
|
|
|
|
const curId = Number(activeLayerIdRef.current) || 1;
|
|
|
|
|
|
const v2Layout = convertLegacyToV2({ ...layout, screenResolution });
|
|
|
|
|
|
await screenApi.saveLayoutV2(selectedScreen.screenId, { ...v2Layout, layerId: curId });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 새 레이어 로드
|
|
|
|
|
|
const data = await screenApi.getLayerLayout(selectedScreen.screenId, layerId);
|
|
|
|
|
|
if (data && data.components) {
|
|
|
|
|
|
const legacy = convertV2ToLegacy(data);
|
|
|
|
|
|
if (legacy) {
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: legacy.components }));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: [] }));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setLayout((prev) => ({ ...prev, components: [] }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setActiveLayerIdWithRef(layerId);
|
|
|
|
|
|
setSelectedComponent(null);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레이어 전환 실패:", error);
|
|
|
|
|
|
toast.error("레이어 전환에 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
components={layout.components}
|
|
|
|
|
|
/>
|
2026-02-06 09:51:29 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<TabsContent value="properties" className="mt-0 flex-1 overflow-hidden">
|
2026-01-28 17:36:19 +09:00
|
|
|
|
{/* 탭 내부 컴포넌트 선택 시에도 V2PropertiesPanel 사용 */}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
{selectedTabComponentInfo ? (
|
2026-01-21 09:33:44 +09:00
|
|
|
|
(() => {
|
|
|
|
|
|
const tabComp = selectedTabComponentInfo.component;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-21 09:33:44 +09:00
|
|
|
|
// 탭 내부 컴포넌트를 ComponentData 형식으로 변환
|
|
|
|
|
|
const tabComponentAsComponentData: ComponentData = {
|
|
|
|
|
|
id: tabComp.id,
|
|
|
|
|
|
type: "component",
|
|
|
|
|
|
componentType: tabComp.componentType,
|
|
|
|
|
|
label: tabComp.label,
|
|
|
|
|
|
position: tabComp.position || { x: 0, y: 0 },
|
|
|
|
|
|
size: tabComp.size || { width: 200, height: 100 },
|
|
|
|
|
|
componentConfig: tabComp.componentConfig || {},
|
|
|
|
|
|
style: tabComp.style || {},
|
|
|
|
|
|
} as ComponentData;
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 탭 내부 컴포넌트용 속성 업데이트 핸들러 (중첩 구조 지원)
|
2026-01-21 09:33:44 +09:00
|
|
|
|
const updateTabComponentProperty = (componentId: string, path: string, value: any) => {
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } =
|
|
|
|
|
|
selectedTabComponentInfo;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔧 updateTabComponentProperty 호출:", {
|
|
|
|
|
|
componentId,
|
|
|
|
|
|
path,
|
|
|
|
|
|
value,
|
|
|
|
|
|
parentSplitPanelId,
|
|
|
|
|
|
parentPanelSide,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 안전한 깊은 경로 업데이트 헬퍼 함수
|
|
|
|
|
|
const setNestedValue = (obj: any, pathStr: string, val: any): any => {
|
|
|
|
|
|
// 깊은 복사로 시작
|
|
|
|
|
|
const result = JSON.parse(JSON.stringify(obj));
|
|
|
|
|
|
const parts = pathStr.split(".");
|
|
|
|
|
|
let current = result;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
|
|
|
|
const part = parts[i];
|
|
|
|
|
|
if (!current[part] || typeof current[part] !== "object") {
|
|
|
|
|
|
current[part] = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
current = current[part];
|
|
|
|
|
|
}
|
|
|
|
|
|
current[parts[parts.length - 1]] = val;
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 탭 컴포넌트 업데이트 함수
|
|
|
|
|
|
const updateTabsComponent = (tabsComponent: any) => {
|
|
|
|
|
|
const currentConfig = JSON.parse(JSON.stringify(tabsComponent.componentConfig || {}));
|
2026-01-21 09:33:44 +09:00
|
|
|
|
const tabs = currentConfig.tabs || [];
|
|
|
|
|
|
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === tabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: (tab.components || []).map((comp: any) => {
|
|
|
|
|
|
if (comp.id !== componentId) return comp;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 안전한 깊은 경로 업데이트 사용
|
|
|
|
|
|
const updatedComp = setNestedValue(comp, path, value);
|
|
|
|
|
|
console.log("🔧 컴포넌트 업데이트 결과:", updatedComp);
|
|
|
|
|
|
return updatedComp;
|
2026-01-21 09:33:44 +09:00
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
2026-01-21 09:33:44 +09:00
|
|
|
|
...tabsComponent,
|
|
|
|
|
|
componentConfig: { ...currentConfig, tabs: updatedTabs },
|
2026-01-20 10:46:34 +09:00
|
|
|
|
};
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
let newLayout;
|
|
|
|
|
|
let updatedTabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭 업데이트
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
const tabsComponent = panelComponents.find(
|
|
|
|
|
|
(pc: any) => pc.id === tabsComponentId,
|
|
|
|
|
|
);
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (!tabsComponent) return c;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponent(tabsComponent);
|
|
|
|
|
|
updatedTabs = updatedTabsComponent.componentConfig.tabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
pc.id === tabsComponentId ? updatedTabsComponent : pc,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭 업데이트
|
|
|
|
|
|
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
|
|
|
|
|
|
if (!tabsComponent) return prevLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponent(tabsComponent);
|
|
|
|
|
|
updatedTabs = updatedTabsComponent.componentConfig.tabs;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
c.id === tabsComponentId ? updatedTabsComponent : c,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
|
2026-01-21 09:33:44 +09:00
|
|
|
|
// 선택된 컴포넌트 정보 업데이트
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (updatedTabs) {
|
|
|
|
|
|
const updatedComp = updatedTabs
|
|
|
|
|
|
.find((t: any) => t.id === tabId)
|
|
|
|
|
|
?.components?.find((c: any) => c.id === componentId);
|
|
|
|
|
|
if (updatedComp) {
|
|
|
|
|
|
setSelectedTabComponentInfo((prev) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
prev ? { ...prev, component: updatedComp } : null,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-21 09:33:44 +09:00
|
|
|
|
}
|
2026-01-20 14:01:35 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return newLayout;
|
2026-01-21 09:33:44 +09:00
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-20 14:01:35 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 탭 내부 컴포넌트 삭제 핸들러 (중첩 구조 지원)
|
2026-01-21 09:33:44 +09:00
|
|
|
|
const deleteTabComponent = (componentId: string) => {
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } =
|
|
|
|
|
|
selectedTabComponentInfo;
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 탭 컴포넌트에서 특정 컴포넌트 삭제
|
|
|
|
|
|
const updateTabsComponentForDelete = (tabsComponent: any) => {
|
|
|
|
|
|
const currentConfig = tabsComponent.componentConfig || {};
|
2026-01-21 09:33:44 +09:00
|
|
|
|
const tabs = currentConfig.tabs || [];
|
|
|
|
|
|
|
|
|
|
|
|
const updatedTabs = tabs.map((tab: any) => {
|
|
|
|
|
|
if (tab.id === tabId) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...tab,
|
|
|
|
|
|
components: (tab.components || []).filter((c: any) => c.id !== componentId),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return tab;
|
|
|
|
|
|
});
|
2026-01-20 14:01:35 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
2026-01-21 09:33:44 +09:00
|
|
|
|
...tabsComponent,
|
|
|
|
|
|
componentConfig: { ...currentConfig, tabs: updatedTabs },
|
|
|
|
|
|
};
|
2026-02-02 17:11:00 +09:00
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
let newLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (parentSplitPanelId && parentPanelSide) {
|
|
|
|
|
|
// 🆕 중첩 구조: 분할 패널 안의 탭에서 삭제
|
|
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) => {
|
|
|
|
|
|
if (c.id === parentSplitPanelId) {
|
|
|
|
|
|
const splitConfig = (c as any).componentConfig || {};
|
|
|
|
|
|
const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel";
|
|
|
|
|
|
const panelConfig = splitConfig[panelKey] || {};
|
|
|
|
|
|
const panelComponents = panelConfig.components || [];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
const tabsComponent = panelComponents.find(
|
|
|
|
|
|
(pc: any) => pc.id === tabsComponentId,
|
|
|
|
|
|
);
|
2026-02-02 17:11:00 +09:00
|
|
|
|
if (!tabsComponent) return c;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return {
|
|
|
|
|
|
...c,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...splitConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: panelComponents.map((pc: any) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
pc.id === tabsComponentId ? updatedTabsComponent : pc,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 일반 구조: 최상위 탭에서 삭제
|
|
|
|
|
|
const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId);
|
|
|
|
|
|
if (!tabsComponent) return prevLayout;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent);
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
c.id === tabsComponentId ? updatedTabsComponent : c,
|
2026-02-02 17:11:00 +09:00
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
|
2026-01-21 09:33:44 +09:00
|
|
|
|
setSelectedTabComponentInfo(null);
|
2026-02-02 17:11:00 +09:00
|
|
|
|
return newLayout;
|
2026-01-21 09:33:44 +09:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
|
<div className="flex items-center justify-between border-b px-4 py-2">
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
<span className="text-muted-foreground text-xs">탭 내부 컴포넌트</span>
|
2026-01-21 09:33:44 +09:00
|
|
|
|
<button
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
className="text-muted-foreground hover:text-foreground text-xs"
|
2026-01-21 09:33:44 +09:00
|
|
|
|
onClick={() => setSelectedTabComponentInfo(null)}
|
|
|
|
|
|
>
|
|
|
|
|
|
선택 해제
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
2026-01-28 17:36:19 +09:00
|
|
|
|
<V2PropertiesPanel
|
2026-01-21 09:33:44 +09:00
|
|
|
|
selectedComponent={tabComponentAsComponentData}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
tables={tables}
|
2026-01-21 09:33:44 +09:00
|
|
|
|
onUpdateProperty={updateTabComponentProperty}
|
|
|
|
|
|
onDeleteComponent={deleteTabComponent}
|
|
|
|
|
|
currentTable={tables.length > 0 ? tables[0] : undefined}
|
|
|
|
|
|
currentTableName={selectedScreen?.tableName}
|
|
|
|
|
|
currentScreenCompanyCode={selectedScreen?.companyCode}
|
|
|
|
|
|
onStyleChange={(style) => {
|
|
|
|
|
|
updateTabComponentProperty(tabComp.id, "style", style);
|
|
|
|
|
|
}}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
allComponents={layout.components}
|
2026-01-21 09:33:44 +09:00
|
|
|
|
menuObjid={menuObjid}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
/>
|
2026-01-21 09:33:44 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()
|
2026-01-30 16:34:05 +09:00
|
|
|
|
) : selectedPanelComponentInfo ? (
|
|
|
|
|
|
// 🆕 분할 패널 내부 컴포넌트 선택 시 V2PropertiesPanel 사용
|
|
|
|
|
|
(() => {
|
|
|
|
|
|
const panelComp = selectedPanelComponentInfo.component;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 분할 패널 내부 컴포넌트를 ComponentData 형식으로 변환
|
|
|
|
|
|
const panelComponentAsComponentData: ComponentData = {
|
|
|
|
|
|
id: panelComp.id,
|
|
|
|
|
|
type: "component",
|
|
|
|
|
|
componentType: panelComp.componentType,
|
|
|
|
|
|
label: panelComp.label,
|
|
|
|
|
|
position: panelComp.position || { x: 0, y: 0 },
|
|
|
|
|
|
size: panelComp.size || { width: 200, height: 100 },
|
|
|
|
|
|
componentConfig: panelComp.componentConfig || {},
|
|
|
|
|
|
style: panelComp.style || {},
|
|
|
|
|
|
} as ComponentData;
|
|
|
|
|
|
|
|
|
|
|
|
// 분할 패널 내부 컴포넌트용 속성 업데이트 핸들러
|
|
|
|
|
|
const updatePanelComponentProperty = (componentId: string, path: string, value: any) => {
|
|
|
|
|
|
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
|
|
|
|
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("🔧 updatePanelComponentProperty 호출:", {
|
|
|
|
|
|
componentId,
|
|
|
|
|
|
path,
|
|
|
|
|
|
value,
|
|
|
|
|
|
splitPanelId,
|
|
|
|
|
|
panelSide,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 안전한 깊은 경로 업데이트 헬퍼 함수
|
|
|
|
|
|
const setNestedValue = (obj: any, pathStr: string, val: any): any => {
|
|
|
|
|
|
const result = JSON.parse(JSON.stringify(obj));
|
|
|
|
|
|
const parts = pathStr.split(".");
|
|
|
|
|
|
let current = result;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
|
|
|
|
const part = parts[i];
|
|
|
|
|
|
if (!current[part] || typeof current[part] !== "object") {
|
|
|
|
|
|
current[part] = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
current = current[part];
|
|
|
|
|
|
}
|
|
|
|
|
|
current[parts[parts.length - 1]] = val;
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
|
|
|
|
|
if (!splitPanelComponent) return prevLayout;
|
|
|
|
|
|
|
|
|
|
|
|
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
|
|
|
|
|
const panelConfig = currentConfig[panelKey] || {};
|
|
|
|
|
|
const components = panelConfig.components || [];
|
|
|
|
|
|
|
|
|
|
|
|
// 해당 컴포넌트 찾기
|
|
|
|
|
|
const targetCompIndex = components.findIndex((c: any) => c.id === componentId);
|
|
|
|
|
|
if (targetCompIndex === -1) return prevLayout;
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
// 🆕 안전한 깊은 경로 업데이트 사용
|
2026-01-30 16:34:05 +09:00
|
|
|
|
const targetComp = components[targetCompIndex];
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const updatedComp =
|
|
|
|
|
|
path === "style"
|
|
|
|
|
|
? { ...targetComp, style: value }
|
|
|
|
|
|
: setNestedValue(targetComp, path, value);
|
|
|
|
|
|
|
2026-02-02 17:11:00 +09:00
|
|
|
|
console.log("🔧 분할 패널 컴포넌트 업데이트 결과:", updatedComp);
|
2026-01-30 16:34:05 +09:00
|
|
|
|
|
|
|
|
|
|
const updatedComponents = [
|
|
|
|
|
|
...components.slice(0, targetCompIndex),
|
|
|
|
|
|
updatedComp,
|
|
|
|
|
|
...components.slice(targetCompIndex + 1),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...splitPanelComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// selectedPanelComponentInfo 업데이트
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
setSelectedPanelComponentInfo((prev) =>
|
|
|
|
|
|
prev ? { ...prev, component: updatedComp } : null,
|
2026-01-30 16:34:05 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
c.id === splitPanelId ? updatedComponent : c,
|
2026-01-30 16:34:05 +09:00
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 분할 패널 내부 컴포넌트 삭제 핸들러
|
|
|
|
|
|
const deletePanelComponent = (componentId: string) => {
|
|
|
|
|
|
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
|
|
|
|
|
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-01-30 16:34:05 +09:00
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
|
|
|
|
|
if (!splitPanelComponent) return prevLayout;
|
|
|
|
|
|
|
|
|
|
|
|
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
|
|
|
|
|
const panelConfig = currentConfig[panelKey] || {};
|
|
|
|
|
|
const components = panelConfig.components || [];
|
|
|
|
|
|
|
|
|
|
|
|
const updatedComponents = components.filter((c: any) => c.id !== componentId);
|
|
|
|
|
|
|
|
|
|
|
|
const updatedComponent = {
|
|
|
|
|
|
...splitPanelComponent,
|
|
|
|
|
|
componentConfig: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
[panelKey]: {
|
|
|
|
|
|
...panelConfig,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedPanelComponentInfo(null);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: prevLayout.components.map((c) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
c.id === splitPanelId ? updatedComponent : c,
|
2026-01-30 16:34:05 +09:00
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
|
<div className="flex items-center justify-between border-b px-4 py-2">
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
<span className="text-muted-foreground text-xs">
|
|
|
|
|
|
분할 패널 ({selectedPanelComponentInfo.panelSide === "left" ? "좌측" : "우측"})
|
|
|
|
|
|
컴포넌트
|
2026-01-30 16:34:05 +09:00
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
className="text-muted-foreground hover:text-foreground text-xs"
|
2026-01-30 16:34:05 +09:00
|
|
|
|
onClick={() => setSelectedPanelComponentInfo(null)}
|
|
|
|
|
|
>
|
|
|
|
|
|
선택 해제
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
|
|
|
|
<V2PropertiesPanel
|
|
|
|
|
|
selectedComponent={panelComponentAsComponentData}
|
|
|
|
|
|
tables={tables}
|
|
|
|
|
|
onUpdateProperty={updatePanelComponentProperty}
|
|
|
|
|
|
onDeleteComponent={deletePanelComponent}
|
|
|
|
|
|
currentTable={tables.length > 0 ? tables[0] : undefined}
|
|
|
|
|
|
currentTableName={selectedScreen?.tableName}
|
|
|
|
|
|
currentScreenCompanyCode={selectedScreen?.companyCode}
|
|
|
|
|
|
onStyleChange={(style) => {
|
|
|
|
|
|
updatePanelComponentProperty(panelComp.id, "style", style);
|
|
|
|
|
|
}}
|
|
|
|
|
|
allComponents={layout.components}
|
|
|
|
|
|
menuObjid={menuObjid}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()
|
2026-01-20 10:46:34 +09:00
|
|
|
|
) : (
|
2026-01-28 17:36:19 +09:00
|
|
|
|
<V2PropertiesPanel
|
2026-01-20 10:46:34 +09:00
|
|
|
|
selectedComponent={selectedComponent || undefined}
|
|
|
|
|
|
tables={tables}
|
|
|
|
|
|
onUpdateProperty={updateComponentProperty}
|
|
|
|
|
|
onDeleteComponent={deleteComponent}
|
|
|
|
|
|
onCopyComponent={copyComponent}
|
|
|
|
|
|
currentTable={tables.length > 0 ? tables[0] : undefined}
|
|
|
|
|
|
currentTableName={selectedScreen?.tableName}
|
|
|
|
|
|
currentScreenCompanyCode={selectedScreen?.companyCode}
|
|
|
|
|
|
dragState={dragState}
|
|
|
|
|
|
onStyleChange={(style) => {
|
|
|
|
|
|
if (selectedComponent) {
|
|
|
|
|
|
updateComponentProperty(selectedComponent.id, "style", style);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
allComponents={layout.components} // 🆕 플로우 위젯 감지용
|
|
|
|
|
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
{/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - GPU 가속 스크롤 적용 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={canvasContainerRef}
|
|
|
|
|
|
className="bg-muted relative flex-1 overflow-auto px-16 py-6"
|
|
|
|
|
|
style={{ willChange: "scroll-position" }}
|
|
|
|
|
|
>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* Pan 모드 안내 - 제거됨 */}
|
|
|
|
|
|
{/* 줌 레벨 표시 */}
|
|
|
|
|
|
<div className="bg-card text-foreground border-border pointer-events-none fixed right-6 bottom-6 z-50 rounded-lg border px-4 py-2 text-sm font-medium shadow-md">
|
|
|
|
|
|
🔍 {Math.round(zoomLevel * 100)}%
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* 🆕 플로우 버튼 그룹 제어 (버튼 선택 시 표시) */}
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
// 선택된 컴포넌트들
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
// 버튼이 선택되었거나 플로우 그룹 버튼이 있으면 표시
|
|
|
|
|
|
const shouldShow = selectedButtons.length >= 1 && (selectedButtons.length >= 2 || hasFlowGroupButton);
|
2025-10-15 10:24:33 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
if (!shouldShow) return null;
|
2025-10-24 17:27:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="bg-card border-border fixed right-6 bottom-20 z-50 rounded-lg border shadow-lg">
|
|
|
|
|
|
<div className="flex flex-col gap-2 p-3">
|
|
|
|
|
|
<div className="text-muted-foreground mb-1 flex items-center gap-2 text-xs">
|
2025-10-28 15:39:22 +09:00
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
width="14"
|
|
|
|
|
|
height="14"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
|
|
|
|
|
<polyline points="3.29 7 12 12 20.71 7"></polyline>
|
|
|
|
|
|
<line x1="12" y1="22" x2="12" y2="12"></line>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</svg>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<span className="font-medium">{selectedButtons.length}개 버튼 선택됨</span>
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* 그룹 생성 버튼 (2개 이상 선택 시) */}
|
|
|
|
|
|
{selectedButtons.length >= 2 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={handleFlowButtonGroup}
|
|
|
|
|
|
disabled={selectedButtons.length < 2}
|
|
|
|
|
|
className="flex items-center gap-2 text-xs"
|
2025-10-28 15:39:22 +09:00
|
|
|
|
>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
width="14"
|
|
|
|
|
|
height="14"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
>
|
|
|
|
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
|
|
|
|
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
|
|
|
|
<line x1="15" y1="3" x2="15" y2="21"></line>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
플로우 그룹으로 묶기
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 그룹 해제 버튼 (플로우 그룹 버튼이 있으면 항상 표시) */}
|
|
|
|
|
|
{hasFlowGroupButton && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handleFlowButtonUngroup}
|
|
|
|
|
|
className="flex items-center gap-2 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg
|
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
|
width="14"
|
|
|
|
|
|
height="14"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
>
|
|
|
|
|
|
<rect x="3" y="3" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="14" y="3" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="14" y="14" width="7" height="7"></rect>
|
|
|
|
|
|
<rect x="3" y="14" width="7" height="7"></rect>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
그룹 해제
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 상태 표시 */}
|
|
|
|
|
|
{hasFlowGroupButton && <p className="mt-1 text-[10px] text-blue-600">✓ 플로우 그룹 버튼</p>}
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
);
|
|
|
|
|
|
})()}
|
2026-02-09 13:21:56 +09:00
|
|
|
|
{/* 🆕 활성 레이어 인디케이터 (기본 레이어가 아닌 경우 표시) */}
|
|
|
|
|
|
{activeLayerId > 1 && (
|
|
|
|
|
|
<div className="sticky top-0 z-30 flex items-center justify-center gap-2 border-b bg-amber-50 px-4 py-1.5 backdrop-blur-sm dark:bg-amber-950/30">
|
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-amber-500" />
|
|
|
|
|
|
<span className="text-xs font-medium">레이어 {activeLayerId} 편집 중</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-06 15:18:27 +09:00
|
|
|
|
{/* 줌 적용 시 스크롤 영역 확보를 위한 래퍼 - 중앙 정렬 + contain 최적화 */}
|
2025-10-22 14:54:50 +09:00
|
|
|
|
<div
|
2025-11-12 10:48:24 +09:00
|
|
|
|
className="flex justify-center"
|
2025-10-28 15:39:22 +09:00
|
|
|
|
style={{
|
2025-11-12 10:48:24 +09:00
|
|
|
|
width: "100%",
|
|
|
|
|
|
minHeight: screenResolution.height * zoomLevel,
|
2026-02-06 15:18:27 +09:00
|
|
|
|
contain: "layout style", // 레이아웃 재계산 범위 제한
|
2025-10-22 14:54:50 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
|
2025-10-28 15:39:22 +09:00
|
|
|
|
<div
|
2025-11-12 10:48:24 +09:00
|
|
|
|
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,
|
2026-02-06 15:18:27 +09:00
|
|
|
|
transform: `scale3d(${zoomLevel}, ${zoomLevel}, 1)`,
|
2025-11-12 10:48:24 +09:00
|
|
|
|
transformOrigin: "top center", // 중앙 기준으로 스케일
|
2026-02-06 15:18:27 +09:00
|
|
|
|
willChange: "transform", // GPU 가속 레이어 생성
|
|
|
|
|
|
backfaceVisibility: "hidden" as const, // 리페인트 최적화
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<div
|
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
|
className="bg-background relative h-full w-full overflow-visible"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2026-02-09 13:21:56 +09:00
|
|
|
|
onMouseMove={(e) => {
|
|
|
|
|
|
// 영역 이동/리사이즈 처리
|
|
|
|
|
|
if (regionDrag.isDragging || regionDrag.isResizing) {
|
|
|
|
|
|
handleRegionCanvasMouseMove(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseUp={() => {
|
|
|
|
|
|
if (regionDrag.isDragging || regionDrag.isResizing) {
|
|
|
|
|
|
handleRegionCanvasMouseUp();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={() => {
|
|
|
|
|
|
if (regionDrag.isDragging || regionDrag.isResizing) {
|
|
|
|
|
|
handleRegionCanvasMouseUp();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.dataTransfer.dropEffect = "copy";
|
|
|
|
|
|
}}
|
2026-01-16 15:12:22 +09:00
|
|
|
|
onDropCapture={(e) => {
|
|
|
|
|
|
// 캡처 단계에서 드롭 이벤트를 처리하여 자식 요소 드롭도 감지
|
2025-11-12 10:48:24 +09:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
handleDrop(e);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 격자 라인 */}
|
|
|
|
|
|
{gridLines.map((line, index) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="bg-border pointer-events-none absolute"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
left: line.type === "vertical" ? `${line.position}px` : 0,
|
|
|
|
|
|
top: line.type === "horizontal" ? `${line.position}px` : 0,
|
|
|
|
|
|
width: line.type === "vertical" ? "1px" : "100%",
|
|
|
|
|
|
height: line.type === "horizontal" ? "1px" : "100%",
|
|
|
|
|
|
opacity: layout.gridSettings?.gridOpacity || 0.3,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 컴포넌트들 */}
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
// 🆕 플로우 버튼 그룹 감지 및 처리
|
2026-02-06 09:51:29 +09:00
|
|
|
|
// visibleComponents를 사용하여 활성 레이어의 컴포넌트만 표시
|
|
|
|
|
|
const topLevelComponents = visibleComponents.filter((component) => !component.parentId);
|
2025-11-12 10:48:24 +09:00
|
|
|
|
|
|
|
|
|
|
// auto-compact 모드의 버튼들을 그룹별로 묶기
|
|
|
|
|
|
const buttonGroups: Record<string, ComponentData[]> = {};
|
|
|
|
|
|
const processedButtonIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}
|
2025-10-22 14:54:50 +09:00
|
|
|
|
}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹에 속하지 않은 일반 컴포넌트들
|
|
|
|
|
|
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
|
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 렌더링 확인 로그 (디버그 완료 - 주석 처리)
|
|
|
|
|
|
// console.log("🔄 ScreenDesigner 렌더링:", { componentsCount: regularComponents.length, forceRenderTrigger, timestamp: Date.now() });
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<>
|
2026-02-09 13:21:56 +09:00
|
|
|
|
{/* 조건부 레이어 영역 (기본 레이어에서만 표시, DB 기반) */}
|
|
|
|
|
|
{activeLayerId === 1 && Object.entries(layerRegions).map(([layerIdStr, region]) => {
|
|
|
|
|
|
const layerId = Number(layerIdStr);
|
|
|
|
|
|
const resizeHandles = ["nw", "ne", "sw", "se", "n", "s", "e", "w"];
|
|
|
|
|
|
const handleCursors: Record<string, string> = {
|
|
|
|
|
|
nw: "nwse-resize", ne: "nesw-resize", sw: "nesw-resize", se: "nwse-resize",
|
|
|
|
|
|
n: "ns-resize", s: "ns-resize", e: "ew-resize", w: "ew-resize",
|
|
|
|
|
|
};
|
|
|
|
|
|
const handlePositions: Record<string, React.CSSProperties> = {
|
|
|
|
|
|
nw: { top: -4, left: -4 }, ne: { top: -4, right: -4 },
|
|
|
|
|
|
sw: { bottom: -4, left: -4 }, se: { bottom: -4, right: -4 },
|
|
|
|
|
|
n: { top: -4, left: "50%", transform: "translateX(-50%)" },
|
|
|
|
|
|
s: { bottom: -4, left: "50%", transform: "translateX(-50%)" },
|
|
|
|
|
|
e: { top: "50%", right: -4, transform: "translateY(-50%)" },
|
|
|
|
|
|
w: { top: "50%", left: -4, transform: "translateY(-50%)" },
|
|
|
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`region-${layerId}`}
|
|
|
|
|
|
className="absolute"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
left: `${region.x}px`,
|
|
|
|
|
|
top: `${region.y}px`,
|
|
|
|
|
|
width: `${region.width}px`,
|
|
|
|
|
|
height: `${region.height}px`,
|
|
|
|
|
|
border: "2px dashed hsl(var(--primary))",
|
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
|
backgroundColor: "hsl(var(--primary) / 0.05)",
|
|
|
|
|
|
zIndex: 9999,
|
|
|
|
|
|
cursor: "move",
|
|
|
|
|
|
pointerEvents: "auto",
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseDown={(e) => handleRegionMouseDown(e, String(layerId), "move")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="pointer-events-none absolute left-2 top-1 select-none text-[10px] font-medium text-primary">
|
|
|
|
|
|
레이어 {layerId} - {region.layerName}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{/* 리사이즈 핸들 */}
|
|
|
|
|
|
{resizeHandles.map((handle) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={handle}
|
|
|
|
|
|
className="absolute z-10 h-2 w-2 rounded-sm border border-primary bg-background"
|
|
|
|
|
|
style={{ ...handlePositions[handle], cursor: handleCursors[handle] }}
|
|
|
|
|
|
onMouseDown={(e) => handleRegionMouseDown(e, String(layerId), "resize", handle)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{/* 삭제 버튼 */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="absolute -right-1 -top-3 flex h-4 w-4 items-center justify-center rounded-full bg-destructive text-[8px] text-destructive-foreground hover:bg-destructive/80"
|
|
|
|
|
|
onClick={async (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
if (!selectedScreen?.screenId) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const layerData = await screenApi.getLayerLayout(selectedScreen.screenId, layerId);
|
|
|
|
|
|
const cond = layerData?.conditionConfig || {};
|
|
|
|
|
|
delete cond.displayRegion;
|
|
|
|
|
|
await screenApi.updateLayerCondition(selectedScreen.screenId, layerId, Object.keys(cond).length > 0 ? cond : null);
|
|
|
|
|
|
setLayerRegions((prev) => {
|
|
|
|
|
|
const next = { ...prev };
|
|
|
|
|
|
delete next[layerId];
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch { toast.error("영역 삭제 실패"); }
|
|
|
|
|
|
}}
|
|
|
|
|
|
title="영역 삭제"
|
|
|
|
|
|
>
|
|
|
|
|
|
x
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* 일반 컴포넌트들 */}
|
|
|
|
|
|
{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) {
|
|
|
|
|
|
// 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트
|
2025-10-28 15:39:22 +09:00
|
|
|
|
displayComponent = {
|
|
|
|
|
|
...component,
|
2025-11-12 10:48:24 +09:00
|
|
|
|
position: dragState.currentPosition,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
style: {
|
|
|
|
|
|
...component.style,
|
|
|
|
|
|
opacity: 0.8,
|
2025-11-12 10:48:24 +09:00
|
|
|
|
transform: "scale(1.02)",
|
2025-10-28 15:39:22 +09:00
|
|
|
|
transition: "none",
|
2025-11-12 10:48:24 +09:00
|
|
|
|
zIndex: 50,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2025-11-12 10:48:24 +09:00
|
|
|
|
} 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, // 주 컴포넌트보다 약간 낮게
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}
|
2025-10-22 14:54:50 +09:00
|
|
|
|
}
|
2025-10-24 10:37:02 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
// 전역 파일 상태도 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}`;
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🆕 style 변경 시 리렌더링을 위한 key 추가
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
const styleKey =
|
|
|
|
|
|
component.style?.labelDisplay !== undefined
|
|
|
|
|
|
? `label-${component.style.labelDisplay}`
|
|
|
|
|
|
: "";
|
2026-02-04 18:01:20 +09:00
|
|
|
|
const fullKey = `${component.id}-${fileStateKey}-${styleKey}-${(component as any).lastFileUpdate || 0}-${forceRenderTrigger}`;
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
|
2026-02-04 18:01:20 +09:00
|
|
|
|
// 🔧 v2-input 계열 컴포넌트 key 변경 로그 (디버그 완료 - 주석 처리)
|
|
|
|
|
|
// if (component.id.includes("v2-") || component.widgetType?.includes("v2-")) { console.log("🔑 RealtimePreview key:", { id: component.id, styleKey, labelDisplay: component.style?.labelDisplay, forceRenderTrigger, fullKey }); }
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 labelDisplay 변경 시 새 객체로 강제 변경 감지
|
|
|
|
|
|
const componentWithLabel = {
|
|
|
|
|
|
...displayComponent,
|
|
|
|
|
|
_labelDisplayKey: component.style?.labelDisplay,
|
|
|
|
|
|
};
|
2025-11-12 10:48:24 +09:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<RealtimePreview
|
2026-02-04 18:01:20 +09:00
|
|
|
|
key={fullKey}
|
|
|
|
|
|
component={componentWithLabel}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
isSelected={
|
|
|
|
|
|
selectedComponent?.id === component.id ||
|
|
|
|
|
|
groupState.selectedComponents.includes(component.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
isDesignMode={true} // 편집 모드로 설정
|
|
|
|
|
|
onClick={(e) => handleComponentClick(component, e)}
|
|
|
|
|
|
onDoubleClick={(e) => handleComponentDoubleClick(component, e)}
|
|
|
|
|
|
onDragStart={(e) => startComponentDrag(component, e)}
|
|
|
|
|
|
onDragEnd={endDrag}
|
|
|
|
|
|
selectedScreen={selectedScreen}
|
2026-02-06 09:15:50 +09:00
|
|
|
|
tableName={selectedScreen?.tableName} // 🆕 디자인 모드에서도 옵션 로딩을 위해 전달
|
2025-11-12 10:48:24 +09:00
|
|
|
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
});
|
2025-10-24 10:37:02 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
console.log("✅ 컴포넌트 설정 업데이트 완료:", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
updatedConfig: config,
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 🆕 컴포넌트 전체 업데이트 핸들러 (탭 내부 컴포넌트 위치 조정 등)
|
|
|
|
|
|
onUpdateComponent={(updatedComponent) => {
|
|
|
|
|
|
const updatedComponents = layout.components.map((comp) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
comp.id === updatedComponent.id ? updatedComponent : comp,
|
2026-01-20 10:46:34 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setLayout(newLayout);
|
|
|
|
|
|
saveToHistory(newLayout);
|
|
|
|
|
|
}}
|
2026-01-21 09:33:44 +09:00
|
|
|
|
// 🆕 리사이즈 핸들러 (10px 스냅 적용됨)
|
|
|
|
|
|
onResize={(componentId, newSize) => {
|
|
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const updatedComponents = prevLayout.components.map((comp) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
comp.id === componentId ? { ...comp, size: newSize } : comp,
|
2026-01-21 09:33:44 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// saveToHistory는 별도로 호출 (prevLayout 기반)
|
|
|
|
|
|
setTimeout(() => saveToHistory(newLayout), 0);
|
|
|
|
|
|
return newLayout;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2026-01-20 10:46:34 +09:00
|
|
|
|
// 🆕 탭 내부 컴포넌트 선택 핸들러
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
onSelectTabComponent={(tabId, compId, comp) =>
|
2026-01-20 10:46:34 +09:00
|
|
|
|
handleSelectTabComponent(component.id, tabId, compId, comp)
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedTabComponentId={
|
|
|
|
|
|
selectedTabComponentInfo?.tabsComponentId === component.id
|
|
|
|
|
|
? selectedTabComponentInfo.componentId
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
}
|
2026-01-30 16:34:05 +09:00
|
|
|
|
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
onSelectPanelComponent={(panelSide, compId, comp) =>
|
2026-01-30 16:34:05 +09:00
|
|
|
|
handleSelectPanelComponent(component.id, panelSide, compId, comp)
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedPanelComponentId={
|
|
|
|
|
|
selectedPanelComponentInfo?.splitPanelId === component.id
|
|
|
|
|
|
? selectedPanelComponentInfo.componentId
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
>
|
2025-11-25 15:22:50 +09:00
|
|
|
|
{/* 컨테이너, 그룹, 영역, 컴포넌트의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{(component.type === "group" ||
|
|
|
|
|
|
component.type === "container" ||
|
2025-11-25 15:22:50 +09:00
|
|
|
|
component.type === "area" ||
|
|
|
|
|
|
component.type === "component") &&
|
2025-11-12 10:48:24 +09:00
|
|
|
|
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 (
|
|
|
|
|
|
<RealtimePreview
|
|
|
|
|
|
key={`${child.id}-${(child as any).uploadedFiles?.length || 0}-${JSON.stringify((child as any).uploadedFiles?.map((f: any) => 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}
|
2026-02-06 09:15:50 +09:00
|
|
|
|
tableName={selectedScreen?.tableName} // 🆕 디자인 모드에서도 옵션 로딩을 위해 전달
|
2025-11-12 10:48:24 +09:00
|
|
|
|
// onZoneComponentDrop 제거
|
|
|
|
|
|
onZoneClick={handleZoneClick}
|
|
|
|
|
|
// 설정 변경 핸들러 (자식 컴포넌트용)
|
|
|
|
|
|
onConfigChange={(config) => {
|
|
|
|
|
|
// console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config);
|
|
|
|
|
|
// TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요
|
|
|
|
|
|
}}
|
2026-01-21 09:33:44 +09:00
|
|
|
|
// 🆕 자식 컴포넌트 리사이즈 핸들러
|
|
|
|
|
|
onResize={(componentId, newSize) => {
|
|
|
|
|
|
setLayout((prevLayout) => {
|
|
|
|
|
|
const updatedComponents = prevLayout.components.map((comp) =>
|
refactor: 코드 정리 및 불필요한 로그 제거
- scheduleService.ts에서 스케줄 생성 로직을 간소화하고, 불필요한 줄바꿈을 제거하여 가독성을 향상시켰습니다.
- v2-sales-order-modal-layout.json에서 JSON 포맷을 정리하여 일관성을 유지했습니다.
- page.tsx, ScreenModal.tsx, ScreenDesigner.tsx, V2Input.tsx, V2Select.tsx, V2SelectConfigPanel.tsx, SimpleRepeaterTableComponent.tsx, ButtonPrimaryComponent.tsx, FileUploadComponent.tsx 등 여러 파일에서 디버깅 로그를 제거하여 코드의 깔끔함을 유지했습니다.
- 전반적으로 코드의 가독성을 높이고, 불필요한 로그를 제거하여 유지보수성을 개선했습니다.
2026-02-05 17:35:13 +09:00
|
|
|
|
comp.id === componentId ? { ...comp, size: newSize } : comp,
|
2026-01-21 09:33:44 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newLayout = {
|
|
|
|
|
|
...prevLayout,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => saveToHistory(newLayout), 0);
|
|
|
|
|
|
return newLayout;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</RealtimePreview>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 🆕 플로우 버튼 그룹들 */}
|
|
|
|
|
|
{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 (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`flow-button-group-${groupId}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: "absolute",
|
|
|
|
|
|
left: `${groupPosition.x}px`,
|
|
|
|
|
|
top: `${groupPosition.y}px`,
|
|
|
|
|
|
zIndex: groupPosition.z,
|
|
|
|
|
|
width: `${groupWidth}px`, // 🆕 명시적 너비
|
|
|
|
|
|
height: `${groupHeight}px`, // 🆕 명시적 높이
|
|
|
|
|
|
pointerEvents: "none", // 그룹 컨테이너는 이벤트 차단하여 개별 버튼 클릭 가능
|
|
|
|
|
|
}}
|
|
|
|
|
|
className={hasAnySelected ? "rounded outline-2 outline-offset-2 outline-blue-500" : ""}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FlowButtonGroup
|
|
|
|
|
|
buttons={buttons}
|
|
|
|
|
|
groupConfig={groupConfig}
|
|
|
|
|
|
isDesignMode={true}
|
|
|
|
|
|
renderButton={(button, isVisible) => {
|
|
|
|
|
|
// 드래그 피드백
|
|
|
|
|
|
const isDraggingThis =
|
|
|
|
|
|
dragState.isDragging && dragState.draggedComponent?.id === button.id;
|
|
|
|
|
|
const isBeingDragged =
|
2025-10-28 15:39:22 +09:00
|
|
|
|
dragState.isDragging &&
|
2025-11-12 10:48:24 +09:00
|
|
|
|
dragState.draggedComponents.some((dragComp) => dragComp.id === button.id);
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
let displayButton = button;
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
if (isBeingDragged) {
|
|
|
|
|
|
if (isDraggingThis) {
|
|
|
|
|
|
displayButton = {
|
|
|
|
|
|
...button,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
position: dragState.currentPosition,
|
|
|
|
|
|
style: {
|
2025-11-12 10:48:24 +09:00
|
|
|
|
...button.style,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
opacity: 0.8,
|
|
|
|
|
|
transform: "scale(1.02)",
|
|
|
|
|
|
transition: "none",
|
|
|
|
|
|
zIndex: 50,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
// 🔧 그룹 내부에서는 상대 위치 사용 (wrapper로 처리)
|
|
|
|
|
|
const relativeButton = {
|
|
|
|
|
|
...displayButton,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
position: {
|
2025-11-12 10:48:24 +09:00
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
z: displayButton.position.z || 1,
|
2025-10-28 15:39:22 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<div
|
|
|
|
|
|
key={button.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: "relative",
|
|
|
|
|
|
opacity: isVisible ? 1 : 0.5,
|
|
|
|
|
|
display: "inline-block",
|
|
|
|
|
|
width: button.size?.width || 100,
|
|
|
|
|
|
height: button.size?.height || 40,
|
|
|
|
|
|
pointerEvents: "auto", // 개별 버튼은 이벤트 활성화
|
|
|
|
|
|
cursor: "pointer",
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
|
// 클릭이 아닌 드래그인 경우에만 드래그 시작
|
|
|
|
|
|
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);
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
};
|
2025-10-28 15:39:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
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,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
handleComponentClick(button, e);
|
2025-10-28 15:39:22 +09:00
|
|
|
|
}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
};
|
2025-10-24 17:27:22 +09:00
|
|
|
|
|
2025-11-12 10:48:24 +09:00
|
|
|
|
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로 직접 렌더링 */}
|
|
|
|
|
|
<div style={{ width: "100%", height: "100%", pointerEvents: "none" }}>
|
|
|
|
|
|
<DynamicComponentRenderer
|
|
|
|
|
|
component={relativeButton}
|
|
|
|
|
|
isDesignMode={true}
|
|
|
|
|
|
formData={{}}
|
2026-02-06 09:15:50 +09:00
|
|
|
|
tableName={selectedScreen?.tableName}
|
2025-11-12 10:48:24 +09:00
|
|
|
|
onDataflowComplete={() => {}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 드래그 선택 영역 */}
|
|
|
|
|
|
{selectionDrag.isSelecting && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="border-primary bg-primary/5 pointer-events-none absolute rounded-md border-2 border-dashed"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
left: `${Math.min(selectionDrag.startPoint.x, selectionDrag.currentPoint.x)}px`,
|
|
|
|
|
|
top: `${Math.min(selectionDrag.startPoint.y, selectionDrag.currentPoint.y)}px`,
|
|
|
|
|
|
width: `${Math.abs(selectionDrag.currentPoint.x - selectionDrag.startPoint.x)}px`,
|
|
|
|
|
|
height: `${Math.abs(selectionDrag.currentPoint.y - selectionDrag.startPoint.y)}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 빈 캔버스 안내 */}
|
|
|
|
|
|
{layout.components.length === 0 && (
|
|
|
|
|
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
|
|
|
|
<div className="max-w-2xl space-y-4 px-6 text-center">
|
|
|
|
|
|
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
|
|
|
|
|
<Database className="text-muted-foreground h-8 w-8" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 className="text-foreground text-xl font-semibold">캔버스가 비어있습니다</h3>
|
|
|
|
|
|
<p className="text-muted-foreground text-sm">
|
|
|
|
|
|
좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</p>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
<div className="text-muted-foreground space-y-2 text-xs">
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<span className="font-medium">단축키:</span> T(테이블), M(템플릿), P(속성), S(스타일),
|
|
|
|
|
|
R(격자), D(상세설정), E(해상도)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<span className="font-medium">편집:</span> Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장),
|
|
|
|
|
|
Ctrl+Z(실행취소), Delete(삭제)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-warning flex items-center justify-center gap-2">
|
|
|
|
|
|
<span>⚠️</span>
|
|
|
|
|
|
<span>브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-10-22 17:19:47 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</div>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
</div>{" "}
|
|
|
|
|
|
{/* 🔥 줌 래퍼 닫기 */}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>{" "}
|
|
|
|
|
|
{/* 메인 컨테이너 닫기 */}
|
|
|
|
|
|
{/* 🆕 플로우 버튼 그룹 생성 다이얼로그 */}
|
|
|
|
|
|
<FlowButtonGroupDialog
|
|
|
|
|
|
open={groupDialogOpen}
|
|
|
|
|
|
onOpenChange={setGroupDialogOpen}
|
|
|
|
|
|
buttonCount={groupState.selectedComponents.length}
|
|
|
|
|
|
onConfirm={handleGroupConfirm}
|
2025-10-28 15:39:22 +09:00
|
|
|
|
/>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
{/* 모달들 */}
|
|
|
|
|
|
{/* 메뉴 할당 모달 */}
|
|
|
|
|
|
{showMenuAssignmentModal && selectedScreen && (
|
|
|
|
|
|
<MenuAssignmentModal
|
|
|
|
|
|
screenInfo={selectedScreen}
|
|
|
|
|
|
isOpen={showMenuAssignmentModal}
|
|
|
|
|
|
onClose={() => setShowMenuAssignmentModal(false)}
|
|
|
|
|
|
onAssignmentComplete={() => {
|
|
|
|
|
|
// 모달을 즉시 닫지 않고, MenuAssignmentModal이 3초 후 자동으로 닫히도록 함
|
|
|
|
|
|
// setShowMenuAssignmentModal(false);
|
|
|
|
|
|
// toast.success("메뉴에 화면이 할당되었습니다.");
|
|
|
|
|
|
}}
|
|
|
|
|
|
onBackToList={onBackToList}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 파일첨부 상세 모달 */}
|
|
|
|
|
|
{showFileAttachmentModal && selectedFileComponent && (
|
|
|
|
|
|
<FileAttachmentDetailModal
|
|
|
|
|
|
isOpen={showFileAttachmentModal}
|
|
|
|
|
|
onClose={() => {
|
|
|
|
|
|
setShowFileAttachmentModal(false);
|
|
|
|
|
|
setSelectedFileComponent(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
component={selectedFileComponent}
|
|
|
|
|
|
screenId={selectedScreen.screenId}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-01-14 11:51:24 +09:00
|
|
|
|
{/* 다국어 설정 모달 */}
|
|
|
|
|
|
<MultilangSettingsModal
|
|
|
|
|
|
isOpen={showMultilangSettingsModal}
|
|
|
|
|
|
onClose={() => setShowMultilangSettingsModal(false)}
|
|
|
|
|
|
components={layout.components}
|
2026-01-14 13:08:44 +09:00
|
|
|
|
onSave={async (updates) => {
|
|
|
|
|
|
if (updates.length === 0) {
|
|
|
|
|
|
toast.info("저장할 변경사항이 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 공통 유틸 사용하여 매핑 적용
|
|
|
|
|
|
const { applyMultilangMappings } = await import("@/lib/utils/multilangLabelExtractor");
|
|
|
|
|
|
|
|
|
|
|
|
// 매핑 형식 변환
|
|
|
|
|
|
const mappings = updates.map((u) => ({
|
|
|
|
|
|
componentId: u.componentId,
|
|
|
|
|
|
keyId: u.langKeyId,
|
|
|
|
|
|
langKey: u.langKey,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 레이아웃 업데이트
|
|
|
|
|
|
const updatedComponents = applyMultilangMappings(layout.components, mappings);
|
|
|
|
|
|
setLayout((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
components: updatedComponents,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${updates.length}개 항목의 다국어 설정이 저장되었습니다.`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("다국어 설정 저장 실패:", error);
|
|
|
|
|
|
toast.error("다국어 설정 저장 중 오류가 발생했습니다.");
|
|
|
|
|
|
}
|
2026-01-14 11:51:24 +09:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-02-06 15:18:27 +09:00
|
|
|
|
{/* 단축키 도움말 모달 */}
|
|
|
|
|
|
<KeyboardShortcutsModal
|
|
|
|
|
|
isOpen={showShortcutsModal}
|
|
|
|
|
|
onClose={() => setShowShortcutsModal(false)}
|
|
|
|
|
|
/>
|
2025-11-12 10:48:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</TableOptionsProvider>
|
2026-02-06 09:51:29 +09:00
|
|
|
|
</LayerProvider>
|
2025-10-28 15:39:22 +09:00
|
|
|
|
</ScreenPreviewProvider>
|
2025-09-01 11:48:12 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|