ERP-node/frontend/components/screen/config-panels/TextareaConfigPanel.tsx

359 lines
13 KiB
TypeScript
Raw Normal View History

2025-09-09 14:29:04 +09:00
"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";