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";
|
||
|
||
|