2026-02-03 19:11:03 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React from "react";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-02-05 14:24:14 +09:00
|
|
|
|
import {
|
|
|
|
|
|
PopComponentDefinitionV5,
|
|
|
|
|
|
PopGridPosition,
|
|
|
|
|
|
GridMode,
|
|
|
|
|
|
GRID_BREAKPOINTS,
|
|
|
|
|
|
} from "../types/pop-layout";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Settings,
|
2026-02-23 18:45:21 +09:00
|
|
|
|
Link2,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
Eye,
|
|
|
|
|
|
Grid3x3,
|
|
|
|
|
|
MoveHorizontal,
|
|
|
|
|
|
MoveVertical,
|
2026-02-10 18:02:30 +09:00
|
|
|
|
Layers,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
} from "lucide-react";
|
2026-02-03 19:11:03 +09:00
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2026-02-05 14:24:14 +09:00
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2026-02-06 17:07:56 +09:00
|
|
|
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
2026-02-24 12:52:29 +09:00
|
|
|
|
import { PopDataConnection, PopModalDefinition } from "../types/pop-layout";
|
2026-02-23 18:45:21 +09:00
|
|
|
|
import ConnectionEditor from "./ConnectionEditor";
|
2026-02-03 19:11:03 +09:00
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// Props
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
interface ComponentEditorPanelProps {
|
2026-02-05 14:24:14 +09:00
|
|
|
|
/** 선택된 컴포넌트 */
|
|
|
|
|
|
component: PopComponentDefinitionV5 | null;
|
|
|
|
|
|
/** 현재 모드 */
|
|
|
|
|
|
currentMode: GridMode;
|
|
|
|
|
|
/** 컴포넌트 업데이트 */
|
|
|
|
|
|
onUpdateComponent?: (updates: Partial<PopComponentDefinitionV5>) => void;
|
2026-02-03 19:11:03 +09:00
|
|
|
|
/** 추가 className */
|
|
|
|
|
|
className?: string;
|
2026-02-10 18:02:30 +09:00
|
|
|
|
/** 그리드에 배치된 모든 컴포넌트 */
|
|
|
|
|
|
allComponents?: PopComponentDefinitionV5[];
|
|
|
|
|
|
/** 컴포넌트 선택 콜백 */
|
|
|
|
|
|
onSelectComponent?: (componentId: string) => void;
|
|
|
|
|
|
/** 현재 선택된 컴포넌트 ID */
|
|
|
|
|
|
selectedComponentId?: string | null;
|
2026-02-11 14:23:20 +09:00
|
|
|
|
/** 대시보드 페이지 미리보기 인덱스 */
|
|
|
|
|
|
previewPageIndex?: number;
|
|
|
|
|
|
/** 페이지 미리보기 요청 콜백 */
|
|
|
|
|
|
onPreviewPage?: (pageIndex: number) => void;
|
2026-02-23 18:45:21 +09:00
|
|
|
|
/** 데이터 흐름 연결 목록 */
|
|
|
|
|
|
connections?: PopDataConnection[];
|
|
|
|
|
|
/** 연결 추가 콜백 */
|
|
|
|
|
|
onAddConnection?: (conn: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
|
/** 연결 수정 콜백 */
|
|
|
|
|
|
onUpdateConnection?: (connectionId: string, conn: Omit<PopDataConnection, "id">) => void;
|
|
|
|
|
|
/** 연결 삭제 콜백 */
|
|
|
|
|
|
onRemoveConnection?: (connectionId: string) => void;
|
2026-02-24 12:52:29 +09:00
|
|
|
|
/** 모달 정의 목록 (설정 패널에 전달) */
|
|
|
|
|
|
modals?: PopModalDefinition[];
|
2026-02-03 19:11:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 컴포넌트 타입별 라벨
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// ========================================
|
2026-02-10 18:02:30 +09:00
|
|
|
|
const COMPONENT_TYPE_LABELS: Record<string, string> = {
|
|
|
|
|
|
"pop-sample": "샘플",
|
|
|
|
|
|
"pop-text": "텍스트",
|
2026-02-11 14:48:59 +09:00
|
|
|
|
"pop-icon": "아이콘",
|
2026-02-10 18:02:30 +09:00
|
|
|
|
"pop-dashboard": "대시보드",
|
feat(pop-string-list): 리스트 목록 컴포넌트 MVP 구현
테이블 데이터를 리스트/카드 두 가지 모드로 표시하는 pop-string-list 컴포넌트 전체 구현
- 6단계 Stepper 설정 패널 (모드 선택, 헤더/오버플로우, 데이터+컬럼 선택, 조인 설정, 카드/리스트 레이아웃, 필터/정렬)
- 카드 모드: 시각적 그리드 편집기 (드래그 너비/높이 조절, 셀 병합, 셀별 컬럼/스타일 설정)
- 리스트 모드: 드래그앤드롭 컬럼 순서 변경, 너비 조절, 런타임 컬럼 전환 설정
- 조인 설정: Combobox 테이블 검색, 자동 연결 가능 컬럼 발견, 타입 기반 필터링, 가져올 컬럼 선택
- CardColumnJoin에 selectedTargetColumns 필드 추가
- 디자이너 팔레트/에디터/렌더러에 pop-string-list 등록
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 09:03:52 +09:00
|
|
|
|
"pop-card-list": "카드 목록",
|
2026-02-05 14:24:14 +09:00
|
|
|
|
"pop-field": "필드",
|
|
|
|
|
|
"pop-button": "버튼",
|
feat(pop-string-list): 리스트 목록 컴포넌트 MVP 구현
테이블 데이터를 리스트/카드 두 가지 모드로 표시하는 pop-string-list 컴포넌트 전체 구현
- 6단계 Stepper 설정 패널 (모드 선택, 헤더/오버플로우, 데이터+컬럼 선택, 조인 설정, 카드/리스트 레이아웃, 필터/정렬)
- 카드 모드: 시각적 그리드 편집기 (드래그 너비/높이 조절, 셀 병합, 셀별 컬럼/스타일 설정)
- 리스트 모드: 드래그앤드롭 컬럼 순서 변경, 너비 조절, 런타임 컬럼 전환 설정
- 조인 설정: Combobox 테이블 검색, 자동 연결 가능 컬럼 발견, 타입 기반 필터링, 가져올 컬럼 선택
- CardColumnJoin에 selectedTargetColumns 필드 추가
- 디자이너 팔레트/에디터/렌더러에 pop-string-list 등록
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 09:03:52 +09:00
|
|
|
|
"pop-string-list": "리스트 목록",
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
|
"pop-search": "검색",
|
2026-02-05 14:24:14 +09:00
|
|
|
|
"pop-list": "리스트",
|
|
|
|
|
|
"pop-indicator": "인디케이터",
|
|
|
|
|
|
"pop-scanner": "스캐너",
|
|
|
|
|
|
"pop-numpad": "숫자패드",
|
|
|
|
|
|
"pop-spacer": "스페이서",
|
|
|
|
|
|
"pop-break": "줄바꿈",
|
|
|
|
|
|
};
|
2026-02-03 19:11:03 +09:00
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 컴포넌트 편집 패널 (v5 그리드 시스템)
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
export default function ComponentEditorPanel({
|
2026-02-03 19:11:03 +09:00
|
|
|
|
component,
|
2026-02-05 14:24:14 +09:00
|
|
|
|
currentMode,
|
|
|
|
|
|
onUpdateComponent,
|
2026-02-03 19:11:03 +09:00
|
|
|
|
className,
|
2026-02-10 18:02:30 +09:00
|
|
|
|
allComponents,
|
|
|
|
|
|
onSelectComponent,
|
|
|
|
|
|
selectedComponentId,
|
2026-02-11 14:23:20 +09:00
|
|
|
|
previewPageIndex,
|
|
|
|
|
|
onPreviewPage,
|
2026-02-23 18:45:21 +09:00
|
|
|
|
connections,
|
|
|
|
|
|
onAddConnection,
|
|
|
|
|
|
onUpdateConnection,
|
|
|
|
|
|
onRemoveConnection,
|
2026-02-24 12:52:29 +09:00
|
|
|
|
modals,
|
2026-02-03 19:11:03 +09:00
|
|
|
|
}: ComponentEditorPanelProps) {
|
2026-02-05 14:24:14 +09:00
|
|
|
|
const breakpoint = GRID_BREAKPOINTS[currentMode];
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 컴포넌트 없음
|
2026-02-03 19:11:03 +09:00
|
|
|
|
if (!component) {
|
|
|
|
|
|
return (
|
2026-02-06 15:30:57 +09:00
|
|
|
|
<div className={cn("flex h-full flex-col bg-white", className)}>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
<div className="border-b px-4 py-3">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<h3 className="text-sm font-medium">속성</h3>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-1 items-center justify-center p-4 text-sm text-muted-foreground">
|
|
|
|
|
|
컴포넌트를 선택하세요
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 기본 모드 여부
|
|
|
|
|
|
const isDefaultMode = currentMode === "tablet_landscape";
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
return (
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<div className={cn("flex h-full flex-col bg-white", className)}>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="border-b px-4 py-3">
|
|
|
|
|
|
<h3 className="text-sm font-medium">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{component.label || COMPONENT_TYPE_LABELS[component.type]}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">{component.type}</p>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{!isDefaultMode && (
|
|
|
|
|
|
<p className="text-xs text-amber-600 mt-1">
|
|
|
|
|
|
기본 모드(태블릿 가로)에서만 위치 편집 가능
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 탭 */}
|
2026-02-11 10:41:30 +09:00
|
|
|
|
<Tabs defaultValue="position" className="flex flex-1 flex-col min-h-0 overflow-hidden">
|
|
|
|
|
|
<TabsList className="w-full justify-start rounded-none border-b bg-transparent px-2 flex-shrink-0">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<TabsTrigger value="position" className="gap-1 text-xs">
|
|
|
|
|
|
<Grid3x3 className="h-3 w-3" />
|
|
|
|
|
|
위치
|
|
|
|
|
|
</TabsTrigger>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
<TabsTrigger value="settings" className="gap-1 text-xs">
|
|
|
|
|
|
<Settings className="h-3 w-3" />
|
|
|
|
|
|
설정
|
|
|
|
|
|
</TabsTrigger>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<TabsTrigger value="visibility" className="gap-1 text-xs">
|
|
|
|
|
|
<Eye className="h-3 w-3" />
|
|
|
|
|
|
표시
|
|
|
|
|
|
</TabsTrigger>
|
2026-02-23 18:45:21 +09:00
|
|
|
|
<TabsTrigger value="connection" className="gap-1 text-xs">
|
|
|
|
|
|
<Link2 className="h-3 w-3" />
|
|
|
|
|
|
연결
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 위치 탭 */}
|
2026-02-11 10:41:30 +09:00
|
|
|
|
<TabsContent value="position" className="flex-1 min-h-0 overflow-y-auto p-4 m-0">
|
2026-02-10 18:02:30 +09:00
|
|
|
|
{/* 배치된 컴포넌트 목록 */}
|
|
|
|
|
|
{allComponents && allComponents.length > 0 && (
|
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
|
<div className="flex items-center gap-1 mb-2">
|
|
|
|
|
|
<Layers className="h-3 w-3 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
배치된 컴포넌트 ({allComponents.length})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{allComponents.map((comp) => {
|
|
|
|
|
|
const label = comp.label
|
|
|
|
|
|
|| COMPONENT_TYPE_LABELS[comp.type]
|
|
|
|
|
|
|| comp.type;
|
|
|
|
|
|
const isActive = comp.id === selectedComponentId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={comp.id}
|
|
|
|
|
|
onClick={() => onSelectComponent?.(comp.id)}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors",
|
|
|
|
|
|
isActive
|
|
|
|
|
|
? "bg-primary/10 text-primary font-medium"
|
|
|
|
|
|
: "hover:bg-gray-100 text-gray-600"
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="truncate flex-1">{label}</span>
|
|
|
|
|
|
<span className="shrink-0 text-[10px] text-gray-400">
|
|
|
|
|
|
({comp.position.col},{comp.position.row})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="h-px bg-gray-200 mt-3" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<PositionForm
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
currentMode={currentMode}
|
|
|
|
|
|
isDefaultMode={isDefaultMode}
|
|
|
|
|
|
columns={breakpoint.columns}
|
|
|
|
|
|
onUpdate={onUpdateComponent}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 설정 탭 */}
|
2026-02-11 10:41:30 +09:00
|
|
|
|
<TabsContent value="settings" className="flex-1 min-h-0 overflow-y-auto p-4 m-0">
|
2026-02-03 19:11:03 +09:00
|
|
|
|
<ComponentSettingsForm
|
|
|
|
|
|
component={component}
|
2026-02-05 14:24:14 +09:00
|
|
|
|
onUpdate={onUpdateComponent}
|
2026-02-24 15:54:57 +09:00
|
|
|
|
currentMode={currentMode}
|
2026-02-11 14:23:20 +09:00
|
|
|
|
previewPageIndex={previewPageIndex}
|
|
|
|
|
|
onPreviewPage={onPreviewPage}
|
2026-02-24 12:52:29 +09:00
|
|
|
|
modals={modals}
|
2026-02-26 16:00:07 +09:00
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
|
connections={connections}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 표시 탭 */}
|
2026-02-11 10:41:30 +09:00
|
|
|
|
<TabsContent value="visibility" className="flex-1 min-h-0 overflow-y-auto p-4 m-0">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<VisibilityForm
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
onUpdate={onUpdateComponent}
|
|
|
|
|
|
/>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
|
{/* 연결 탭 */}
|
|
|
|
|
|
<TabsContent value="connection" className="flex-1 min-h-0 overflow-y-auto p-4 m-0">
|
|
|
|
|
|
<ConnectionEditor
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
allComponents={allComponents || []}
|
|
|
|
|
|
connections={connections || []}
|
|
|
|
|
|
onAddConnection={onAddConnection}
|
|
|
|
|
|
onUpdateConnection={onUpdateConnection}
|
|
|
|
|
|
onRemoveConnection={onRemoveConnection}
|
|
|
|
|
|
/>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 위치 편집 폼
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
interface PositionFormProps {
|
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
|
currentMode: GridMode;
|
|
|
|
|
|
isDefaultMode: boolean;
|
|
|
|
|
|
columns: number;
|
|
|
|
|
|
onUpdate?: (updates: Partial<PopComponentDefinitionV5>) => void;
|
2026-02-03 19:11:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate }: PositionFormProps) {
|
|
|
|
|
|
const { position } = component;
|
|
|
|
|
|
|
|
|
|
|
|
const handlePositionChange = (field: keyof PopGridPosition, value: number) => {
|
|
|
|
|
|
// 범위 체크
|
|
|
|
|
|
let clampedValue = Math.max(1, value);
|
|
|
|
|
|
|
|
|
|
|
|
if (field === "col" || field === "colSpan") {
|
|
|
|
|
|
clampedValue = Math.min(columns, clampedValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (field === "colSpan" && position.col + clampedValue - 1 > columns) {
|
|
|
|
|
|
clampedValue = columns - position.col + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onUpdate?.({
|
|
|
|
|
|
position: {
|
|
|
|
|
|
...position,
|
|
|
|
|
|
[field]: clampedValue,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
return (
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{/* 그리드 정보 */}
|
|
|
|
|
|
<div className="rounded-lg bg-gray-50 p-3">
|
|
|
|
|
|
<p className="text-xs font-medium text-gray-700 mb-1">
|
|
|
|
|
|
현재 그리드: {GRID_BREAKPOINTS[currentMode].label}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
최대 {columns}칸 × 무제한 행
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 열 위치 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium flex items-center gap-1">
|
|
|
|
|
|
<MoveHorizontal className="h-3 w-3" />
|
|
|
|
|
|
열 위치 (Col)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={columns}
|
|
|
|
|
|
value={position.col}
|
|
|
|
|
|
onChange={(e) => handlePositionChange("col", parseInt(e.target.value) || 1)}
|
|
|
|
|
|
disabled={!isDefaultMode}
|
|
|
|
|
|
className="h-8 w-20 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
(1~{columns})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 행 위치 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium flex items-center gap-1">
|
|
|
|
|
|
<MoveVertical className="h-3 w-3" />
|
|
|
|
|
|
행 위치 (Row)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
value={position.row}
|
|
|
|
|
|
onChange={(e) => handlePositionChange("row", parseInt(e.target.value) || 1)}
|
|
|
|
|
|
disabled={!isDefaultMode}
|
|
|
|
|
|
className="h-8 w-20 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
(1~)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<div className="h-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 열 크기 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium flex items-center gap-1">
|
|
|
|
|
|
<MoveHorizontal className="h-3 w-3" />
|
|
|
|
|
|
열 크기 (ColSpan)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={columns}
|
|
|
|
|
|
value={position.colSpan}
|
|
|
|
|
|
onChange={(e) => handlePositionChange("colSpan", parseInt(e.target.value) || 1)}
|
|
|
|
|
|
disabled={!isDefaultMode}
|
|
|
|
|
|
className="h-8 w-20 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
칸 (1~{columns})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
{Math.round((position.colSpan / columns) * 100)}% 너비
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</p>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 행 크기 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium flex items-center gap-1">
|
|
|
|
|
|
<MoveVertical className="h-3 w-3" />
|
|
|
|
|
|
행 크기 (RowSpan)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
value={position.rowSpan}
|
|
|
|
|
|
onChange={(e) => handlePositionChange("rowSpan", parseInt(e.target.value) || 1)}
|
|
|
|
|
|
disabled={!isDefaultMode}
|
|
|
|
|
|
className="h-8 w-20 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
행
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
높이: {position.rowSpan * GRID_BREAKPOINTS[currentMode].rowHeight}px
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-02-05 14:24:14 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 비활성화 안내 */}
|
|
|
|
|
|
{!isDefaultMode && (
|
|
|
|
|
|
<div className="rounded-lg bg-amber-50 border border-amber-200 p-3">
|
|
|
|
|
|
<p className="text-xs text-amber-800">
|
|
|
|
|
|
위치 편집은 기본 모드(태블릿 가로)에서만 가능합니다.
|
|
|
|
|
|
다른 모드에서는 자동으로 변환됩니다.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 설정 폼
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
interface ComponentSettingsFormProps {
|
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
|
onUpdate?: (updates: Partial<PopComponentDefinitionV5>) => void;
|
2026-02-24 15:54:57 +09:00
|
|
|
|
currentMode?: GridMode;
|
2026-02-11 14:23:20 +09:00
|
|
|
|
previewPageIndex?: number;
|
|
|
|
|
|
onPreviewPage?: (pageIndex: number) => void;
|
2026-02-24 12:52:29 +09:00
|
|
|
|
modals?: PopModalDefinition[];
|
2026-02-26 16:00:07 +09:00
|
|
|
|
allComponents?: PopComponentDefinitionV5[];
|
|
|
|
|
|
connections?: PopDataConnection[];
|
2026-02-05 14:24:14 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:00:07 +09:00
|
|
|
|
function ComponentSettingsForm({ component, onUpdate, currentMode, previewPageIndex, onPreviewPage, modals, allComponents, connections }: ComponentSettingsFormProps) {
|
2026-02-06 17:07:56 +09:00
|
|
|
|
// PopComponentRegistry에서 configPanel 가져오기
|
|
|
|
|
|
const registeredComp = PopComponentRegistry.getComponent(component.type);
|
|
|
|
|
|
const ConfigPanel = registeredComp?.configPanel;
|
|
|
|
|
|
|
|
|
|
|
|
// config 업데이트 핸들러
|
|
|
|
|
|
const handleConfigUpdate = (newConfig: any) => {
|
|
|
|
|
|
onUpdate?.({ config: newConfig });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
{/* 라벨 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">라벨</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={component.label || ""}
|
|
|
|
|
|
onChange={(e) => onUpdate?.({ label: e.target.value })}
|
|
|
|
|
|
placeholder="컴포넌트 이름"
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-06 17:07:56 +09:00
|
|
|
|
{/* 컴포넌트 타입별 설정 패널 */}
|
|
|
|
|
|
{ConfigPanel ? (
|
|
|
|
|
|
<ConfigPanel
|
|
|
|
|
|
config={component.config || {}}
|
|
|
|
|
|
onUpdate={handleConfigUpdate}
|
2026-02-24 15:54:57 +09:00
|
|
|
|
currentMode={currentMode}
|
|
|
|
|
|
currentColSpan={component.position.colSpan}
|
2026-02-11 14:23:20 +09:00
|
|
|
|
onPreviewPage={onPreviewPage}
|
|
|
|
|
|
previewPageIndex={previewPageIndex}
|
2026-02-24 12:52:29 +09:00
|
|
|
|
modals={modals}
|
2026-02-26 16:00:07 +09:00
|
|
|
|
allComponents={allComponents}
|
|
|
|
|
|
connections={connections}
|
|
|
|
|
|
componentId={component.id}
|
2026-02-06 17:07:56 +09:00
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="rounded-lg bg-gray-50 p-3">
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
{component.type} 전용 설정이 없습니다
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
2026-02-05 14:24:14 +09:00
|
|
|
|
// 표시/숨김 폼
|
2026-02-03 19:11:03 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
|
2026-02-05 14:24:14 +09:00
|
|
|
|
interface VisibilityFormProps {
|
|
|
|
|
|
component: PopComponentDefinitionV5;
|
|
|
|
|
|
onUpdate?: (updates: Partial<PopComponentDefinitionV5>) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function VisibilityForm({ component, onUpdate }: VisibilityFormProps) {
|
|
|
|
|
|
const modes: Array<{ key: GridMode; label: string }> = [
|
|
|
|
|
|
{ key: "tablet_landscape", label: "태블릿 가로 (12칸)" },
|
|
|
|
|
|
{ key: "tablet_portrait", label: "태블릿 세로 (8칸)" },
|
|
|
|
|
|
{ key: "mobile_landscape", label: "모바일 가로 (6칸)" },
|
|
|
|
|
|
{ key: "mobile_portrait", label: "모바일 세로 (4칸)" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const handleVisibilityChange = (mode: GridMode, visible: boolean) => {
|
|
|
|
|
|
onUpdate?.({
|
|
|
|
|
|
visibility: {
|
|
|
|
|
|
...component.visibility,
|
|
|
|
|
|
[mode]: visible,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
2026-02-05 14:24:14 +09:00
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<Label className="text-xs font-medium">모드별 표시 설정</Label>
|
|
|
|
|
|
|
|
|
|
|
|
{modes.map((mode) => {
|
|
|
|
|
|
const isVisible = component.visibility?.[mode.key] !== false;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={mode.key} className="flex items-center gap-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`visibility-${mode.key}`}
|
|
|
|
|
|
checked={isVisible}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
handleVisibilityChange(mode.key, checked === true)
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor={`visibility-${mode.key}`}
|
|
|
|
|
|
className="text-xs cursor-pointer"
|
|
|
|
|
|
>
|
|
|
|
|
|
{mode.label}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3">
|
|
|
|
|
|
<p className="text-xs text-blue-800">
|
|
|
|
|
|
체크 해제하면 해당 모드에서 컴포넌트가 숨겨집니다
|
|
|
|
|
|
</p>
|
2026-02-03 19:11:03 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 17:03:47 +09:00
|
|
|
|
|