359 lines
13 KiB
TypeScript
359 lines
13 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import React, { useState, useEffect } from "react";
|
|||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|||
|
|
import { Input } from "@/components/ui/input";
|
|||
|
|
import { Label } from "@/components/ui/label";
|
|||
|
|
import { Switch } from "@/components/ui/switch";
|
|||
|
|
import { Textarea } from "@/components/ui/textarea";
|
|||
|
|
import { Slider } from "@/components/ui/slider";
|
|||
|
|
import { AlignLeft } from "lucide-react";
|
|||
|
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
|||
|
|
import { WidgetComponent, TextareaTypeConfig } from "@/types/screen";
|
|||
|
|
|
|||
|
|
export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
|
|
component,
|
|||
|
|
onUpdateComponent,
|
|||
|
|
onUpdateProperty,
|
|||
|
|
}) => {
|
|||
|
|
const widget = component as WidgetComponent;
|
|||
|
|
const config = (widget.webTypeConfig as TextareaTypeConfig) || {};
|
|||
|
|
|
|||
|
|
// 로컬 상태
|
|||
|
|
const [localConfig, setLocalConfig] = useState<TextareaTypeConfig>({
|
|||
|
|
rows: config.rows || 4,
|
|||
|
|
cols: config.cols || undefined,
|
|||
|
|
minLength: config.minLength || undefined,
|
|||
|
|
maxLength: config.maxLength || undefined,
|
|||
|
|
placeholder: config.placeholder || "",
|
|||
|
|
defaultValue: config.defaultValue || "",
|
|||
|
|
required: config.required || false,
|
|||
|
|
readonly: config.readonly || false,
|
|||
|
|
resizable: config.resizable !== false, // 기본값 true
|
|||
|
|
autoHeight: config.autoHeight || false,
|
|||
|
|
showCharCount: config.showCharCount || false,
|
|||
|
|
wrap: config.wrap || "soft",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|||
|
|
useEffect(() => {
|
|||
|
|
const currentConfig = (widget.webTypeConfig as TextareaTypeConfig) || {};
|
|||
|
|
setLocalConfig({
|
|||
|
|
rows: currentConfig.rows || 4,
|
|||
|
|
cols: currentConfig.cols || undefined,
|
|||
|
|
minLength: currentConfig.minLength || undefined,
|
|||
|
|
maxLength: currentConfig.maxLength || undefined,
|
|||
|
|
placeholder: currentConfig.placeholder || "",
|
|||
|
|
defaultValue: currentConfig.defaultValue || "",
|
|||
|
|
required: currentConfig.required || false,
|
|||
|
|
readonly: currentConfig.readonly || false,
|
|||
|
|
resizable: currentConfig.resizable !== false,
|
|||
|
|
autoHeight: currentConfig.autoHeight || false,
|
|||
|
|
showCharCount: currentConfig.showCharCount || false,
|
|||
|
|
wrap: currentConfig.wrap || "soft",
|
|||
|
|
});
|
|||
|
|
}, [widget.webTypeConfig]);
|
|||
|
|
|
|||
|
|
// 설정 업데이트 핸들러
|
|||
|
|
const updateConfig = (field: keyof TextareaTypeConfig, value: any) => {
|
|||
|
|
const newConfig = { ...localConfig, [field]: value };
|
|||
|
|
setLocalConfig(newConfig);
|
|||
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 현재 문자 수 계산
|
|||
|
|
const currentCharCount = (localConfig.defaultValue || "").length;
|
|||
|
|
const isOverLimit = localConfig.maxLength ? currentCharCount > localConfig.maxLength : false;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|||
|
|
<AlignLeft className="h-4 w-4" />
|
|||
|
|
텍스트영역 설정
|
|||
|
|
</CardTitle>
|
|||
|
|
<CardDescription className="text-xs">여러 줄 텍스트 입력 영역의 세부 설정을 관리합니다.</CardDescription>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="space-y-4">
|
|||
|
|
{/* 기본 설정 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">기본 설정</h4>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="placeholder" className="text-xs">
|
|||
|
|
플레이스홀더
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="placeholder"
|
|||
|
|
value={localConfig.placeholder || ""}
|
|||
|
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
|||
|
|
placeholder="내용을 입력하세요"
|
|||
|
|
className="text-xs"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="defaultValue" className="text-xs">
|
|||
|
|
기본값
|
|||
|
|
</Label>
|
|||
|
|
<Textarea
|
|||
|
|
id="defaultValue"
|
|||
|
|
value={localConfig.defaultValue || ""}
|
|||
|
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
|||
|
|
placeholder="기본 텍스트 내용"
|
|||
|
|
className="text-xs"
|
|||
|
|
rows={3}
|
|||
|
|
/>
|
|||
|
|
{localConfig.showCharCount && (
|
|||
|
|
<div className={`text-xs ${isOverLimit ? "text-red-500" : "text-muted-foreground"}`}>
|
|||
|
|
{currentCharCount}
|
|||
|
|
{localConfig.maxLength && ` / ${localConfig.maxLength}`} 글자
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 크기 설정 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">크기 설정</h4>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="rows" className="text-xs">
|
|||
|
|
행 수: {localConfig.rows}
|
|||
|
|
</Label>
|
|||
|
|
<Slider
|
|||
|
|
id="rows"
|
|||
|
|
min={1}
|
|||
|
|
max={20}
|
|||
|
|
step={1}
|
|||
|
|
value={[localConfig.rows || 4]}
|
|||
|
|
onValueChange={([value]) => updateConfig("rows", value)}
|
|||
|
|
className="w-full"
|
|||
|
|
/>
|
|||
|
|
<div className="text-muted-foreground flex justify-between text-xs">
|
|||
|
|
<span>1줄</span>
|
|||
|
|
<span>20줄</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="cols" className="text-xs">
|
|||
|
|
열 수 (선택사항)
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="cols"
|
|||
|
|
type="number"
|
|||
|
|
value={localConfig.cols || ""}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const value = e.target.value ? parseInt(e.target.value) : undefined;
|
|||
|
|
updateConfig("cols", value);
|
|||
|
|
}}
|
|||
|
|
placeholder="자동 (CSS로 제어)"
|
|||
|
|
min={10}
|
|||
|
|
max={200}
|
|||
|
|
className="text-xs"
|
|||
|
|
/>
|
|||
|
|
<p className="text-muted-foreground text-xs">비워두면 CSS width로 제어됩니다.</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<Label htmlFor="resizable" className="text-xs">
|
|||
|
|
크기 조절 가능
|
|||
|
|
</Label>
|
|||
|
|
<p className="text-muted-foreground text-xs">사용자가 텍스트영역 크기를 조절할 수 있습니다.</p>
|
|||
|
|
</div>
|
|||
|
|
<Switch
|
|||
|
|
id="resizable"
|
|||
|
|
checked={localConfig.resizable || false}
|
|||
|
|
onCheckedChange={(checked) => updateConfig("resizable", checked)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<Label htmlFor="autoHeight" className="text-xs">
|
|||
|
|
자동 높이
|
|||
|
|
</Label>
|
|||
|
|
<p className="text-muted-foreground text-xs">내용에 따라 높이가 자동으로 조절됩니다.</p>
|
|||
|
|
</div>
|
|||
|
|
<Switch
|
|||
|
|
id="autoHeight"
|
|||
|
|
checked={localConfig.autoHeight || false}
|
|||
|
|
onCheckedChange={(checked) => updateConfig("autoHeight", checked)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 텍스트 제한 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">텍스트 제한</h4>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="minLength" className="text-xs">
|
|||
|
|
최소 글자 수
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="minLength"
|
|||
|
|
type="number"
|
|||
|
|
value={localConfig.minLength || ""}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const value = e.target.value ? parseInt(e.target.value) : undefined;
|
|||
|
|
updateConfig("minLength", value);
|
|||
|
|
}}
|
|||
|
|
placeholder="제한 없음"
|
|||
|
|
min={0}
|
|||
|
|
className="text-xs"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="maxLength" className="text-xs">
|
|||
|
|
최대 글자 수
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="maxLength"
|
|||
|
|
type="number"
|
|||
|
|
value={localConfig.maxLength || ""}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const value = e.target.value ? parseInt(e.target.value) : undefined;
|
|||
|
|
updateConfig("maxLength", value);
|
|||
|
|
}}
|
|||
|
|
placeholder="제한 없음"
|
|||
|
|
min={1}
|
|||
|
|
className="text-xs"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<Label htmlFor="showCharCount" className="text-xs">
|
|||
|
|
글자 수 표시
|
|||
|
|
</Label>
|
|||
|
|
<p className="text-muted-foreground text-xs">현재 입력된 글자 수를 표시합니다.</p>
|
|||
|
|
</div>
|
|||
|
|
<Switch
|
|||
|
|
id="showCharCount"
|
|||
|
|
checked={localConfig.showCharCount || false}
|
|||
|
|
onCheckedChange={(checked) => updateConfig("showCharCount", checked)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 텍스트 줄바꿈 설정 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">줄바꿈 설정</h4>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="text-xs">줄바꿈 방식</Label>
|
|||
|
|
<div className="grid grid-cols-3 gap-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => updateConfig("wrap", "soft")}
|
|||
|
|
className={`rounded border p-2 text-xs ${
|
|||
|
|
localConfig.wrap === "soft" ? "bg-primary text-primary-foreground" : "bg-background"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
Soft
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => updateConfig("wrap", "hard")}
|
|||
|
|
className={`rounded border p-2 text-xs ${
|
|||
|
|
localConfig.wrap === "hard" ? "bg-primary text-primary-foreground" : "bg-background"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
Hard
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => updateConfig("wrap", "off")}
|
|||
|
|
className={`rounded border p-2 text-xs ${
|
|||
|
|
localConfig.wrap === "off" ? "bg-primary text-primary-foreground" : "bg-background"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
Off
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-muted-foreground text-xs">
|
|||
|
|
{localConfig.wrap === "soft" && "화면에서만 줄바꿈 (기본값)"}
|
|||
|
|
{localConfig.wrap === "hard" && "실제 텍스트에 줄바꿈 포함"}
|
|||
|
|
{localConfig.wrap === "off" && "줄바꿈 없음 (스크롤)"}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 상태 설정 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">상태 설정</h4>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<Label htmlFor="required" className="text-xs">
|
|||
|
|
필수 입력
|
|||
|
|
</Label>
|
|||
|
|
<p className="text-muted-foreground text-xs">텍스트가 입력되어야 합니다.</p>
|
|||
|
|
</div>
|
|||
|
|
<Switch
|
|||
|
|
id="required"
|
|||
|
|
checked={localConfig.required || false}
|
|||
|
|
onCheckedChange={(checked) => updateConfig("required", checked)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<Label htmlFor="readonly" className="text-xs">
|
|||
|
|
읽기 전용
|
|||
|
|
</Label>
|
|||
|
|
<p className="text-muted-foreground text-xs">텍스트를 수정할 수 없습니다.</p>
|
|||
|
|
</div>
|
|||
|
|
<Switch
|
|||
|
|
id="readonly"
|
|||
|
|
checked={localConfig.readonly || false}
|
|||
|
|
onCheckedChange={(checked) => updateConfig("readonly", checked)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 미리보기 */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h4 className="text-sm font-medium">미리보기</h4>
|
|||
|
|
<div className="bg-muted/50 rounded-md border p-3">
|
|||
|
|
<Textarea
|
|||
|
|
placeholder={localConfig.placeholder || "텍스트 입력 미리보기"}
|
|||
|
|
rows={localConfig.rows}
|
|||
|
|
cols={localConfig.cols}
|
|||
|
|
disabled={localConfig.readonly}
|
|||
|
|
required={localConfig.required}
|
|||
|
|
minLength={localConfig.minLength}
|
|||
|
|
maxLength={localConfig.maxLength}
|
|||
|
|
defaultValue={localConfig.defaultValue}
|
|||
|
|
style={{
|
|||
|
|
resize: localConfig.resizable ? "both" : "none",
|
|||
|
|
minHeight: localConfig.autoHeight ? "auto" : undefined,
|
|||
|
|
}}
|
|||
|
|
className="text-xs"
|
|||
|
|
wrap={localConfig.wrap}
|
|||
|
|
/>
|
|||
|
|
{localConfig.showCharCount && (
|
|||
|
|
<div className="text-muted-foreground mt-1 text-right text-xs">
|
|||
|
|
0{localConfig.maxLength && ` / ${localConfig.maxLength}`} 글자
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div className="text-muted-foreground mt-2 text-xs">
|
|||
|
|
{localConfig.rows}행{localConfig.cols && ` × ${localConfig.cols}열`}
|
|||
|
|
{localConfig.resizable && " • 크기조절가능"}
|
|||
|
|
{localConfig.autoHeight && " • 자동높이"}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
TextareaConfigPanel.displayName = "TextareaConfigPanel";
|
|||
|
|
|
|||
|
|
|