2026-02-02 15:15:01 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
import React, { useEffect, useState } from "react";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { useParams, useSearchParams } from "next/navigation";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-03-09 12:16:26 +09:00
|
|
|
import { Loader2, ArrowLeft, Smartphone, Tablet, RotateCcw, RotateCw, LayoutGrid, Monitor } from "lucide-react";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { screenApi } from "@/lib/api/screen";
|
2026-02-05 14:24:14 +09:00
|
|
|
import { ScreenDefinition } from "@/types/screen";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
import { toast } from "sonner";
|
2026-03-03 16:04:11 +09:00
|
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
|
|
|
|
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
|
|
|
|
import { ScreenContextProvider } from "@/contexts/ScreenContext";
|
|
|
|
|
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
|
|
|
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
2026-02-03 19:11:03 +09:00
|
|
|
import {
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
PopLayoutData,
|
2026-02-05 14:24:14 +09:00
|
|
|
GridMode,
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
isPopLayout,
|
|
|
|
|
createEmptyLayout,
|
2026-02-06 15:30:57 +09:00
|
|
|
GAP_PRESETS,
|
|
|
|
|
GRID_BREAKPOINTS,
|
feat: V6 정사각형 블록 그리드 시스템 실험 구현
고정 칸 수(4/6/8/12) 기반의 V5 그리드를 24px 정사각형 블록 기반의
동적 칸 수 시스템으로 교체한다. 뷰포트 너비에 따라 블록 수가 자동
계산되며(375px=13칸, 1024px=38칸), 작은 화면에서는 행 그룹 리플로우
(CSS Flexbox wrap 원리)로 자동 재배치된다.
[그리드 코어]
- pop-layout.ts: BLOCK_SIZE=24, BLOCK_GAP=2, BLOCK_PADDING=8,
getBlockColumns() 동적 칸 수 계산, GRID_BREAKPOINTS V6 값
- gridUtils.ts: 행 그룹 리플로우(방식 F) - 같은 행 묶음 처리,
최소 2x2칸 터치 보장, 메인 컨텐츠 전체 너비 확장
- PopRenderer.tsx: repeat(N, 1fr) 블록 렌더링, 동적 칸 수
- PopCanvas.tsx: 뷰포트 프리셋 동적 칸 수, 블록 좌표 변환
[V5→V6 런타임 변환]
- convertV5LayoutToV6: DB 미수정, 로드 시 메모리 변환
12칸 좌표 → 38칸 블록 변환, V5 overrides 제거
- PopDesigner/page.tsx: 로드 지점에 변환 함수 삽입
[충돌 해결]
- ComponentEditorPanel: 높이 표시/모드 라벨 V6 수치
- PopCardListConfig: 카드 추천 threshold V6 기준
- PopDesigner: handleHideComponent 기본 모드 제한 해제
[기본 사이즈]
- 소형(2x2): 아이콘, 프로필, 스캐너
- 중형(8x4): 검색, 버튼, 텍스트
- 대형(19x6~10): 카드, 대시보드, 필드
DB 변경 0건, 백엔드 변경 0건, 컴포넌트 코드 변경 0건.
2026-03-13 16:03:24 +09:00
|
|
|
BLOCK_GAP,
|
|
|
|
|
BLOCK_PADDING,
|
2026-02-06 15:30:57 +09:00
|
|
|
detectGridMode,
|
2026-02-03 19:11:03 +09:00
|
|
|
} from "@/components/pop/designer/types/pop-layout";
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
import { loadLegacyLayout } from "@/components/pop/designer/utils/legacyLoader";
|
2026-02-09 19:31:46 +09:00
|
|
|
// POP 컴포넌트 자동 등록 (레지스트리 초기화 - PopRenderer보다 먼저 import)
|
|
|
|
|
import "@/lib/registry/pop-components";
|
2026-02-23 13:54:49 +09:00
|
|
|
import PopViewerWithModals from "@/components/pop/viewer/PopViewerWithModals";
|
2026-02-03 19:11:03 +09:00
|
|
|
import {
|
|
|
|
|
useResponsiveModeWithOverride,
|
|
|
|
|
type DeviceType,
|
|
|
|
|
} from "@/hooks/useDeviceOrientation";
|
2026-02-02 15:15:01 +09:00
|
|
|
|
2026-02-06 15:30:57 +09:00
|
|
|
// 디바이스별 크기 (너비만, 높이는 콘텐츠 기반)
|
|
|
|
|
const DEVICE_SIZES: Record<DeviceType, Record<"landscape" | "portrait", { width: number; label: string }>> = {
|
2026-02-03 19:11:03 +09:00
|
|
|
mobile: {
|
2026-02-06 15:30:57 +09:00
|
|
|
landscape: { width: 600, label: "모바일 가로" },
|
|
|
|
|
portrait: { width: 375, label: "모바일 세로" },
|
2026-02-03 19:11:03 +09:00
|
|
|
},
|
|
|
|
|
tablet: {
|
2026-02-06 15:30:57 +09:00
|
|
|
landscape: { width: 1024, label: "태블릿 가로" },
|
|
|
|
|
portrait: { width: 820, label: "태블릿 세로" },
|
2026-02-03 19:11:03 +09:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
// 모드 키 변환
|
|
|
|
|
const getModeKey = (device: DeviceType, isLandscape: boolean): GridMode => {
|
2026-02-03 19:11:03 +09:00
|
|
|
if (device === "tablet") {
|
|
|
|
|
return isLandscape ? "tablet_landscape" : "tablet_portrait";
|
|
|
|
|
}
|
|
|
|
|
return isLandscape ? "mobile_landscape" : "mobile_portrait";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
// 메인 컴포넌트 (v5 그리드 시스템 전용)
|
2026-02-03 19:11:03 +09:00
|
|
|
// ========================================
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
function PopScreenViewPage() {
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const screenId = parseInt(params.screenId as string);
|
|
|
|
|
|
|
|
|
|
const isPreviewMode = searchParams.get("preview") === "true";
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
// 반응형 모드 감지 (화면 크기에 따라 tablet/mobile, landscape/portrait 자동 전환)
|
|
|
|
|
// 프리뷰 모드에서는 수동 전환 가능
|
|
|
|
|
const { mode, setDevice, setOrientation, isAutoDetect } = useResponsiveModeWithOverride(
|
|
|
|
|
isPreviewMode ? "tablet" : undefined,
|
|
|
|
|
isPreviewMode ? true : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 현재 모드 정보
|
|
|
|
|
const deviceType = mode.device;
|
|
|
|
|
const isLandscape = mode.isLandscape;
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
const { user } = useAuth();
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
|
|
|
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
const [layout, setLayout] = useState<PopLayoutData>(createEmptyLayout());
|
2026-02-02 15:15:01 +09:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
2026-02-04 14:14:48 +09:00
|
|
|
// 뷰포트 너비 (클라이언트 사이드에서만 계산, 최대 1366px)
|
|
|
|
|
const [viewportWidth, setViewportWidth] = useState(1024); // 기본값: 태블릿 가로
|
|
|
|
|
|
2026-02-06 15:30:57 +09:00
|
|
|
// 모드 결정:
|
|
|
|
|
// - 프리뷰 모드: 수동 선택한 device/orientation 사용
|
|
|
|
|
// - 일반 모드: 화면 너비 기준으로 자동 결정 (GRID_BREAKPOINTS와 일치)
|
|
|
|
|
const currentModeKey = isPreviewMode
|
|
|
|
|
? getModeKey(deviceType, isLandscape)
|
|
|
|
|
: detectGridMode(viewportWidth);
|
|
|
|
|
|
2026-02-04 14:14:48 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
const updateViewportWidth = () => {
|
|
|
|
|
setViewportWidth(Math.min(window.innerWidth, 1366));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
updateViewportWidth();
|
|
|
|
|
window.addEventListener("resize", updateViewportWidth);
|
|
|
|
|
return () => window.removeEventListener("resize", updateViewportWidth);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
// 화면 및 POP 레이아웃 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadScreen = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
const screenData = await screenApi.getScreen(screenId);
|
|
|
|
|
setScreen(screenData);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const popLayout = await screenApi.getLayoutPop(screenId);
|
|
|
|
|
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
if (popLayout && isPopLayout(popLayout)) {
|
|
|
|
|
const v6Layout = loadLegacyLayout(popLayout);
|
feat: V6 정사각형 블록 그리드 시스템 실험 구현
고정 칸 수(4/6/8/12) 기반의 V5 그리드를 24px 정사각형 블록 기반의
동적 칸 수 시스템으로 교체한다. 뷰포트 너비에 따라 블록 수가 자동
계산되며(375px=13칸, 1024px=38칸), 작은 화면에서는 행 그룹 리플로우
(CSS Flexbox wrap 원리)로 자동 재배치된다.
[그리드 코어]
- pop-layout.ts: BLOCK_SIZE=24, BLOCK_GAP=2, BLOCK_PADDING=8,
getBlockColumns() 동적 칸 수 계산, GRID_BREAKPOINTS V6 값
- gridUtils.ts: 행 그룹 리플로우(방식 F) - 같은 행 묶음 처리,
최소 2x2칸 터치 보장, 메인 컨텐츠 전체 너비 확장
- PopRenderer.tsx: repeat(N, 1fr) 블록 렌더링, 동적 칸 수
- PopCanvas.tsx: 뷰포트 프리셋 동적 칸 수, 블록 좌표 변환
[V5→V6 런타임 변환]
- convertV5LayoutToV6: DB 미수정, 로드 시 메모리 변환
12칸 좌표 → 38칸 블록 변환, V5 overrides 제거
- PopDesigner/page.tsx: 로드 지점에 변환 함수 삽입
[충돌 해결]
- ComponentEditorPanel: 높이 표시/모드 라벨 V6 수치
- PopCardListConfig: 카드 추천 threshold V6 기준
- PopDesigner: handleHideComponent 기본 모드 제한 해제
[기본 사이즈]
- 소형(2x2): 아이콘, 프로필, 스캐너
- 중형(8x4): 검색, 버튼, 텍스트
- 대형(19x6~10): 카드, 대시보드, 필드
DB 변경 0건, 백엔드 변경 0건, 컴포넌트 코드 변경 0건.
2026-03-13 16:03:24 +09:00
|
|
|
setLayout(v6Layout);
|
2026-02-04 14:14:48 +09:00
|
|
|
const componentCount = Object.keys(popLayout.components).length;
|
2026-02-05 14:24:14 +09:00
|
|
|
console.log(`[POP] v5 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
|
|
|
|
|
} else if (popLayout) {
|
|
|
|
|
// 다른 버전 레이아웃은 빈 v5로 처리
|
|
|
|
|
console.log("[POP] 레거시 레이아웃 감지, 빈 레이아웃으로 시작합니다:", popLayout.version);
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
setLayout(createEmptyLayout());
|
2026-02-02 15:15:01 +09:00
|
|
|
} else {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.log("[POP] 레이아웃 없음");
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
setLayout(createEmptyLayout());
|
2026-02-02 15:15:01 +09:00
|
|
|
}
|
|
|
|
|
} catch (layoutError) {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.warn("[POP] 레이아웃 로드 실패:", layoutError);
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
setLayout(createEmptyLayout());
|
2026-02-02 15:15:01 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.error("[POP] 화면 로드 실패:", error);
|
2026-02-02 15:15:01 +09:00
|
|
|
setError("화면을 불러오는데 실패했습니다.");
|
2026-03-03 16:04:11 +09:00
|
|
|
showErrorToast("POP 화면을 불러오는 데 실패했습니다", error, { guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요." });
|
2026-02-02 15:15:01 +09:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (screenId) {
|
|
|
|
|
loadScreen();
|
|
|
|
|
}
|
|
|
|
|
}, [screenId]);
|
|
|
|
|
|
2026-02-24 15:54:57 +09:00
|
|
|
// 뷰어 모드에서도 컴포넌트 크기 변경 지원 (더보기 등)
|
|
|
|
|
const handleRequestResize = React.useCallback((componentId: string, newRowSpan: number, newColSpan?: number) => {
|
|
|
|
|
setLayout((prev) => {
|
|
|
|
|
const comp = prev.components[componentId];
|
|
|
|
|
if (!comp) return prev;
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
components: {
|
|
|
|
|
...prev.components,
|
|
|
|
|
[componentId]: {
|
|
|
|
|
...comp,
|
|
|
|
|
position: {
|
|
|
|
|
...comp.position,
|
|
|
|
|
rowSpan: newRowSpan,
|
|
|
|
|
...(newColSpan !== undefined ? { colSpan: newColSpan } : {}),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
const currentDevice = DEVICE_SIZES[deviceType][isLandscape ? "landscape" : "portrait"];
|
2026-02-05 14:24:14 +09:00
|
|
|
const hasComponents = Object.keys(layout.components).length > 0;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="flex h-screen w-full items-center justify-center bg-muted">
|
2026-02-02 15:15:01 +09:00
|
|
|
<div className="text-center">
|
2026-03-09 14:31:59 +09:00
|
|
|
<Loader2 className="mx-auto h-10 w-10 animate-spin text-primary" />
|
|
|
|
|
<p className="mt-4 text-muted-foreground">POP 화면 로딩 중...</p>
|
2026-02-02 15:15:01 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error || !screen) {
|
|
|
|
|
return (
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="flex h-screen w-full items-center justify-center bg-muted">
|
2026-02-02 15:15:01 +09:00
|
|
|
<div className="text-center max-w-md p-6">
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
|
2026-02-02 15:15:01 +09:00
|
|
|
<span className="text-2xl">!</span>
|
|
|
|
|
</div>
|
2026-03-09 14:31:59 +09:00
|
|
|
<h2 className="mb-2 text-xl font-bold text-foreground">화면을 찾을 수 없습니다</h2>
|
|
|
|
|
<p className="mb-4 text-muted-foreground">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
2026-02-02 15:15:01 +09:00
|
|
|
<Button onClick={() => router.back()} variant="outline">
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
돌아가기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScreenPreviewProvider isPreviewMode={isPreviewMode}>
|
|
|
|
|
<ActiveTabProvider>
|
|
|
|
|
<TableOptionsProvider>
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="h-screen bg-muted flex flex-col">
|
2026-02-02 15:15:01 +09:00
|
|
|
{/* 상단 툴바 (프리뷰 모드에서만) */}
|
|
|
|
|
{isPreviewMode && (
|
|
|
|
|
<div className="sticky top-0 z-50 bg-white border-b shadow-sm">
|
|
|
|
|
<div className="flex items-center justify-between px-4 py-2">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => window.close()}>
|
|
|
|
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
|
|
|
닫기
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="text-sm font-medium">{screen.screenName}</span>
|
2026-03-09 14:31:59 +09:00
|
|
|
<span className="text-xs text-muted-foreground/70">
|
2026-02-03 19:11:03 +09:00
|
|
|
({currentModeKey.replace("_", " ")})
|
|
|
|
|
</span>
|
2026-02-02 15:15:01 +09:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
<div className="flex items-center gap-2">
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
2026-02-03 19:11:03 +09:00
|
|
|
<Button
|
|
|
|
|
variant={deviceType === "mobile" ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setDevice("mobile")}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Smartphone className="h-4 w-4" />
|
|
|
|
|
모바일
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={deviceType === "tablet" ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setDevice("tablet")}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Tablet className="h-4 w-4" />
|
|
|
|
|
태블릿
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
2026-02-03 19:11:03 +09:00
|
|
|
<Button
|
|
|
|
|
variant={isLandscape ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setOrientation(true)}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<RotateCw className="h-4 w-4" />
|
|
|
|
|
가로
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={!isLandscape ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setOrientation(false)}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
세로
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 자동 감지 모드 버튼 */}
|
2026-02-02 15:15:01 +09:00
|
|
|
<Button
|
2026-02-03 19:11:03 +09:00
|
|
|
variant={isAutoDetect ? "default" : "outline"}
|
2026-02-02 15:15:01 +09:00
|
|
|
size="sm"
|
2026-02-03 19:11:03 +09:00
|
|
|
onClick={() => {
|
|
|
|
|
setDevice(undefined);
|
|
|
|
|
setOrientation(undefined);
|
|
|
|
|
}}
|
2026-02-02 15:15:01 +09:00
|
|
|
className="gap-1"
|
|
|
|
|
>
|
2026-02-03 19:11:03 +09:00
|
|
|
자동
|
2026-02-02 15:15:01 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => window.location.reload()}>
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-09 12:16:26 +09:00
|
|
|
{/* 일반 모드 네비게이션 바 */}
|
|
|
|
|
{!isPreviewMode && (
|
|
|
|
|
<div className="sticky top-0 z-50 flex h-10 items-center justify-between border-b bg-white/80 px-3 backdrop-blur">
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => router.push("/pop")} className="gap-1 text-xs">
|
|
|
|
|
<LayoutGrid className="h-3.5 w-3.5" />
|
|
|
|
|
POP 대시보드
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="text-xs text-gray-500">{screen.screenName}</span>
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => router.push("/")} className="gap-1 text-xs">
|
|
|
|
|
<Monitor className="h-3.5 w-3.5" />
|
|
|
|
|
PC 모드
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
{/* POP 화면 컨텐츠 */}
|
2026-03-03 18:57:59 +09:00
|
|
|
<div className={`flex-1 flex flex-col overflow-auto ${isPreviewMode ? "py-4 items-center" : "bg-white"}`}>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
<div
|
2026-03-09 14:31:59 +09:00
|
|
|
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-foreground" : "w-full min-h-full"}`}
|
2026-02-03 19:11:03 +09:00
|
|
|
style={isPreviewMode ? {
|
|
|
|
|
width: currentDevice.width,
|
2026-02-06 15:30:57 +09:00
|
|
|
maxHeight: "80vh",
|
2026-02-03 19:11:03 +09:00
|
|
|
flexShrink: 0,
|
|
|
|
|
} : undefined}
|
2026-02-02 15:15:01 +09:00
|
|
|
>
|
2026-02-05 14:24:14 +09:00
|
|
|
{/* v5 그리드 렌더러 */}
|
|
|
|
|
{hasComponents ? (
|
2026-02-04 14:14:48 +09:00
|
|
|
<div
|
2026-02-06 15:30:57 +09:00
|
|
|
className="mx-auto min-h-full"
|
2026-02-04 14:14:48 +09:00
|
|
|
style={{ maxWidth: 1366 }}
|
|
|
|
|
>
|
2026-02-06 15:30:57 +09:00
|
|
|
{(() => {
|
feat: V6 정사각형 블록 그리드 시스템 실험 구현
고정 칸 수(4/6/8/12) 기반의 V5 그리드를 24px 정사각형 블록 기반의
동적 칸 수 시스템으로 교체한다. 뷰포트 너비에 따라 블록 수가 자동
계산되며(375px=13칸, 1024px=38칸), 작은 화면에서는 행 그룹 리플로우
(CSS Flexbox wrap 원리)로 자동 재배치된다.
[그리드 코어]
- pop-layout.ts: BLOCK_SIZE=24, BLOCK_GAP=2, BLOCK_PADDING=8,
getBlockColumns() 동적 칸 수 계산, GRID_BREAKPOINTS V6 값
- gridUtils.ts: 행 그룹 리플로우(방식 F) - 같은 행 묶음 처리,
최소 2x2칸 터치 보장, 메인 컨텐츠 전체 너비 확장
- PopRenderer.tsx: repeat(N, 1fr) 블록 렌더링, 동적 칸 수
- PopCanvas.tsx: 뷰포트 프리셋 동적 칸 수, 블록 좌표 변환
[V5→V6 런타임 변환]
- convertV5LayoutToV6: DB 미수정, 로드 시 메모리 변환
12칸 좌표 → 38칸 블록 변환, V5 overrides 제거
- PopDesigner/page.tsx: 로드 지점에 변환 함수 삽입
[충돌 해결]
- ComponentEditorPanel: 높이 표시/모드 라벨 V6 수치
- PopCardListConfig: 카드 추천 threshold V6 기준
- PopDesigner: handleHideComponent 기본 모드 제한 해제
[기본 사이즈]
- 소형(2x2): 아이콘, 프로필, 스캐너
- 중형(8x4): 검색, 버튼, 텍스트
- 대형(19x6~10): 카드, 대시보드, 필드
DB 변경 0건, 백엔드 변경 0건, 컴포넌트 코드 변경 0건.
2026-03-13 16:03:24 +09:00
|
|
|
const adjustedGap = BLOCK_GAP;
|
|
|
|
|
const adjustedPadding = BLOCK_PADDING;
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
|
|
|
return (
|
2026-02-23 13:54:49 +09:00
|
|
|
<PopViewerWithModals
|
2026-02-06 15:30:57 +09:00
|
|
|
layout={layout}
|
|
|
|
|
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
|
2026-02-23 13:54:49 +09:00
|
|
|
screenId={String(screenId)}
|
2026-02-06 15:30:57 +09:00
|
|
|
currentMode={currentModeKey}
|
|
|
|
|
overrideGap={adjustedGap}
|
|
|
|
|
overridePadding={adjustedPadding}
|
2026-02-24 15:54:57 +09:00
|
|
|
onRequestResize={handleRequestResize}
|
|
|
|
|
currentScreenId={screenId}
|
2026-02-06 15:30:57 +09:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
2026-02-04 14:14:48 +09:00
|
|
|
</div>
|
2026-02-02 15:15:01 +09:00
|
|
|
) : (
|
|
|
|
|
// 빈 화면
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] p-8 text-center">
|
2026-03-09 14:31:59 +09:00
|
|
|
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
|
|
|
<Smartphone className="h-8 w-8 text-muted-foreground/70" />
|
2026-02-02 15:15:01 +09:00
|
|
|
</div>
|
2026-03-09 14:31:59 +09:00
|
|
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
2026-02-02 15:15:01 +09:00
|
|
|
화면이 비어있습니다
|
|
|
|
|
</h3>
|
2026-03-09 14:31:59 +09:00
|
|
|
<p className="text-sm text-muted-foreground max-w-xs">
|
2026-02-02 15:15:01 +09:00
|
|
|
POP 화면 디자이너에서 컴포넌트를 추가하여 화면을 구성하세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</TableOptionsProvider>
|
|
|
|
|
</ActiveTabProvider>
|
|
|
|
|
</ScreenPreviewProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Provider 래퍼
|
|
|
|
|
export default function PopScreenViewPageWrapper() {
|
|
|
|
|
return (
|
|
|
|
|
<TableSearchWidgetHeightProvider>
|
|
|
|
|
<ScreenContextProvider>
|
|
|
|
|
<SplitPanelProvider>
|
|
|
|
|
<PopScreenViewPage />
|
|
|
|
|
</SplitPanelProvider>
|
|
|
|
|
</ScreenContextProvider>
|
|
|
|
|
</TableSearchWidgetHeightProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|