feat(pop-text): POP 텍스트 컴포넌트 추가
- pop-text 컴포넌트 구현 (텍스트/시간/이미지/제목 타입) - PopComponentRegistry에 preview 속성 추가 - ComponentEditorPanel에서 configPanel 동적 렌더링 - PopRenderer에서 preview 컴포넌트 렌더링 지원 - ComponentPalette에 텍스트 컴포넌트 추가 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
40219fed08
commit
2dfc3cc681
|
|
@ -12,6 +12,9 @@ import {
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
// POP 컴포넌트 자동 등록 (반드시 다른 import보다 먼저)
|
||||||
|
import "@/lib/registry/pop-components";
|
||||||
|
|
||||||
import PopCanvas from "./PopCanvas";
|
import PopCanvas from "./PopCanvas";
|
||||||
import ComponentEditorPanel from "./panels/ComponentEditorPanel";
|
import ComponentEditorPanel from "./panels/ComponentEditorPanel";
|
||||||
import ComponentPalette from "./panels/ComponentPalette";
|
import ComponentPalette from "./panels/ComponentPalette";
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -315,6 +316,15 @@ interface ComponentSettingsFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComponentSettingsForm({ component, onUpdate }: ComponentSettingsFormProps) {
|
function ComponentSettingsForm({ component, onUpdate }: ComponentSettingsFormProps) {
|
||||||
|
// PopComponentRegistry에서 configPanel 가져오기
|
||||||
|
const registeredComp = PopComponentRegistry.getComponent(component.type);
|
||||||
|
const ConfigPanel = registeredComp?.configPanel;
|
||||||
|
|
||||||
|
// config 업데이트 핸들러
|
||||||
|
const handleConfigUpdate = (newConfig: any) => {
|
||||||
|
onUpdate?.({ config: newConfig });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 라벨 */}
|
{/* 라벨 */}
|
||||||
|
|
@ -329,12 +339,19 @@ function ComponentSettingsForm({ component, onUpdate }: ComponentSettingsFormPro
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컴포넌트 타입별 설정 (추후 구현) */}
|
{/* 컴포넌트 타입별 설정 패널 */}
|
||||||
<div className="rounded-lg bg-gray-50 p-3">
|
{ConfigPanel ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<ConfigPanel
|
||||||
{component.type} 전용 설정은 Phase 4에서 구현 예정
|
config={component.config || {}}
|
||||||
</p>
|
onUpdate={handleConfigUpdate}
|
||||||
</div>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg bg-gray-50 p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{component.type} 전용 설정이 없습니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useDrag } from "react-dnd";
|
import { useDrag } from "react-dnd";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PopComponentType } from "../types/pop-layout";
|
import { PopComponentType } from "../types/pop-layout";
|
||||||
import { Square } from "lucide-react";
|
import { Square, FileText } from "lucide-react";
|
||||||
import { DND_ITEM_TYPES } from "../constants";
|
import { DND_ITEM_TYPES } from "../constants";
|
||||||
|
|
||||||
// 컴포넌트 정의
|
// 컴포넌트 정의
|
||||||
|
|
@ -21,6 +21,12 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||||
icon: Square,
|
icon: Square,
|
||||||
description: "크기 조정 테스트용",
|
description: "크기 조정 테스트용",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "pop-text",
|
||||||
|
label: "텍스트",
|
||||||
|
icon: FileText,
|
||||||
|
description: "텍스트, 시간, 이미지 표시",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 드래그 가능한 컴포넌트 아이템
|
// 드래그 가능한 컴포넌트 아이템
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
isOverlapping,
|
isOverlapping,
|
||||||
getAllEffectivePositions,
|
getAllEffectivePositions,
|
||||||
} from "../utils/gridUtils";
|
} from "../utils/gridUtils";
|
||||||
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -500,7 +501,11 @@ interface ComponentContentProps {
|
||||||
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected }: ComponentContentProps) {
|
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected }: ComponentContentProps) {
|
||||||
const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type;
|
const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type;
|
||||||
|
|
||||||
// 디자인 모드: 플레이스홀더 표시
|
// PopComponentRegistry에서 등록된 컴포넌트 가져오기
|
||||||
|
const registeredComp = PopComponentRegistry.getComponent(component.type);
|
||||||
|
const PreviewComponent = registeredComp?.preview;
|
||||||
|
|
||||||
|
// 디자인 모드: 미리보기 컴포넌트 또는 플레이스홀더 표시
|
||||||
if (isDesignMode) {
|
if (isDesignMode) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
|
|
@ -519,11 +524,15 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내용 */}
|
{/* 내용: 등록된 preview 컴포넌트 또는 기본 플레이스홀더 */}
|
||||||
<div className="flex flex-1 items-center justify-center p-2">
|
<div className="flex flex-1 items-center justify-center overflow-hidden">
|
||||||
<span className="text-xs text-gray-400">
|
{PreviewComponent ? (
|
||||||
{typeLabel}
|
<PreviewComponent config={component.config} />
|
||||||
</span>
|
) : (
|
||||||
|
<span className="text-xs text-gray-400 p-2">
|
||||||
|
{typeLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위치 정보 표시 (유효 위치 사용) */}
|
{/* 위치 정보 표시 (유효 위치 사용) */}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
/**
|
/**
|
||||||
* POP 컴포넌트 타입
|
* POP 컴포넌트 타입
|
||||||
*/
|
*/
|
||||||
export type PopComponentType = "pop-sample"; // 테스트용 샘플 박스
|
export type PopComponentType = "pop-sample" | "pop-text"; // 테스트용 샘플 박스, 텍스트 컴포넌트
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터 흐름 정의
|
* 데이터 흐름 정의
|
||||||
|
|
@ -341,6 +341,7 @@ export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => {
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_COMPONENT_GRID_SIZE: Record<PopComponentType, { colSpan: number; rowSpan: number }> = {
|
export const DEFAULT_COMPONENT_GRID_SIZE: Record<PopComponentType, { colSpan: number; rowSpan: number }> = {
|
||||||
"pop-sample": { colSpan: 2, rowSpan: 1 },
|
"pop-sample": { colSpan: 2, rowSpan: 1 },
|
||||||
|
"pop-text": { colSpan: 3, rowSpan: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface PopComponentDefinition {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
component: React.ComponentType<any>;
|
component: React.ComponentType<any>;
|
||||||
configPanel?: React.ComponentType<any>;
|
configPanel?: React.ComponentType<any>;
|
||||||
|
preview?: React.ComponentType<{ config?: any }>; // 디자이너 미리보기용
|
||||||
defaultProps?: Record<string, any>;
|
defaultProps?: Record<string, any>;
|
||||||
// POP 전용 속성
|
// POP 전용 속성
|
||||||
touchOptimized?: boolean;
|
touchOptimized?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POP 컴포넌트 인덱스
|
* POP 컴포넌트 자동 등록 진입점
|
||||||
*
|
*
|
||||||
* POP(모바일/태블릿) 전용 컴포넌트를 export합니다.
|
* [역할]
|
||||||
* 새로운 POP 컴포넌트 추가 시 여기에 export를 추가하세요.
|
* - 각 컴포넌트 파일을 import하면 해당 파일 끝의 registerComponent()가 실행되어 자동 등록됨
|
||||||
|
* - 예: import "./pop-text" → pop-text.tsx 실행 → PopComponentRegistry.registerComponent() 호출
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================================
|
// 공통 타입 re-export (외부에서 필요 시 사용 가능)
|
||||||
// POP 컴포넌트 목록
|
export * from "./types";
|
||||||
// ============================================
|
|
||||||
// 4단계에서 추가될 컴포넌트들:
|
|
||||||
// - pop-card-list: 카드형 리스트
|
|
||||||
// - pop-touch-button: 터치 버튼
|
|
||||||
// - pop-scanner-input: 스캐너 입력
|
|
||||||
// - pop-status-badge: 상태 배지
|
|
||||||
|
|
||||||
// 예시: 컴포넌트가 추가되면 다음과 같이 export
|
// POP 컴포넌트 등록
|
||||||
// export * from "./pop-card-list";
|
import "./pop-text";
|
||||||
// export * from "./pop-touch-button";
|
|
||||||
// export * from "./pop-scanner-input";
|
|
||||||
// export * from "./pop-status-badge";
|
|
||||||
|
|
||||||
// 현재는 빈 export (컴포넌트 개발 전)
|
// 향후 추가될 컴포넌트들:
|
||||||
export { };
|
// import "./pop-field";
|
||||||
|
// import "./pop-button";
|
||||||
|
// import "./pop-list";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,831 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ko } from "date-fns/locale";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
||||||
|
import {
|
||||||
|
FontSize,
|
||||||
|
FontWeight,
|
||||||
|
TextAlign,
|
||||||
|
ObjectFit,
|
||||||
|
VerticalAlign,
|
||||||
|
FONT_SIZE_LABELS,
|
||||||
|
FONT_WEIGHT_LABELS,
|
||||||
|
OBJECT_FIT_LABELS,
|
||||||
|
FONT_SIZE_CLASSES,
|
||||||
|
FONT_WEIGHT_CLASSES,
|
||||||
|
TEXT_ALIGN_CLASSES,
|
||||||
|
VERTICAL_ALIGN_LABELS,
|
||||||
|
VERTICAL_ALIGN_CLASSES,
|
||||||
|
JUSTIFY_CLASSES,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 타입 정의
|
||||||
|
// ========================================
|
||||||
|
export type PopTextType = "text" | "datetime" | "image" | "title";
|
||||||
|
|
||||||
|
// datetime 빌더 설정 타입
|
||||||
|
export interface DateTimeBuilderConfig {
|
||||||
|
// 날짜 요소
|
||||||
|
showYear?: boolean;
|
||||||
|
showMonth?: boolean;
|
||||||
|
showDay?: boolean;
|
||||||
|
showWeekday?: boolean;
|
||||||
|
// 시간 요소
|
||||||
|
showHour?: boolean;
|
||||||
|
showMinute?: boolean;
|
||||||
|
showSecond?: boolean;
|
||||||
|
// 표기 방식
|
||||||
|
useKorean?: boolean; // true: 한글 (02월 04일), false: 숫자 (02/04)
|
||||||
|
// 구분자
|
||||||
|
dateSeparator?: string; // "-", "/", "."
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopTextConfig {
|
||||||
|
textType: PopTextType;
|
||||||
|
content?: string;
|
||||||
|
dateFormat?: string; // 기존 호환용 (deprecated)
|
||||||
|
dateTimeConfig?: DateTimeBuilderConfig; // 새로운 빌더 설정
|
||||||
|
isRealtime?: boolean;
|
||||||
|
imageUrl?: string;
|
||||||
|
objectFit?: ObjectFit;
|
||||||
|
imageScale?: number; // 이미지 크기 조정 (10-100%)
|
||||||
|
fontSize?: FontSize;
|
||||||
|
fontWeight?: FontWeight;
|
||||||
|
textAlign?: TextAlign;
|
||||||
|
verticalAlign?: VerticalAlign; // 상하 정렬
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEXT_TYPE_LABELS: Record<PopTextType, string> = {
|
||||||
|
text: "일반 텍스트",
|
||||||
|
datetime: "시간/날짜",
|
||||||
|
image: "이미지",
|
||||||
|
title: "제목",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// datetime 포맷 빌드 함수
|
||||||
|
// ========================================
|
||||||
|
function buildDateTimeFormat(config?: DateTimeBuilderConfig): string {
|
||||||
|
// 설정이 없으면 기본값 (시:분:초)
|
||||||
|
if (!config) return "HH:mm:ss";
|
||||||
|
|
||||||
|
const sep = config.dateSeparator || "-";
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// 날짜 부분 조합
|
||||||
|
const hasDateParts = config.showYear || config.showMonth || config.showDay;
|
||||||
|
if (hasDateParts) {
|
||||||
|
const dateParts: string[] = [];
|
||||||
|
if (config.showYear) dateParts.push(config.useKorean ? "yyyy년" : "yyyy");
|
||||||
|
if (config.showMonth) dateParts.push(config.useKorean ? "MM월" : "MM");
|
||||||
|
if (config.showDay) dateParts.push(config.useKorean ? "dd일" : "dd");
|
||||||
|
|
||||||
|
// 한글 모드: 공백으로 연결, 숫자 모드: 구분자로 연결
|
||||||
|
parts.push(config.useKorean ? dateParts.join(" ") : dateParts.join(sep));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 요일
|
||||||
|
if (config.showWeekday) {
|
||||||
|
parts.push(config.useKorean ? "(EEEE)" : "(EEE)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시간 부분 조합
|
||||||
|
const timeParts: string[] = [];
|
||||||
|
if (config.showHour) timeParts.push(config.useKorean ? "HH시" : "HH");
|
||||||
|
if (config.showMinute) timeParts.push(config.useKorean ? "mm분" : "mm");
|
||||||
|
if (config.showSecond) timeParts.push(config.useKorean ? "ss초" : "ss");
|
||||||
|
|
||||||
|
if (timeParts.length > 0) {
|
||||||
|
// 한글 모드: 공백으로 연결, 숫자 모드: 콜론으로 연결
|
||||||
|
parts.push(config.useKorean ? timeParts.join(" ") : timeParts.join(":"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 아무것도 선택 안 했으면 기본값
|
||||||
|
return parts.join(" ") || "HH:mm:ss";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 메인 컴포넌트
|
||||||
|
// ========================================
|
||||||
|
interface PopTextComponentProps {
|
||||||
|
config?: PopTextConfig;
|
||||||
|
label?: string;
|
||||||
|
isDesignMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopTextComponent({
|
||||||
|
config,
|
||||||
|
label,
|
||||||
|
isDesignMode,
|
||||||
|
}: PopTextComponentProps) {
|
||||||
|
const textType = config?.textType || "text";
|
||||||
|
|
||||||
|
if (isDesignMode) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<DesignModePreview config={config} label={label} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 렌더링
|
||||||
|
switch (textType) {
|
||||||
|
case "datetime":
|
||||||
|
return <DateTimeDisplay config={config} />;
|
||||||
|
case "image":
|
||||||
|
return <ImageDisplay config={config} />;
|
||||||
|
case "title":
|
||||||
|
return <TitleDisplay config={config} label={label} />;
|
||||||
|
default:
|
||||||
|
return <TextDisplay config={config} label={label} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디자인 모드 미리보기 (실제 설정값 표시)
|
||||||
|
function DesignModePreview({
|
||||||
|
config,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
config?: PopTextConfig;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
|
const textType = config?.textType || "text";
|
||||||
|
|
||||||
|
// 공통 정렬 래퍼 클래스 (상하좌우 정렬)
|
||||||
|
const alignWrapperClass = cn(
|
||||||
|
"flex w-full h-full",
|
||||||
|
VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"],
|
||||||
|
JUSTIFY_CLASSES[config?.textAlign || "left"]
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (textType) {
|
||||||
|
case "datetime":
|
||||||
|
// 실시간 시간 미리보기
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<DateTimePreview config={config} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "image":
|
||||||
|
// 이미지 미리보기
|
||||||
|
if (!config?.imageUrl) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center border border-dashed border-gray-300 text-[10px] text-gray-400">
|
||||||
|
이미지 URL 없음
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 이미지도 정렬 래퍼 적용
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<img
|
||||||
|
src={config.imageUrl}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
objectFit: config.objectFit || "none",
|
||||||
|
width: `${config.imageScale || 100}%`,
|
||||||
|
height: `${config.imageScale || 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "title":
|
||||||
|
// 제목 미리보기
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"whitespace-pre-wrap",
|
||||||
|
FONT_SIZE_CLASSES[config?.fontSize || "lg"],
|
||||||
|
FONT_WEIGHT_CLASSES[config?.fontWeight || "bold"]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{config?.content || label || "제목"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
// 일반 텍스트 미리보기
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"whitespace-pre-wrap",
|
||||||
|
FONT_SIZE_CLASSES[config?.fontSize || "base"]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{config?.content || label || "텍스트"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디자인 모드용 시간 미리보기 (실시간)
|
||||||
|
function DateTimePreview({ config }: { config?: PopTextConfig }) {
|
||||||
|
const [now, setNow] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 디자인 모드에서도 실시간 업데이트 (간격 늘림)
|
||||||
|
const timer = setInterval(() => setNow(new Date()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 빌더 설정 또는 기존 dateFormat 사용 (하위 호환)
|
||||||
|
const dateFormat = config?.dateTimeConfig
|
||||||
|
? buildDateTimeFormat(config.dateTimeConfig)
|
||||||
|
: config?.dateFormat || "HH:mm:ss";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-mono text-gray-600",
|
||||||
|
FONT_SIZE_CLASSES[config?.fontSize || "base"]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{format(now, dateFormat, { locale: ko })}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시간/날짜 (실시간 지원)
|
||||||
|
function DateTimeDisplay({ config }: { config?: PopTextConfig }) {
|
||||||
|
const [now, setNow] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config?.isRealtime) return;
|
||||||
|
const timer = setInterval(() => setNow(new Date()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [config?.isRealtime]);
|
||||||
|
|
||||||
|
// 빌더 설정 또는 기존 dateFormat 사용 (하위 호환)
|
||||||
|
const dateFormat = config?.dateTimeConfig
|
||||||
|
? buildDateTimeFormat(config.dateTimeConfig)
|
||||||
|
: config?.dateFormat || "HH:mm:ss";
|
||||||
|
|
||||||
|
// 정렬 래퍼 클래스
|
||||||
|
const alignWrapperClass = cn(
|
||||||
|
"flex w-full h-full",
|
||||||
|
VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"],
|
||||||
|
JUSTIFY_CLASSES[config?.textAlign || "left"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<span
|
||||||
|
className={cn("font-mono", FONT_SIZE_CLASSES[config?.fontSize || "base"])}
|
||||||
|
>
|
||||||
|
{format(now, dateFormat, { locale: ko })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지
|
||||||
|
function ImageDisplay({ config }: { config?: PopTextConfig }) {
|
||||||
|
if (!config?.imageUrl) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center border-2 border-dashed text-xs text-gray-400">
|
||||||
|
이미지 URL 필요
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬 래퍼 클래스
|
||||||
|
const alignWrapperClass = cn(
|
||||||
|
"flex w-full h-full",
|
||||||
|
VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"],
|
||||||
|
JUSTIFY_CLASSES[config?.textAlign || "left"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<img
|
||||||
|
src={config.imageUrl}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
objectFit: config.objectFit || "none",
|
||||||
|
width: `${config?.imageScale || 100}%`,
|
||||||
|
height: `${config?.imageScale || 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 제목
|
||||||
|
function TitleDisplay({
|
||||||
|
config,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
config?: PopTextConfig;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
|
const sizeClass = FONT_SIZE_CLASSES[config?.fontSize || "base"];
|
||||||
|
const weightClass = FONT_WEIGHT_CLASSES[config?.fontWeight || "normal"];
|
||||||
|
|
||||||
|
// 정렬 래퍼 클래스
|
||||||
|
const alignWrapperClass = cn(
|
||||||
|
"flex w-full h-full",
|
||||||
|
VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"],
|
||||||
|
JUSTIFY_CLASSES[config?.textAlign || "left"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<span className={cn("whitespace-pre-wrap", sizeClass, weightClass)}>
|
||||||
|
{config?.content || label || "제목"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 텍스트
|
||||||
|
function TextDisplay({
|
||||||
|
config,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
config?: PopTextConfig;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
|
const sizeClass = FONT_SIZE_CLASSES[config?.fontSize || "base"];
|
||||||
|
|
||||||
|
// 정렬 래퍼 클래스
|
||||||
|
const alignWrapperClass = cn(
|
||||||
|
"flex w-full h-full",
|
||||||
|
VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"],
|
||||||
|
JUSTIFY_CLASSES[config?.textAlign || "left"]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={alignWrapperClass}>
|
||||||
|
<span className={cn("whitespace-pre-wrap", sizeClass)}>
|
||||||
|
{config?.content || label || "텍스트"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 설정 패널
|
||||||
|
// ========================================
|
||||||
|
interface PopTextConfigPanelProps {
|
||||||
|
config: PopTextConfig;
|
||||||
|
onUpdate: (config: PopTextConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PopTextConfigPanel({
|
||||||
|
config,
|
||||||
|
onUpdate,
|
||||||
|
}: PopTextConfigPanelProps) {
|
||||||
|
const textType = config?.textType || "text";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 텍스트 타입 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">텍스트 타입</Label>
|
||||||
|
<Select
|
||||||
|
value={textType}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onUpdate({ ...config, textType: v as PopTextType })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(TEXT_TYPE_LABELS).map(([key, label]) => (
|
||||||
|
<SelectItem key={key} value={key} className="text-xs">
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 서브타입별 설정 */}
|
||||||
|
{textType === "text" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">내용</Label>
|
||||||
|
<Textarea
|
||||||
|
value={config?.content || ""}
|
||||||
|
onChange={(e) => onUpdate({ ...config, content: e.target.value })}
|
||||||
|
placeholder="여러 줄 입력 가능"
|
||||||
|
rows={3}
|
||||||
|
className="text-xs resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FontSizeSelect config={config} onUpdate={onUpdate} />
|
||||||
|
<AlignmentSelect config={config} onUpdate={onUpdate} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{textType === "datetime" && (
|
||||||
|
<>
|
||||||
|
{/* 포맷 빌더 UI */}
|
||||||
|
<DateTimeFormatBuilder config={config} onUpdate={onUpdate} />
|
||||||
|
|
||||||
|
{/* 실시간 업데이트 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={config?.isRealtime ?? true}
|
||||||
|
onCheckedChange={(v) => onUpdate({ ...config, isRealtime: v })}
|
||||||
|
/>
|
||||||
|
<Label className="text-xs">실시간 업데이트</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FontSizeSelect config={config} onUpdate={onUpdate} />
|
||||||
|
<AlignmentSelect config={config} onUpdate={onUpdate} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{textType === "image" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">이미지 URL</Label>
|
||||||
|
<Input
|
||||||
|
value={config?.imageUrl || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate({ ...config, imageUrl: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="https://..."
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">맞춤</Label>
|
||||||
|
<Select
|
||||||
|
value={config?.objectFit || "none"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onUpdate({ ...config, objectFit: v as ObjectFit })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(OBJECT_FIT_LABELS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value} className="text-xs">
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">
|
||||||
|
크기: {config?.imageScale || 100}%
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={10}
|
||||||
|
max={100}
|
||||||
|
step={10}
|
||||||
|
value={config?.imageScale || 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate({ ...config, imageScale: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AlignmentSelect config={config} onUpdate={onUpdate} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{textType === "title" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">제목 텍스트</Label>
|
||||||
|
<Input
|
||||||
|
value={config?.content || ""}
|
||||||
|
onChange={(e) => onUpdate({ ...config, content: e.target.value })}
|
||||||
|
placeholder="제목 입력"
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FontSizeSelect config={config} onUpdate={onUpdate} />
|
||||||
|
<FontWeightSelect config={config} onUpdate={onUpdate} />
|
||||||
|
<AlignmentSelect config={config} onUpdate={onUpdate} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공통: 글자 크기
|
||||||
|
function FontSizeSelect({ config, onUpdate }: PopTextConfigPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">글자 크기</Label>
|
||||||
|
<Select
|
||||||
|
value={config?.fontSize || "base"}
|
||||||
|
onValueChange={(v) => onUpdate({ ...config, fontSize: v as FontSize })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(FONT_SIZE_LABELS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value} className="text-xs">
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공통: 글자 굵기
|
||||||
|
function FontWeightSelect({ config, onUpdate }: PopTextConfigPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">글자 굵기</Label>
|
||||||
|
<Select
|
||||||
|
value={config?.fontWeight || "normal"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onUpdate({ ...config, fontWeight: v as FontWeight })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(FONT_WEIGHT_LABELS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value} className="text-xs">
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// datetime 포맷 빌더 UI
|
||||||
|
function DateTimeFormatBuilder({ config, onUpdate }: PopTextConfigPanelProps) {
|
||||||
|
// 기본값 설정 (시:분:초)
|
||||||
|
const dtConfig: DateTimeBuilderConfig = config?.dateTimeConfig || {
|
||||||
|
showHour: true,
|
||||||
|
showMinute: true,
|
||||||
|
showSecond: true,
|
||||||
|
useKorean: false,
|
||||||
|
dateSeparator: "-",
|
||||||
|
};
|
||||||
|
|
||||||
|
// dateTimeConfig 업데이트 헬퍼
|
||||||
|
const updateDtConfig = (updates: Partial<DateTimeBuilderConfig>) => {
|
||||||
|
onUpdate({
|
||||||
|
...config,
|
||||||
|
dateTimeConfig: { ...dtConfig, ...updates },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 요소가 하나라도 선택되었는지
|
||||||
|
const hasDateParts = dtConfig.showYear || dtConfig.showMonth || dtConfig.showDay;
|
||||||
|
|
||||||
|
// 미리보기용 포맷 생성
|
||||||
|
const previewFormat = buildDateTimeFormat(dtConfig);
|
||||||
|
const previewText = format(new Date(), previewFormat, { locale: ko });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 날짜 요소 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">날짜 요소</Label>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showYear || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showYear: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
년
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showMonth || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showMonth: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
월
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showDay || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showDay: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
일
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showWeekday || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showWeekday: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
요일
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 시간 요소 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">시간 요소</Label>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showHour || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showHour: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
시
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showMinute || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showMinute: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
분
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Checkbox
|
||||||
|
checked={dtConfig.showSecond || false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateDtConfig({ showSecond: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
초
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 표기 방식 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">표기 방식</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={dtConfig.useKorean ? "korean" : "number"}
|
||||||
|
onValueChange={(v) => updateDtConfig({ useKorean: v === "korean" })}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<RadioGroupItem value="number" />
|
||||||
|
숫자 (02/04)
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs">
|
||||||
|
<RadioGroupItem value="korean" />
|
||||||
|
한글 (02월 04일)
|
||||||
|
</label>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 구분자 (숫자 모드 + 날짜 요소가 있을 때만) */}
|
||||||
|
{!dtConfig.useKorean && hasDateParts && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">구분자</Label>
|
||||||
|
<Select
|
||||||
|
value={dtConfig.dateSeparator || "-"}
|
||||||
|
onValueChange={(v) => updateDtConfig({ dateSeparator: v })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="-" className="text-xs">
|
||||||
|
- (하이픈)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="/" className="text-xs">
|
||||||
|
/ (슬래시)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="." className="text-xs">
|
||||||
|
. (점)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 미리보기 */}
|
||||||
|
<div className="rounded border bg-muted/50 p-2">
|
||||||
|
<span className="text-[10px] text-muted-foreground">미리보기: </span>
|
||||||
|
<span className="text-xs font-medium">{previewText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공통: 정렬 (좌우 + 상하)
|
||||||
|
function AlignmentSelect({ config, onUpdate }: PopTextConfigPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-0">
|
||||||
|
<Label className="text-xs">정렬</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{/* 좌우 정렬 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-[10px] text-muted-foreground">좌/우</span>
|
||||||
|
<Select
|
||||||
|
value={config?.textAlign || "left"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onUpdate({ ...config, textAlign: v as TextAlign })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="좌우" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left" className="text-xs">
|
||||||
|
왼쪽
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="center" className="text-xs">
|
||||||
|
가운데
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="right" className="text-xs">
|
||||||
|
오른쪽
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 상하 정렬 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-[10px] text-muted-foreground">상/하</span>
|
||||||
|
<Select
|
||||||
|
value={config?.verticalAlign || "center"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onUpdate({ ...config, verticalAlign: v as VerticalAlign })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="상하" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="top" className="text-xs">
|
||||||
|
위
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="center" className="text-xs">
|
||||||
|
가운데
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="bottom" className="text-xs">
|
||||||
|
아래
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 디자이너 미리보기 컴포넌트
|
||||||
|
// ========================================
|
||||||
|
function PopTextPreviewComponent({ config }: { config?: PopTextConfig }) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center overflow-hidden">
|
||||||
|
<DesignModePreview config={config} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 레지스트리 등록
|
||||||
|
// ========================================
|
||||||
|
PopComponentRegistry.registerComponent({
|
||||||
|
id: "pop-text",
|
||||||
|
name: "텍스트",
|
||||||
|
description: "텍스트, 시간, 이미지 표시",
|
||||||
|
category: "display",
|
||||||
|
icon: "FileText",
|
||||||
|
component: PopTextComponent,
|
||||||
|
configPanel: PopTextConfigPanel,
|
||||||
|
preview: PopTextPreviewComponent,
|
||||||
|
defaultProps: { textType: "text", fontSize: "base" },
|
||||||
|
touchOptimized: true,
|
||||||
|
supportedDevices: ["mobile", "tablet"],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* POP 컴포넌트 공통 타입 정의
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ===== 스타일 관련 공통 타입 =====
|
||||||
|
// ===== 폰트 사이즈, 폰트 굵기, 텍스트 정렬, 이미지 맞춤 방식 정의 =====
|
||||||
|
|
||||||
|
export type FontSize = "xs" | "sm" | "base" | "lg" | "xl";
|
||||||
|
export type FontWeight = "normal" | "medium" | "bold";
|
||||||
|
export type TextAlign = "left" | "center" | "right" | "justify";
|
||||||
|
export type ObjectFit = "contain" | "cover" | "fill" | "none";
|
||||||
|
|
||||||
|
// ===== 라벨 매핑 =====
|
||||||
|
|
||||||
|
export const FONT_SIZE_LABELS: Record<FontSize, string> = {
|
||||||
|
xs: "아주 작게",
|
||||||
|
sm: "작게",
|
||||||
|
base: "보통",
|
||||||
|
lg: "크게",
|
||||||
|
xl: "매우 크게",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FONT_WEIGHT_LABELS: Record<FontWeight, string> = {
|
||||||
|
normal: "보통",
|
||||||
|
medium: "중간",
|
||||||
|
bold: "굵게",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEXT_ALIGN_LABELS: Record<TextAlign, string> = {
|
||||||
|
left: "왼쪽",
|
||||||
|
center: "가운데",
|
||||||
|
right: "오른쪽",
|
||||||
|
justify: "양쪽 정렬",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OBJECT_FIT_LABELS: Record<ObjectFit, string> = {
|
||||||
|
contain: "비율 유지",
|
||||||
|
cover: "채우기",
|
||||||
|
fill: "늘리기",
|
||||||
|
none: "원본 크기",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Tailwind 클래스 매핑 =====
|
||||||
|
|
||||||
|
// 작게는 Tailwind 기본, 크게는 base(16px) 기준 2배씩: 12px → 14px → 16px → 32px → 64px
|
||||||
|
export const FONT_SIZE_CLASSES: Record<FontSize, string> = {
|
||||||
|
xs: "text-xs",
|
||||||
|
sm: "text-sm",
|
||||||
|
base: "text-base",
|
||||||
|
lg: "text-[32px]",
|
||||||
|
xl: "text-[64px]",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FONT_WEIGHT_CLASSES: Record<FontWeight, string> = {
|
||||||
|
normal: "font-normal",
|
||||||
|
medium: "font-medium",
|
||||||
|
bold: "font-bold",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEXT_ALIGN_CLASSES: Record<TextAlign, string> = {
|
||||||
|
left: "text-left",
|
||||||
|
center: "text-center",
|
||||||
|
right: "text-right",
|
||||||
|
justify: "text-justify",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 상하 정렬 =====
|
||||||
|
|
||||||
|
export type VerticalAlign = "top" | "center" | "bottom";
|
||||||
|
|
||||||
|
export const VERTICAL_ALIGN_LABELS: Record<VerticalAlign, string> = {
|
||||||
|
top: "위",
|
||||||
|
center: "가운데",
|
||||||
|
bottom: "아래",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VERTICAL_ALIGN_CLASSES: Record<VerticalAlign, string> = {
|
||||||
|
top: "items-start",
|
||||||
|
center: "items-center",
|
||||||
|
bottom: "items-end",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 좌우 정렬 (justify용 - flex 컨테이너에서 사용)
|
||||||
|
export const JUSTIFY_CLASSES: Record<string, string> = {
|
||||||
|
left: "justify-start",
|
||||||
|
center: "justify-center",
|
||||||
|
right: "justify-end",
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue