832 lines
25 KiB
TypeScript
832 lines
25 KiB
TypeScript
"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"],
|
|
});
|