2026-02-02 15:15:01 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
import { useCallback, useRef, useState, useEffect, useMemo } from "react";
|
2026-02-02 15:15:01 +09:00
|
|
|
|
import { useDrop } from "react-dnd";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
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,
|
|
|
|
|
|
PopComponentDefinition,
|
2026-02-02 15:15:01 +09:00
|
|
|
|
PopComponentType,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
PopGridPosition,
|
|
|
|
|
|
GridMode,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
GapPreset,
|
|
|
|
|
|
GAP_PRESETS,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
GRID_BREAKPOINTS,
|
|
|
|
|
|
DEFAULT_COMPONENT_GRID_SIZE,
|
2026-02-23 13:54:49 +09:00
|
|
|
|
PopModalDefinition,
|
|
|
|
|
|
ModalSizePreset,
|
|
|
|
|
|
MODAL_SIZE_PRESETS,
|
|
|
|
|
|
resolveModalWidth,
|
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_SIZE,
|
|
|
|
|
|
BLOCK_GAP,
|
|
|
|
|
|
BLOCK_PADDING,
|
|
|
|
|
|
getBlockColumns,
|
2026-02-02 15:15:01 +09:00
|
|
|
|
} from "./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 { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, EyeOff, Monitor, ChevronDown, ChevronUp } from "lucide-react";
|
2026-02-05 19:16:23 +09:00
|
|
|
|
import { useDrag } from "react-dnd";
|
2026-02-02 15:15:01 +09:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-02-06 15:30:57 +09:00
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue
|
|
|
|
|
|
} from "@/components/ui/select";
|
2026-02-05 19:16:23 +09:00
|
|
|
|
import { toast } from "sonner";
|
2026-02-05 14:24:14 +09:00
|
|
|
|
import PopRenderer from "./renderers/PopRenderer";
|
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 { findNextEmptyPosition, isOverlapping, getAllEffectivePositions } from "./utils/gridUtils";
|
2026-02-05 19:16:23 +09:00
|
|
|
|
import { DND_ITEM_TYPES } from "./constants";
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +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
|
|
|
|
* V6: 캔버스 내 상대 좌표 → 블록 그리드 좌표 변환
|
|
|
|
|
|
* 블록 크기가 고정(BLOCK_SIZE)이므로 1fr 계산 불필요
|
2026-02-05 19:16:23 +09:00
|
|
|
|
*/
|
|
|
|
|
|
function calcGridPosition(
|
|
|
|
|
|
relX: number,
|
|
|
|
|
|
relY: number,
|
|
|
|
|
|
canvasWidth: number,
|
|
|
|
|
|
columns: number,
|
|
|
|
|
|
rowHeight: number,
|
|
|
|
|
|
gap: number,
|
|
|
|
|
|
padding: number
|
|
|
|
|
|
): { col: number; row: number } {
|
|
|
|
|
|
const x = relX - padding;
|
|
|
|
|
|
const y = relY - padding;
|
|
|
|
|
|
|
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 cellStride = BLOCK_SIZE + gap;
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
const col = Math.max(1, Math.min(columns, Math.floor(x / cellStride) + 1));
|
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 row = Math.max(1, Math.floor(y / cellStride) + 1);
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
return { col, row };
|
|
|
|
|
|
}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
// 드래그 아이템 타입 정의
|
2026-02-05 14:24:14 +09:00
|
|
|
|
interface DragItemComponent {
|
|
|
|
|
|
type: typeof DND_ITEM_TYPES.COMPONENT;
|
|
|
|
|
|
componentType: PopComponentType;
|
|
|
|
|
|
}
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
interface DragItemMoveComponent {
|
|
|
|
|
|
componentId: string;
|
|
|
|
|
|
originalPosition: PopGridPosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +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
|
|
|
|
// V6: 프리셋 해상도 (블록 칸 수 동적 계산)
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const VIEWPORT_PRESETS = [
|
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
|
|
|
|
{ id: "mobile_portrait", label: "모바일 세로", shortLabel: `모바일↕ (${getBlockColumns(375)}칸)`, width: 375, icon: Smartphone },
|
|
|
|
|
|
{ id: "mobile_landscape", label: "모바일 가로", shortLabel: `모바일↔ (${getBlockColumns(600)}칸)`, width: 600, icon: Smartphone },
|
|
|
|
|
|
{ id: "tablet_portrait", label: "태블릿 세로", shortLabel: `태블릿↕ (${getBlockColumns(820)}칸)`, width: 820, icon: Tablet },
|
|
|
|
|
|
{ id: "tablet_landscape", label: "태블릿 가로", shortLabel: `태블릿↔ (${getBlockColumns(1024)}칸)`, width: 1024, icon: Tablet },
|
2026-02-05 14:24:14 +09:00
|
|
|
|
] as const;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
type ViewportPreset = GridMode;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 기본 프리셋 (태블릿 가로)
|
|
|
|
|
|
const DEFAULT_PRESET: ViewportPreset = "tablet_landscape";
|
2026-02-03 19:11:03 +09:00
|
|
|
|
|
2026-02-06 15:30:57 +09:00
|
|
|
|
// 캔버스 세로 자동 확장 설정
|
|
|
|
|
|
const MIN_CANVAS_HEIGHT = 600; // 최소 캔버스 높이 (px)
|
|
|
|
|
|
const CANVAS_EXTRA_ROWS = 3; // 여유 행 수
|
|
|
|
|
|
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// Props
|
|
|
|
|
|
// ========================================
|
2026-02-02 15:15:01 +09:00
|
|
|
|
interface PopCanvasProps {
|
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
|
|
|
|
layout: PopLayoutData;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
selectedComponentId: string | null;
|
2026-02-05 14:24:14 +09:00
|
|
|
|
currentMode: GridMode;
|
|
|
|
|
|
onModeChange: (mode: GridMode) => void;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
onSelectComponent: (id: string | null) => void;
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onDropComponent: (type: PopComponentType, position: PopGridPosition) => void;
|
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
|
|
|
|
onUpdateComponent: (componentId: string, updates: Partial<PopComponentDefinition>) => void;
|
2026-02-03 19:11:03 +09:00
|
|
|
|
onDeleteComponent: (componentId: string) => void;
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onMoveComponent?: (componentId: string, newPosition: PopGridPosition) => void;
|
|
|
|
|
|
onResizeComponent?: (componentId: string, newPosition: PopGridPosition) => void;
|
2026-02-05 19:16:23 +09:00
|
|
|
|
onResizeEnd?: (componentId: string) => void;
|
|
|
|
|
|
onHideComponent?: (componentId: string) => void;
|
|
|
|
|
|
onUnhideComponent?: (componentId: string) => void;
|
|
|
|
|
|
onLockLayout?: () => void;
|
|
|
|
|
|
onResetOverride?: (mode: GridMode) => void;
|
2026-02-06 15:30:57 +09:00
|
|
|
|
onChangeGapPreset?: (preset: GapPreset) => void;
|
2026-02-24 15:54:57 +09:00
|
|
|
|
/** 컴포넌트가 자신의 rowSpan/colSpan 변경을 요청 (CardList 확장 등) */
|
|
|
|
|
|
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
|
2026-02-11 14:23:20 +09:00
|
|
|
|
/** 대시보드 페이지 미리보기 인덱스 (-1이면 기본 모드) */
|
|
|
|
|
|
previewPageIndex?: number;
|
2026-02-23 13:54:49 +09:00
|
|
|
|
/** 현재 활성 캔버스 ID ("main" 또는 모달 ID) */
|
|
|
|
|
|
activeCanvasId?: string;
|
|
|
|
|
|
/** 캔버스 전환 콜백 */
|
|
|
|
|
|
onActiveCanvasChange?: (canvasId: string) => void;
|
|
|
|
|
|
/** 모달 정의 업데이트 콜백 */
|
|
|
|
|
|
onUpdateModal?: (modalId: string, updates: Partial<PopModalDefinition>) => void;
|
2026-02-02 15:15:01 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// PopCanvas: 그리드 캔버스
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
|
|
|
|
|
export default function PopCanvas({
|
2026-02-02 15:15:01 +09:00
|
|
|
|
layout,
|
|
|
|
|
|
selectedComponentId,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
currentMode,
|
|
|
|
|
|
onModeChange,
|
2026-02-02 15:15:01 +09:00
|
|
|
|
onSelectComponent,
|
|
|
|
|
|
onDropComponent,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onUpdateComponent,
|
2026-02-02 15:15:01 +09:00
|
|
|
|
onDeleteComponent,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onMoveComponent,
|
|
|
|
|
|
onResizeComponent,
|
2026-02-05 19:16:23 +09:00
|
|
|
|
onResizeEnd,
|
|
|
|
|
|
onHideComponent,
|
|
|
|
|
|
onUnhideComponent,
|
|
|
|
|
|
onLockLayout,
|
|
|
|
|
|
onResetOverride,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
onChangeGapPreset,
|
2026-02-24 15:54:57 +09:00
|
|
|
|
onRequestResize,
|
2026-02-11 14:23:20 +09:00
|
|
|
|
previewPageIndex,
|
2026-02-23 13:54:49 +09:00
|
|
|
|
activeCanvasId = "main",
|
|
|
|
|
|
onActiveCanvasChange,
|
|
|
|
|
|
onUpdateModal,
|
2026-02-02 15:15:01 +09:00
|
|
|
|
}: PopCanvasProps) {
|
2026-02-23 13:54:49 +09:00
|
|
|
|
// 모달 탭 데이터
|
|
|
|
|
|
const modalTabs = useMemo(() => {
|
|
|
|
|
|
const tabs: { id: string; label: string }[] = [{ id: "main", label: "메인화면" }];
|
|
|
|
|
|
if (layout.modals?.length) {
|
|
|
|
|
|
for (const modal of layout.modals) {
|
|
|
|
|
|
const numbering = modal.id.replace("modal-", "");
|
|
|
|
|
|
tabs.push({ id: modal.id, label: `모달화면 ${numbering}` });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return tabs;
|
|
|
|
|
|
}, [layout.modals]);
|
|
|
|
|
|
|
|
|
|
|
|
// activeCanvasId에 따라 렌더링할 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
|
|
|
|
const activeLayout = useMemo((): PopLayoutData => {
|
2026-02-23 13:54:49 +09:00
|
|
|
|
if (activeCanvasId === "main") return layout;
|
|
|
|
|
|
const modal = layout.modals?.find(m => m.id === activeCanvasId);
|
|
|
|
|
|
if (!modal) return layout; // fallback
|
|
|
|
|
|
return {
|
|
|
|
|
|
...layout,
|
|
|
|
|
|
gridConfig: modal.gridConfig,
|
|
|
|
|
|
components: modal.components,
|
|
|
|
|
|
overrides: modal.overrides,
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [layout, activeCanvasId]);
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 활성 모달 정의 (모달 캔버스일 때만)
|
|
|
|
|
|
const activeModal = useMemo(() => {
|
|
|
|
|
|
if (activeCanvasId === "main") return null;
|
|
|
|
|
|
return layout.modals?.find(m => m.id === activeCanvasId) || null;
|
|
|
|
|
|
}, [layout.modals, activeCanvasId]);
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 줌 상태
|
|
|
|
|
|
const [canvasScale, setCanvasScale] = useState(0.8);
|
|
|
|
|
|
|
2026-02-06 15:30:57 +09:00
|
|
|
|
// 커스텀 뷰포트 너비
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const [customWidth, setCustomWidth] = useState(1024);
|
|
|
|
|
|
|
|
|
|
|
|
// 그리드 가이드 표시 여부
|
|
|
|
|
|
const [showGridGuide, setShowGridGuide] = useState(true);
|
|
|
|
|
|
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
// 패닝 상태
|
|
|
|
|
|
const [isPanning, setIsPanning] = useState(false);
|
|
|
|
|
|
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
|
2026-02-03 19:11:03 +09:00
|
|
|
|
const [isSpacePressed, setIsSpacePressed] = useState(false);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
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
|
|
|
|
// V6: 뷰포트에서 동적 블록 칸 수 계산
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === currentMode)!;
|
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 dynamicColumns = getBlockColumns(customWidth);
|
|
|
|
|
|
const breakpoint = {
|
|
|
|
|
|
...GRID_BREAKPOINTS[currentMode],
|
|
|
|
|
|
columns: dynamicColumns,
|
|
|
|
|
|
rowHeight: BLOCK_SIZE,
|
|
|
|
|
|
gap: BLOCK_GAP,
|
|
|
|
|
|
padding: BLOCK_PADDING,
|
|
|
|
|
|
label: `${dynamicColumns}칸 블록`,
|
|
|
|
|
|
};
|
2026-02-05 14:24:14 +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
|
|
|
|
// V6: 블록 간격 고정 (프리셋 무관)
|
2026-02-06 15:30:57 +09:00
|
|
|
|
const currentGapPreset = layout.settings.gapPreset || "medium";
|
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
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
|
// 숨김 컴포넌트 ID 목록 (activeLayout 기반)
|
|
|
|
|
|
const hiddenComponentIds = activeLayout.overrides?.[currentMode]?.hidden || [];
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
|
|
|
|
|
// 동적 캔버스 높이 계산 (컴포넌트 배치 기반)
|
|
|
|
|
|
const dynamicCanvasHeight = useMemo(() => {
|
2026-02-23 13:54:49 +09:00
|
|
|
|
const visibleComps = Object.values(activeLayout.components).filter(
|
2026-02-06 15:30:57 +09:00
|
|
|
|
comp => !hiddenComponentIds.includes(comp.id)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (visibleComps.length === 0) return MIN_CANVAS_HEIGHT;
|
|
|
|
|
|
|
|
|
|
|
|
// 최대 row + rowSpan 찾기
|
|
|
|
|
|
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
|
|
|
|
|
const overridePos = layout.overrides?.[currentMode]?.positions?.[comp.id];
|
|
|
|
|
|
const pos = overridePos ? { ...comp.position, ...overridePos } : comp.position;
|
|
|
|
|
|
const rowEnd = pos.row + pos.rowSpan;
|
|
|
|
|
|
return Math.max(max, rowEnd);
|
|
|
|
|
|
}, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// 높이 계산: (행 수 + 여유) * (행높이 + gap) + padding
|
|
|
|
|
|
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
|
|
|
|
|
const height = totalRows * (breakpoint.rowHeight + adjustedGap) + adjustedPadding * 2;
|
|
|
|
|
|
|
|
|
|
|
|
return Math.max(MIN_CANVAS_HEIGHT, height);
|
2026-02-23 13:54:49 +09:00
|
|
|
|
}, [activeLayout.components, activeLayout.overrides, currentMode, hiddenComponentIds, breakpoint.rowHeight, adjustedGap, adjustedPadding]);
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
|
|
|
|
|
// 그리드 라벨 계산 (동적 행 수)
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const gridLabels = useMemo(() => {
|
|
|
|
|
|
const columnLabels = Array.from({ length: breakpoint.columns }, (_, i) => i + 1);
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
|
|
|
|
|
// 동적 행 수 계산
|
|
|
|
|
|
const rowCount = Math.ceil(dynamicCanvasHeight / (breakpoint.rowHeight + adjustedGap));
|
|
|
|
|
|
const rowLabels = Array.from({ length: rowCount }, (_, i) => i + 1);
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
return { columnLabels, rowLabels };
|
2026-02-06 15:30:57 +09:00
|
|
|
|
}, [breakpoint.columns, breakpoint.rowHeight, dynamicCanvasHeight, adjustedGap]);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// 줌 컨트롤
|
|
|
|
|
|
const handleZoomIn = () => setCanvasScale((prev) => Math.min(1.5, prev + 0.1));
|
|
|
|
|
|
const handleZoomOut = () => setCanvasScale((prev) => Math.max(0.3, prev - 0.1));
|
|
|
|
|
|
const handleZoomFit = () => setCanvasScale(1.0);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 모드 변경
|
|
|
|
|
|
const handleViewportChange = (mode: GridMode) => {
|
|
|
|
|
|
onModeChange(mode);
|
|
|
|
|
|
const presetData = VIEWPORT_PRESETS.find((p) => p.id === mode)!;
|
|
|
|
|
|
setCustomWidth(presetData.width);
|
2026-02-06 15:30:57 +09:00
|
|
|
|
// customHeight는 dynamicCanvasHeight로 자동 계산됨
|
2026-02-05 14:24:14 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// 패닝
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
const handlePanStart = (e: React.MouseEvent) => {
|
|
|
|
|
|
const isMiddleButton = e.button === 1;
|
2026-02-05 14:24:14 +09:00
|
|
|
|
if (isMiddleButton || isSpacePressed) {
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
setIsPanning(true);
|
|
|
|
|
|
setPanStart({ x: e.clientX, y: e.clientY });
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePanMove = (e: React.MouseEvent) => {
|
|
|
|
|
|
if (!isPanning || !containerRef.current) return;
|
|
|
|
|
|
const deltaX = e.clientX - panStart.x;
|
|
|
|
|
|
const deltaY = e.clientY - panStart.y;
|
|
|
|
|
|
containerRef.current.scrollLeft -= deltaX;
|
|
|
|
|
|
containerRef.current.scrollTop -= deltaY;
|
|
|
|
|
|
setPanStart({ x: e.clientX, y: e.clientY });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
const handlePanEnd = () => setIsPanning(false);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// Ctrl + 휠로 줌 조정
|
|
|
|
|
|
const handleWheel = (e: React.WheelEvent) => {
|
|
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
|
|
|
|
setCanvasScale((prev) => Math.max(0.3, Math.min(1.5, prev + delta)));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
|
|
|
|
|
|
// Space 키 감지
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
2026-02-03 19:11:03 +09:00
|
|
|
|
if (e.code === "Space" && !isSpacePressed) setIsSpacePressed(true);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
};
|
|
|
|
|
|
const handleKeyUp = (e: KeyboardEvent) => {
|
2026-02-03 19:11:03 +09:00
|
|
|
|
if (e.code === "Space") setIsSpacePressed(false);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
};
|
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
window.addEventListener("keyup", handleKeyUp);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
window.removeEventListener("keyup", handleKeyUp);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [isSpacePressed]);
|
|
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
// 통합 드롭 핸들러 (팔레트에서 추가 + 컴포넌트 이동)
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
const [{ isOver, canDrop }, drop] = useDrop(
|
|
|
|
|
|
() => ({
|
2026-02-05 19:16:23 +09:00
|
|
|
|
accept: [DND_ITEM_TYPES.COMPONENT, DND_ITEM_TYPES.MOVE_COMPONENT],
|
|
|
|
|
|
drop: (item: DragItemComponent | DragItemMoveComponent, monitor) => {
|
2026-02-05 14:24:14 +09:00
|
|
|
|
if (!canvasRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const canvasRect = canvasRef.current.getBoundingClientRect();
|
2026-02-05 19:16:23 +09:00
|
|
|
|
const itemType = monitor.getItemType();
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
// 팔레트에서 새 컴포넌트 추가 - 마우스 위치 기준
|
|
|
|
|
|
if (itemType === DND_ITEM_TYPES.COMPONENT) {
|
|
|
|
|
|
const offset = monitor.getClientOffset();
|
|
|
|
|
|
if (!offset) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 캔버스 내 상대 좌표 (스케일 보정)
|
|
|
|
|
|
// canvasRect는 scale 적용된 크기이므로, 상대 좌표를 scale로 나눠야 실제 좌표
|
|
|
|
|
|
const relX = (offset.x - canvasRect.left) / canvasScale;
|
|
|
|
|
|
const relY = (offset.y - canvasRect.top) / canvasScale;
|
|
|
|
|
|
|
|
|
|
|
|
// 그리드 좌표 계산
|
|
|
|
|
|
const gridPos = calcGridPosition(
|
|
|
|
|
|
relX,
|
|
|
|
|
|
relY,
|
|
|
|
|
|
customWidth,
|
|
|
|
|
|
breakpoint.columns,
|
|
|
|
|
|
breakpoint.rowHeight,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
adjustedGap,
|
|
|
|
|
|
adjustedPadding
|
2026-02-05 19:16:23 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const dragItem = item as DragItemComponent;
|
|
|
|
|
|
const defaultSize = DEFAULT_COMPONENT_GRID_SIZE[dragItem.componentType];
|
|
|
|
|
|
|
|
|
|
|
|
const candidatePosition: PopGridPosition = {
|
|
|
|
|
|
col: gridPos.col,
|
|
|
|
|
|
row: gridPos.row,
|
|
|
|
|
|
colSpan: defaultSize.colSpan,
|
|
|
|
|
|
rowSpan: defaultSize.rowSpan,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 모드에서의 유효 위치들로 중첩 검사
|
2026-02-23 13:54:49 +09:00
|
|
|
|
const effectivePositions = getAllEffectivePositions(activeLayout, currentMode);
|
2026-02-05 19:16:23 +09:00
|
|
|
|
const existingPositions = Array.from(effectivePositions.values());
|
|
|
|
|
|
|
|
|
|
|
|
const hasOverlap = existingPositions.some(pos =>
|
|
|
|
|
|
isOverlapping(candidatePosition, pos)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let finalPosition: PopGridPosition;
|
|
|
|
|
|
|
|
|
|
|
|
if (hasOverlap) {
|
|
|
|
|
|
finalPosition = findNextEmptyPosition(
|
|
|
|
|
|
existingPositions,
|
|
|
|
|
|
defaultSize.colSpan,
|
|
|
|
|
|
defaultSize.rowSpan,
|
|
|
|
|
|
breakpoint.columns
|
|
|
|
|
|
);
|
|
|
|
|
|
toast.info("겹치는 위치입니다. 빈 위치로 자동 배치됩니다.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
finalPosition = candidatePosition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onDropComponent(dragItem.componentType, finalPosition);
|
|
|
|
|
|
}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
// 기존 컴포넌트 이동 - 마우스 위치 기준
|
|
|
|
|
|
if (itemType === DND_ITEM_TYPES.MOVE_COMPONENT) {
|
|
|
|
|
|
const offset = monitor.getClientOffset();
|
|
|
|
|
|
if (!offset) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 캔버스 내 상대 좌표 (스케일 보정)
|
|
|
|
|
|
const relX = (offset.x - canvasRect.left) / canvasScale;
|
|
|
|
|
|
const relY = (offset.y - canvasRect.top) / canvasScale;
|
|
|
|
|
|
|
|
|
|
|
|
const gridPos = calcGridPosition(
|
|
|
|
|
|
relX,
|
|
|
|
|
|
relY,
|
|
|
|
|
|
customWidth,
|
|
|
|
|
|
breakpoint.columns,
|
|
|
|
|
|
breakpoint.rowHeight,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
adjustedGap,
|
|
|
|
|
|
adjustedPadding
|
2026-02-05 19:16:23 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const dragItem = item as DragItemMoveComponent & { fromHidden?: boolean };
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 모드에서의 유효 위치들 가져오기
|
2026-02-23 13:54:49 +09:00
|
|
|
|
const effectivePositions = getAllEffectivePositions(activeLayout, currentMode);
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
// 이동 중인 컴포넌트의 현재 유효 위치에서 colSpan/rowSpan 가져오기
|
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
|
|
|
|
// 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용
|
2026-02-05 19:16:23 +09:00
|
|
|
|
const currentEffectivePos = effectivePositions.get(dragItem.componentId);
|
|
|
|
|
|
const componentData = layout.components[dragItem.componentId];
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentEffectivePos && !componentData) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sourcePosition = currentEffectivePos || componentData.position;
|
|
|
|
|
|
|
|
|
|
|
|
// colSpan이 현재 모드의 columns를 초과하면 제한
|
|
|
|
|
|
const adjustedColSpan = Math.min(sourcePosition.colSpan, breakpoint.columns);
|
|
|
|
|
|
|
|
|
|
|
|
// 드롭 위치 + 크기가 범위를 초과하면 드롭 위치를 자동 조정
|
|
|
|
|
|
let adjustedCol = gridPos.col;
|
|
|
|
|
|
if (adjustedCol + adjustedColSpan - 1 > breakpoint.columns) {
|
|
|
|
|
|
adjustedCol = Math.max(1, breakpoint.columns - adjustedColSpan + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newPosition: PopGridPosition = {
|
|
|
|
|
|
col: adjustedCol,
|
|
|
|
|
|
row: gridPos.row,
|
|
|
|
|
|
colSpan: adjustedColSpan,
|
|
|
|
|
|
rowSpan: sourcePosition.rowSpan,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 자기 자신 제외한 다른 컴포넌트들의 유효 위치와 겹침 체크
|
|
|
|
|
|
const hasOverlap = Array.from(effectivePositions.entries()).some(([id, pos]) => {
|
|
|
|
|
|
if (id === dragItem.componentId) return false; // 자기 자신 제외
|
|
|
|
|
|
return isOverlapping(newPosition, pos);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (hasOverlap) {
|
|
|
|
|
|
toast.error("이 위치로 이동할 수 없습니다 (다른 컴포넌트와 겹침)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 이동 처리 (숨김 컴포넌트의 경우 handleMoveComponent에서 숨김 해제도 함께 처리됨)
|
|
|
|
|
|
onMoveComponent?.(dragItem.componentId, newPosition);
|
|
|
|
|
|
|
|
|
|
|
|
// 숨김 패널에서 드래그한 경우 안내 메시지
|
|
|
|
|
|
if (dragItem.fromHidden) {
|
|
|
|
|
|
toast.info("컴포넌트가 다시 표시됩니다");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
},
|
|
|
|
|
|
collect: (monitor) => ({
|
|
|
|
|
|
isOver: monitor.isOver(),
|
|
|
|
|
|
canDrop: monitor.canDrop(),
|
|
|
|
|
|
}),
|
2026-02-02 15:15:01 +09:00
|
|
|
|
}),
|
2026-02-23 13:54:49 +09:00
|
|
|
|
[onDropComponent, onMoveComponent, onUnhideComponent, breakpoint, layout, activeLayout, currentMode, canvasScale, customWidth, adjustedGap, adjustedPadding]
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
);
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
drop(canvasRef);
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
|
// 빈 상태 체크 (activeLayout 기반)
|
|
|
|
|
|
const isEmpty = Object.keys(activeLayout.components).length === 0;
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
|
// 숨김 처리된 컴포넌트 객체 목록
|
2026-02-05 19:16:23 +09:00
|
|
|
|
const hiddenComponents = useMemo(() => {
|
|
|
|
|
|
return hiddenComponentIds
|
2026-02-23 13:54:49 +09:00
|
|
|
|
.map(id => activeLayout.components[id])
|
2026-02-05 19:16:23 +09:00
|
|
|
|
.filter(Boolean);
|
2026-02-23 13:54:49 +09:00
|
|
|
|
}, [hiddenComponentIds, activeLayout.components]);
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
2026-02-06 15:30:57 +09:00
|
|
|
|
// 표시되는 컴포넌트 목록 (숨김 제외)
|
|
|
|
|
|
const visibleComponents = useMemo(() => {
|
2026-02-23 13:54:49 +09:00
|
|
|
|
return Object.values(activeLayout.components).filter(
|
2026-02-06 15:30:57 +09:00
|
|
|
|
comp => !hiddenComponentIds.includes(comp.id)
|
|
|
|
|
|
);
|
2026-02-23 13:54:49 +09:00
|
|
|
|
}, [activeLayout.components, hiddenComponentIds]);
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
|
const hasGridComponents = Object.keys(activeLayout.components).length > 0;
|
2026-02-05 19:16:23 +09:00
|
|
|
|
const showHiddenPanel = currentMode !== "tablet_landscape" && (hiddenComponents.length > 0 || hasGridComponents);
|
feat(pop-designer): POP 디자이너 v2.0 - 4가지 디바이스 모드 및 캔버스 UX 개선
- v2 레이아웃 데이터 구조 도입 (4모드별 별도 레이아웃 + 공유 컴포넌트 정의)
- tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
- sections/components를 Record<string, Definition> 객체로 관리
- v1 → v2 자동 마이그레이션 지원
- 캔버스 UX 개선
- 줌 기능 (30%~150%, 마우스 휠 + 버튼)
- 패닝 기능 (중앙 마우스, Space+드래그, 배경 드래그)
- 2개 캔버스 동시 표시 (가로/세로 모드)
- Delete 키로 섹션/컴포넌트 삭제 기능 추가
- layout.sections 순회하여 componentIds에서 부모 섹션 찾는 방식
- 미리보기 v2 레이아웃 호환성 수정
- Object.keys(layout.sections).length 체크로 변경
수정 파일: PopDesigner.tsx, PopCanvas.tsx, SectionGridV2.tsx(신규),
types/pop-layout.ts, PopPanel.tsx, PopScreenPreview.tsx,
PopCategoryTree.tsx, screenManagementService.ts
2026-02-03 11:25:00 +09:00
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
|
return (
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="flex h-full flex-col bg-muted">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 상단 컨트롤 */}
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="flex items-center gap-2 border-b bg-background px-4 py-2">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 모드 프리셋 버튼 */}
|
|
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
|
{VIEWPORT_PRESETS.map((preset) => {
|
|
|
|
|
|
const Icon = preset.icon;
|
|
|
|
|
|
const isActive = currentMode === preset.id;
|
|
|
|
|
|
const isDefault = preset.id === DEFAULT_PRESET;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={preset.id}
|
|
|
|
|
|
variant={isActive ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => handleViewportChange(preset.id as GridMode)}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-8 gap-1 text-xs",
|
|
|
|
|
|
isActive && "shadow-sm"
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon className="h-3 w-3" />
|
|
|
|
|
|
{preset.shortLabel}
|
|
|
|
|
|
{isDefault && " (기본)"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="h-4 w-px bg-border" />
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
2026-02-05 19:16:23 +09:00
|
|
|
|
{/* 고정/되돌리기 버튼 (기본 모드 아닐 때만 표시) */}
|
|
|
|
|
|
{currentMode !== DEFAULT_PRESET && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={onLockLayout}
|
|
|
|
|
|
className="h-8 gap-1 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Lock className="h-3 w-3" />
|
|
|
|
|
|
고정
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{layout.overrides?.[currentMode] && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => onResetOverride?.(currentMode)}
|
|
|
|
|
|
className="h-8 gap-1 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RotateCcw className="h-3 w-3" />
|
|
|
|
|
|
자동으로 되돌리기
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="h-4 w-px bg-border" />
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 해상도 표시 */}
|
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
2026-02-06 15:30:57 +09:00
|
|
|
|
{customWidth} × {Math.round(dynamicCanvasHeight)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="h-4 w-px bg-border" />
|
2026-02-06 15:30:57 +09:00
|
|
|
|
|
|
|
|
|
|
{/* Gap 프리셋 선택 */}
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">간격:</span>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={currentGapPreset}
|
|
|
|
|
|
onValueChange={(value) => onChangeGapPreset?.(value as GapPreset)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-20 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{(Object.keys(GAP_PRESETS) as GapPreset[]).map((preset) => (
|
|
|
|
|
|
<SelectItem key={preset} value={preset} className="text-xs">
|
|
|
|
|
|
{GAP_PRESETS[preset].label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex-1" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 줌 컨트롤 */}
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
{Math.round(canvasScale * 100)}%
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
onClick={handleZoomOut}
|
|
|
|
|
|
disabled={canvasScale <= 0.3}
|
|
|
|
|
|
className="h-7 w-7"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ZoomOut className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
onClick={handleZoomFit}
|
|
|
|
|
|
className="h-7 w-7"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Maximize2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
onClick={handleZoomIn}
|
|
|
|
|
|
disabled={canvasScale >= 1.5}
|
|
|
|
|
|
className="h-7 w-7"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ZoomIn className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="h-4 w-px bg-border" />
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 그리드 가이드 토글 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={showGridGuide ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => setShowGridGuide(!showGridGuide)}
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
그리드 {showGridGuide ? "ON" : "OFF"}
|
|
|
|
|
|
</Button>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
|
{/* 모달 탭 바 (모달이 1개 이상 있을 때만 표시) */}
|
|
|
|
|
|
{modalTabs.length > 1 && (
|
|
|
|
|
|
<div className="flex gap-1 border-b bg-muted/30 px-4 py-1">
|
|
|
|
|
|
{modalTabs.map(tab => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={tab.id}
|
|
|
|
|
|
variant={activeCanvasId === tab.id ? "default" : "ghost"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => onActiveCanvasChange?.(tab.id)}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
{tab.label}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모달 사이즈 설정 패널 (모달 캔버스 활성 시) */}
|
|
|
|
|
|
{activeModal && (
|
|
|
|
|
|
<ModalSizeSettingsPanel
|
|
|
|
|
|
modal={activeModal}
|
|
|
|
|
|
currentMode={currentMode}
|
|
|
|
|
|
onUpdate={(updates) => onUpdateModal?.(activeModal.id, updates)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 캔버스 영역 */}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
<div
|
2026-02-05 14:24:14 +09:00
|
|
|
|
ref={containerRef}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
className={cn(
|
2026-03-09 14:31:59 +09:00
|
|
|
|
"canvas-scroll-area relative flex-1 overflow-auto bg-muted",
|
2026-02-05 14:24:14 +09:00
|
|
|
|
isSpacePressed && "cursor-grab",
|
|
|
|
|
|
isPanning && "cursor-grabbing"
|
2026-02-03 19:11:03 +09:00
|
|
|
|
)}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onMouseDown={handlePanStart}
|
|
|
|
|
|
onMouseMove={handlePanMove}
|
|
|
|
|
|
onMouseUp={handlePanEnd}
|
|
|
|
|
|
onMouseLeave={handlePanEnd}
|
|
|
|
|
|
onWheel={handleWheel}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
>
|
|
|
|
|
|
<div
|
2026-02-05 19:16:23 +09:00
|
|
|
|
className="relative mx-auto my-8 origin-top overflow-visible flex gap-4"
|
2026-02-03 19:11:03 +09:00
|
|
|
|
style={{
|
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
|
|
|
|
width: showHiddenPanel
|
2026-02-05 19:16:23 +09:00
|
|
|
|
? `${customWidth + 32 + 220}px` // 오른쪽 패널 공간 추가
|
|
|
|
|
|
: `${customWidth + 32}px`,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
minHeight: `${dynamicCanvasHeight + 32}px`,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
transform: `scale(${canvasScale})`,
|
2026-02-03 19:11:03 +09:00
|
|
|
|
}}
|
2026-02-02 15:15:01 +09:00
|
|
|
|
>
|
2026-02-05 19:16:23 +09:00
|
|
|
|
{/* 그리드 + 라벨 영역 */}
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
{/* 그리드 라벨 영역 */}
|
|
|
|
|
|
{showGridGuide && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* 열 라벨 (상단) */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex absolute top-0 left-8"
|
|
|
|
|
|
style={{
|
2026-02-06 15:30:57 +09:00
|
|
|
|
gap: `${adjustedGap}px`,
|
|
|
|
|
|
paddingLeft: `${adjustedPadding}px`,
|
2026-02-05 19:16:23 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{gridLabels.columnLabels.map((num) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`col-${num}`}
|
2026-03-09 14:31:59 +09:00
|
|
|
|
className="flex items-center justify-center text-xs font-semibold text-primary"
|
2026-02-05 19:16:23 +09:00
|
|
|
|
style={{
|
2026-02-06 15:30:57 +09:00
|
|
|
|
width: `calc((${customWidth}px - ${adjustedPadding * 2}px - ${adjustedGap * (breakpoint.columns - 1)}px) / ${breakpoint.columns})`,
|
2026-02-05 19:16:23 +09:00
|
|
|
|
height: "24px",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{num}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 행 라벨 (좌측) */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-col absolute top-8 left-0"
|
|
|
|
|
|
style={{
|
2026-02-06 15:30:57 +09:00
|
|
|
|
gap: `${adjustedGap}px`,
|
|
|
|
|
|
paddingTop: `${adjustedPadding}px`,
|
2026-02-05 19:16:23 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{gridLabels.rowLabels.map((num) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`row-${num}`}
|
2026-03-09 14:31:59 +09:00
|
|
|
|
className="flex items-center justify-center text-xs font-semibold text-primary"
|
2026-02-05 19:16:23 +09:00
|
|
|
|
style={{
|
|
|
|
|
|
width: "24px",
|
|
|
|
|
|
height: `${breakpoint.rowHeight}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{num}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
)}
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 디바이스 스크린 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
|
className={cn(
|
2026-03-09 15:51:42 +09:00
|
|
|
|
"relative rounded-lg border-2 bg-background shadow-xl overflow-visible",
|
2026-02-05 19:16:23 +09:00
|
|
|
|
canDrop && isOver && "ring-4 ring-primary/20"
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: `${customWidth}px`,
|
2026-02-06 15:30:57 +09:00
|
|
|
|
minHeight: `${dynamicCanvasHeight}px`,
|
2026-02-05 19:16:23 +09:00
|
|
|
|
marginLeft: "32px",
|
|
|
|
|
|
marginTop: "32px",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isEmpty ? (
|
|
|
|
|
|
// 빈 상태
|
|
|
|
|
|
<div className="flex h-full items-center justify-center p-8">
|
|
|
|
|
|
<div className="text-center">
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="mb-2 text-sm font-medium text-muted-foreground">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
컴포넌트를 드래그하여 배치하세요
|
|
|
|
|
|
</div>
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="text-xs text-muted-foreground/70">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
{breakpoint.label} - {breakpoint.columns}칸 그리드
|
|
|
|
|
|
</div>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-05 19:16:23 +09:00
|
|
|
|
) : (
|
|
|
|
|
|
// 그리드 렌더러
|
|
|
|
|
|
<PopRenderer
|
2026-02-23 13:54:49 +09:00
|
|
|
|
layout={activeLayout}
|
2026-02-05 19:16:23 +09:00
|
|
|
|
viewportWidth={customWidth}
|
|
|
|
|
|
currentMode={currentMode}
|
|
|
|
|
|
isDesignMode={true}
|
|
|
|
|
|
showGridGuide={showGridGuide}
|
|
|
|
|
|
selectedComponentId={selectedComponentId}
|
|
|
|
|
|
onComponentClick={onSelectComponent}
|
|
|
|
|
|
onBackgroundClick={() => onSelectComponent(null)}
|
|
|
|
|
|
onComponentMove={onMoveComponent}
|
|
|
|
|
|
onComponentResize={onResizeComponent}
|
|
|
|
|
|
onComponentResizeEnd={onResizeEnd}
|
2026-02-24 15:54:57 +09:00
|
|
|
|
onRequestResize={onRequestResize}
|
2026-02-06 15:30:57 +09:00
|
|
|
|
overrideGap={adjustedGap}
|
|
|
|
|
|
overridePadding={adjustedPadding}
|
2026-02-11 14:23:20 +09:00
|
|
|
|
previewPageIndex={previewPageIndex}
|
2026-02-05 19:16:23 +09:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 오른쪽 패널 영역 (초과 컴포넌트 + 숨김 컴포넌트) */}
|
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
|
|
|
|
{showHiddenPanel && (
|
2026-02-05 19:16:23 +09:00
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-col gap-3"
|
|
|
|
|
|
style={{ marginTop: "32px" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 숨김 컴포넌트 패널 */}
|
|
|
|
|
|
{showHiddenPanel && (
|
|
|
|
|
|
<HiddenPanel
|
|
|
|
|
|
components={hiddenComponents}
|
|
|
|
|
|
selectedComponentId={selectedComponentId}
|
|
|
|
|
|
onSelectComponent={onSelectComponent}
|
|
|
|
|
|
onHideComponent={onHideComponent}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 하단 정보 */}
|
2026-03-09 15:51:42 +09:00
|
|
|
|
<div className="flex items-center justify-between border-t bg-background px-4 py-2">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<div className="text-xs text-muted-foreground">
|
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
|
|
|
|
V6 블록 그리드 - {dynamicColumns}칸 (블록: {BLOCK_SIZE}px, 간격: {BLOCK_GAP}px)
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
|
Space + 드래그: 패닝 | Ctrl + 휠: 줌
|
2026-02-02 15:15:01 +09:00
|
|
|
|
</div>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
2026-02-02 15:15:01 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-05 19:16:23 +09:00
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-06 15:30:57 +09:00
|
|
|
|
// 검토 필요 영역 (오른쪽 패널)
|
2026-02-05 19:16:23 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 숨김 컴포넌트 영역 (오른쪽 패널)
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
interface HiddenPanelProps {
|
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
|
|
|
|
components: PopComponentDefinition[];
|
2026-02-05 19:16:23 +09:00
|
|
|
|
selectedComponentId: string | null;
|
|
|
|
|
|
onSelectComponent: (id: string | null) => void;
|
|
|
|
|
|
onHideComponent?: (componentId: string) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function HiddenPanel({
|
|
|
|
|
|
components,
|
|
|
|
|
|
selectedComponentId,
|
|
|
|
|
|
onSelectComponent,
|
|
|
|
|
|
onHideComponent,
|
|
|
|
|
|
}: HiddenPanelProps) {
|
|
|
|
|
|
// 그리드에서 컴포넌트를 드래그하여 이 패널에 드롭하면 숨김 처리
|
|
|
|
|
|
const [{ isOver, canDrop }, drop] = useDrop(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
accept: DND_ITEM_TYPES.MOVE_COMPONENT,
|
|
|
|
|
|
drop: (item: { componentId: string; fromHidden?: boolean }) => {
|
|
|
|
|
|
// 이미 숨김 패널에서 온 아이템은 무시
|
|
|
|
|
|
if (item.fromHidden) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 숨김 처리
|
|
|
|
|
|
onHideComponent?.(item.componentId);
|
|
|
|
|
|
toast.info("컴포넌트가 숨김 처리되었습니다");
|
|
|
|
|
|
},
|
|
|
|
|
|
canDrop: (item: { componentId: string; fromHidden?: boolean }) => {
|
|
|
|
|
|
// 숨김 패널에서 온 아이템은 드롭 불가
|
|
|
|
|
|
return !item.fromHidden;
|
|
|
|
|
|
},
|
|
|
|
|
|
collect: (monitor) => ({
|
|
|
|
|
|
isOver: monitor.isOver(),
|
|
|
|
|
|
canDrop: monitor.canDrop(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
[onHideComponent]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={drop}
|
|
|
|
|
|
className={cn(
|
2026-03-09 14:31:59 +09:00
|
|
|
|
"flex flex-col rounded-lg border-2 border-dashed bg-muted/50 transition-colors",
|
2026-02-05 19:16:23 +09:00
|
|
|
|
isOver && canDrop
|
2026-03-09 16:34:53 +09:00
|
|
|
|
? "border-input bg-muted/70"
|
2026-03-09 14:31:59 +09:00
|
|
|
|
: "border-input"
|
2026-02-05 19:16:23 +09:00
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: "200px",
|
|
|
|
|
|
maxHeight: "300px",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 헤더 */}
|
2026-03-09 16:34:53 +09:00
|
|
|
|
<div className="flex items-center gap-2 border-b border-input bg-muted/50 px-3 py-2 rounded-t-lg">
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-xs font-semibold text-foreground">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
숨김 ({components.length}개)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 컴포넌트 목록 */}
|
|
|
|
|
|
<div className="flex-1 overflow-auto p-2 space-y-2">
|
|
|
|
|
|
{components.map((comp) => (
|
|
|
|
|
|
<HiddenItem
|
|
|
|
|
|
key={comp.id}
|
|
|
|
|
|
component={comp}
|
|
|
|
|
|
isSelected={selectedComponentId === comp.id}
|
|
|
|
|
|
onSelect={() => onSelectComponent(comp.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 안내 문구 */}
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="border-t border-input px-3 py-2 bg-muted/80 rounded-b-lg">
|
|
|
|
|
|
<p className="text-[10px] text-muted-foreground leading-tight">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
그리드로 드래그하여 다시 표시
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 숨김 컴포넌트 아이템 (드래그 가능)
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
interface HiddenItemProps {
|
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
|
|
|
|
component: PopComponentDefinition;
|
2026-02-05 19:16:23 +09:00
|
|
|
|
isSelected: boolean;
|
|
|
|
|
|
onSelect: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function HiddenItem({
|
|
|
|
|
|
component,
|
|
|
|
|
|
isSelected,
|
|
|
|
|
|
onSelect,
|
|
|
|
|
|
}: HiddenItemProps) {
|
|
|
|
|
|
const [{ isDragging }, drag] = useDrag(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
type: DND_ITEM_TYPES.MOVE_COMPONENT,
|
|
|
|
|
|
item: {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
originalPosition: component.position,
|
|
|
|
|
|
fromHidden: true, // 숨김 패널에서 왔음을 표시
|
|
|
|
|
|
},
|
|
|
|
|
|
collect: (monitor) => ({
|
|
|
|
|
|
isDragging: monitor.isDragging(),
|
|
|
|
|
|
}),
|
|
|
|
|
|
}),
|
|
|
|
|
|
[component.id, component.position]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={drag}
|
|
|
|
|
|
className={cn(
|
2026-03-09 15:51:42 +09:00
|
|
|
|
"rounded-md border-2 bg-background p-2 cursor-move transition-all opacity-60",
|
2026-02-05 19:16:23 +09:00
|
|
|
|
isSelected
|
|
|
|
|
|
? "border-primary ring-2 ring-primary/30"
|
2026-03-09 14:31:59 +09:00
|
|
|
|
: "border-input hover:border-input",
|
2026-02-05 19:16:23 +09:00
|
|
|
|
isDragging && "opacity-30"
|
|
|
|
|
|
)}
|
|
|
|
|
|
onClick={onSelect}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 컴포넌트 이름 */}
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="flex items-center gap-1 text-xs font-medium text-muted-foreground truncate">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
<EyeOff className="h-3 w-3" />
|
|
|
|
|
|
{component.label || component.type}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 원본 위치 정보 */}
|
2026-03-09 14:31:59 +09:00
|
|
|
|
<div className="text-[10px] text-muted-foreground mt-1">
|
2026-02-05 19:16:23 +09:00
|
|
|
|
원본: {component.position.col}열, {component.position.row}행
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-23 13:54:49 +09:00
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 모달 사이즈 설정 패널
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
const SIZE_PRESET_ORDER: ModalSizePreset[] = ["sm", "md", "lg", "xl", "full"];
|
|
|
|
|
|
|
|
|
|
|
|
const MODE_LABELS: { mode: GridMode; label: string; icon: typeof Smartphone; width: number }[] = [
|
|
|
|
|
|
{ mode: "mobile_portrait", label: "모바일 세로", icon: Smartphone, width: 375 },
|
|
|
|
|
|
{ mode: "mobile_landscape", label: "모바일 가로", icon: Smartphone, width: 667 },
|
|
|
|
|
|
{ mode: "tablet_portrait", label: "태블릿 세로", icon: Tablet, width: 768 },
|
|
|
|
|
|
{ mode: "tablet_landscape", label: "태블릿 가로", icon: Monitor, width: 1024 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function ModalSizeSettingsPanel({
|
|
|
|
|
|
modal,
|
|
|
|
|
|
currentMode,
|
|
|
|
|
|
onUpdate,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
modal: PopModalDefinition;
|
|
|
|
|
|
currentMode: GridMode;
|
|
|
|
|
|
onUpdate: (updates: Partial<PopModalDefinition>) => void;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
|
|
const sizeConfig = modal.sizeConfig || { default: "md" };
|
|
|
|
|
|
const usePerMode = !!sizeConfig.modeOverrides && Object.keys(sizeConfig.modeOverrides).length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
const currentModeInfo = MODE_LABELS.find(m => m.mode === currentMode)!;
|
|
|
|
|
|
const currentModeWidth = currentModeInfo.width;
|
|
|
|
|
|
const currentModalWidth = resolveModalWidth(
|
|
|
|
|
|
{ default: sizeConfig.default, modeOverrides: sizeConfig.modeOverrides as Record<GridMode, ModalSizePreset> | undefined },
|
|
|
|
|
|
currentMode,
|
|
|
|
|
|
currentModeWidth,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleDefaultChange = (preset: ModalSizePreset) => {
|
|
|
|
|
|
onUpdate({
|
|
|
|
|
|
sizeConfig: {
|
|
|
|
|
|
...sizeConfig,
|
|
|
|
|
|
default: preset,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTogglePerMode = () => {
|
|
|
|
|
|
if (usePerMode) {
|
|
|
|
|
|
onUpdate({
|
|
|
|
|
|
sizeConfig: {
|
|
|
|
|
|
default: sizeConfig.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
onUpdate({
|
|
|
|
|
|
sizeConfig: {
|
|
|
|
|
|
...sizeConfig,
|
|
|
|
|
|
modeOverrides: {
|
|
|
|
|
|
mobile_portrait: sizeConfig.default,
|
|
|
|
|
|
mobile_landscape: sizeConfig.default,
|
|
|
|
|
|
tablet_portrait: sizeConfig.default,
|
|
|
|
|
|
tablet_landscape: sizeConfig.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleModeChange = (mode: GridMode, preset: ModalSizePreset) => {
|
|
|
|
|
|
onUpdate({
|
|
|
|
|
|
sizeConfig: {
|
|
|
|
|
|
...sizeConfig,
|
|
|
|
|
|
modeOverrides: {
|
|
|
|
|
|
...sizeConfig.modeOverrides,
|
|
|
|
|
|
[mode]: preset,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="border-b bg-muted/20">
|
|
|
|
|
|
{/* 헤더 (항상 표시) */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
|
|
|
|
className="flex w-full items-center justify-between px-4 py-2 hover:bg-muted/30 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
{isExpanded
|
|
|
|
|
|
? <ChevronUp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
|
: <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
|
}
|
|
|
|
|
|
<span className="text-xs font-semibold">{modal.title}</span>
|
|
|
|
|
|
<span className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
|
|
|
|
|
|
{sizeConfig.default.toUpperCase()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">
|
|
|
|
|
|
{currentModalWidth}px / {currentModeWidth}px
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">{modal.id}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 펼침 영역 */}
|
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
|
<div className="px-4 pb-3 space-y-3">
|
|
|
|
|
|
{/* 기본 사이즈 선택 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<span className="text-[11px] text-muted-foreground font-medium">모달 사이즈</span>
|
|
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
|
{SIZE_PRESET_ORDER.map(preset => {
|
|
|
|
|
|
const info = MODAL_SIZE_PRESETS[preset];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={preset}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleDefaultChange(preset)}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex-1 h-8 rounded-md text-xs font-medium transition-colors flex flex-col items-center justify-center gap-0",
|
|
|
|
|
|
sizeConfig.default === preset
|
|
|
|
|
|
? "bg-primary text-primary-foreground"
|
|
|
|
|
|
: "bg-background border hover:bg-accent"
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="leading-none">{preset.toUpperCase()}</span>
|
|
|
|
|
|
<span className={cn(
|
|
|
|
|
|
"text-[9px] leading-none",
|
|
|
|
|
|
sizeConfig.default === preset ? "text-primary-foreground/70" : "text-muted-foreground"
|
|
|
|
|
|
)}>
|
|
|
|
|
|
{preset === "full" ? "100%" : `${info.width}px`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모드별 개별 설정 토글 */}
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="text-[11px] text-muted-foreground">모드별 개별 사이즈</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleTogglePerMode}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
2026-03-09 14:31:59 +09:00
|
|
|
|
usePerMode ? "bg-primary" : "bg-muted/60"
|
2026-02-23 13:54:49 +09:00
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
2026-03-09 17:18:45 +09:00
|
|
|
|
"inline-block h-3.5 w-3.5 rounded-full bg-background transition-transform",
|
2026-02-23 13:54:49 +09:00
|
|
|
|
usePerMode ? "translate-x-4.5" : "translate-x-0.5"
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모드별 설정 */}
|
|
|
|
|
|
{usePerMode && (
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
{MODE_LABELS.map(({ mode, label, icon: Icon }) => {
|
|
|
|
|
|
const modePreset = sizeConfig.modeOverrides?.[mode] ?? sizeConfig.default;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={mode} className={cn(
|
|
|
|
|
|
"flex items-center justify-between rounded-md px-2 py-1",
|
|
|
|
|
|
mode === currentMode ? "bg-primary/10 ring-1 ring-primary/30" : ""
|
|
|
|
|
|
)}>
|
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
|
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-[11px]">{label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-0.5">
|
|
|
|
|
|
{SIZE_PRESET_ORDER.map(preset => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={preset}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleModeChange(mode, preset)}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-6 px-1.5 rounded text-[10px] font-medium transition-colors",
|
|
|
|
|
|
modePreset === preset
|
|
|
|
|
|
? "bg-primary text-primary-foreground"
|
|
|
|
|
|
: "bg-background border hover:bg-accent"
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{preset.toUpperCase()}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 캔버스 축소판 미리보기 */}
|
|
|
|
|
|
<ModalThumbnailPreview sizeConfig={sizeConfig} currentMode={currentMode} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 모달 사이즈 썸네일 미리보기 (캔버스 축소판 + 모달 오버레이)
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
function ModalThumbnailPreview({
|
|
|
|
|
|
sizeConfig,
|
|
|
|
|
|
currentMode,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
sizeConfig: { default: ModalSizePreset; modeOverrides?: Partial<Record<GridMode, ModalSizePreset>> };
|
|
|
|
|
|
currentMode: GridMode;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const PREVIEW_WIDTH = 260;
|
|
|
|
|
|
const ASPECT_RATIO = 0.65;
|
|
|
|
|
|
|
|
|
|
|
|
const modeInfo = MODE_LABELS.find(m => m.mode === currentMode)!;
|
|
|
|
|
|
const modeWidth = modeInfo.width;
|
|
|
|
|
|
const modeHeight = modeWidth * ASPECT_RATIO;
|
|
|
|
|
|
|
|
|
|
|
|
const scale = PREVIEW_WIDTH / modeWidth;
|
|
|
|
|
|
const previewHeight = Math.round(modeHeight * scale);
|
|
|
|
|
|
|
|
|
|
|
|
const modalWidth = resolveModalWidth(
|
|
|
|
|
|
{ default: sizeConfig.default, modeOverrides: sizeConfig.modeOverrides as Record<GridMode, ModalSizePreset> | undefined },
|
|
|
|
|
|
currentMode,
|
|
|
|
|
|
modeWidth,
|
|
|
|
|
|
);
|
|
|
|
|
|
const scaledModalWidth = Math.min(Math.round(modalWidth * scale), PREVIEW_WIDTH);
|
|
|
|
|
|
const isFull = modalWidth >= modeWidth;
|
|
|
|
|
|
const scaledModalHeight = isFull ? previewHeight : Math.round(previewHeight * 0.75);
|
|
|
|
|
|
const Icon = modeInfo.icon;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="text-[11px] text-muted-foreground font-medium">미리보기</span>
|
|
|
|
|
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
|
|
|
|
<Icon className="h-3 w-3" />
|
|
|
|
|
|
<span>{modeInfo.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
2026-03-09 14:31:59 +09:00
|
|
|
|
className="relative mx-auto rounded-md border bg-muted overflow-hidden"
|
2026-02-23 13:54:49 +09:00
|
|
|
|
style={{ width: `${PREVIEW_WIDTH}px`, height: `${previewHeight}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 반투명 배경 오버레이 (모달이 열렸을 때의 딤 효과) */}
|
|
|
|
|
|
<div className="absolute inset-0 bg-black/10" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모달 영역 (가운데 정렬, FULL이면 전체 채움) */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"absolute border-2 border-primary/60 bg-primary/15",
|
|
|
|
|
|
isFull ? "rounded-none" : "rounded-sm"
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: `${scaledModalWidth}px`,
|
|
|
|
|
|
height: `${scaledModalHeight}px`,
|
|
|
|
|
|
left: `${(PREVIEW_WIDTH - scaledModalWidth) / 2}px`,
|
|
|
|
|
|
top: `${(previewHeight - scaledModalHeight) / 2}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="absolute top-1 left-1.5 text-[8px] font-medium text-primary/80 leading-none">
|
|
|
|
|
|
모달
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 하단 수치 표시 */}
|
|
|
|
|
|
<div className="absolute bottom-1 right-1.5 rounded bg-black/50 px-1.5 py-0.5 text-[9px] text-white">
|
|
|
|
|
|
{isFull ? "FULL" : `${modalWidth}px`} / {modeWidth}px
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|