401 lines
15 KiB
TypeScript
401 lines
15 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 { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Upload, File, X } from "lucide-react";
|
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
|
import { WidgetComponent, FileTypeConfig } from "@/types/screen";
|
|
|
|
export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|
component,
|
|
onUpdateComponent,
|
|
onUpdateProperty,
|
|
}) => {
|
|
const widget = component as WidgetComponent;
|
|
const config = (widget.webTypeConfig as FileTypeConfig) || {};
|
|
|
|
// 로컬 상태
|
|
const [localConfig, setLocalConfig] = useState<FileTypeConfig>({
|
|
multiple: config.multiple || false,
|
|
maxFileSize: config.maxFileSize || 10, // MB
|
|
maxFiles: config.maxFiles || 1,
|
|
acceptedTypes: config.acceptedTypes || [],
|
|
showPreview: config.showPreview !== false, // 기본값 true
|
|
showProgress: config.showProgress !== false, // 기본값 true
|
|
dragAndDrop: config.dragAndDrop !== false, // 기본값 true
|
|
required: config.required || false,
|
|
readonly: config.readonly || false,
|
|
uploadText: config.uploadText || "파일을 선택하거나 여기에 드래그하세요",
|
|
browseText: config.browseText || "파일 선택",
|
|
});
|
|
|
|
// 새 파일 타입 추가용 상태
|
|
const [newFileType, setNewFileType] = useState("");
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
useEffect(() => {
|
|
const currentConfig = (widget.webTypeConfig as FileTypeConfig) || {};
|
|
setLocalConfig({
|
|
multiple: currentConfig.multiple || false,
|
|
maxFileSize: currentConfig.maxFileSize || 10,
|
|
maxFiles: currentConfig.maxFiles || 1,
|
|
acceptedTypes: currentConfig.acceptedTypes || [],
|
|
showPreview: currentConfig.showPreview !== false,
|
|
showProgress: currentConfig.showProgress !== false,
|
|
dragAndDrop: currentConfig.dragAndDrop !== false,
|
|
required: currentConfig.required || false,
|
|
readonly: currentConfig.readonly || false,
|
|
uploadText: currentConfig.uploadText || "파일을 선택하거나 여기에 드래그하세요",
|
|
browseText: currentConfig.browseText || "파일 선택",
|
|
});
|
|
}, [widget.webTypeConfig]);
|
|
|
|
// 설정 업데이트 핸들러
|
|
const updateConfig = (field: keyof FileTypeConfig, value: any) => {
|
|
const newConfig = { ...localConfig, [field]: value };
|
|
setLocalConfig(newConfig);
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|
};
|
|
|
|
// 파일 타입 추가
|
|
const addFileType = () => {
|
|
if (!newFileType.trim()) return;
|
|
|
|
let extension = newFileType.trim();
|
|
if (!extension.startsWith(".")) {
|
|
extension = "." + extension;
|
|
}
|
|
|
|
if (!localConfig.acceptedTypes.includes(extension)) {
|
|
const newTypes = [...localConfig.acceptedTypes, extension];
|
|
updateConfig("acceptedTypes", newTypes);
|
|
}
|
|
|
|
setNewFileType("");
|
|
};
|
|
|
|
// 파일 타입 제거
|
|
const removeFileType = (typeToRemove: string) => {
|
|
const newTypes = localConfig.acceptedTypes.filter((type) => type !== typeToRemove);
|
|
updateConfig("acceptedTypes", newTypes);
|
|
};
|
|
|
|
// 기본 파일 타입 세트
|
|
const defaultFileTypeSets = {
|
|
images: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
|
|
documents: [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt"],
|
|
archives: [".zip", ".rar", ".7z", ".tar", ".gz"],
|
|
media: [".mp4", ".avi", ".mov", ".wmv", ".mp3", ".wav", ".flac"],
|
|
web: [".html", ".css", ".js", ".json", ".xml"],
|
|
all: ["*"],
|
|
};
|
|
|
|
const applyFileTypeSet = (setName: keyof typeof defaultFileTypeSets) => {
|
|
updateConfig("acceptedTypes", defaultFileTypeSets[setName]);
|
|
};
|
|
|
|
// 파일 크기 단위 변환
|
|
const formatFileSize = (sizeInMB: number) => {
|
|
if (sizeInMB < 1) {
|
|
return `${(sizeInMB * 1024).toFixed(0)}KB`;
|
|
} else if (sizeInMB >= 1024) {
|
|
return `${(sizeInMB / 1024).toFixed(1)}GB`;
|
|
} else {
|
|
return `${sizeInMB}MB`;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|
<Upload 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="uploadText" className="text-xs">
|
|
업로드 안내 텍스트
|
|
</Label>
|
|
<Input
|
|
id="uploadText"
|
|
value={localConfig.uploadText || ""}
|
|
onChange={(e) => updateConfig("uploadText", e.target.value)}
|
|
placeholder="파일을 선택하거나 여기에 드래그하세요"
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="browseText" className="text-xs">
|
|
찾아보기 버튼 텍스트
|
|
</Label>
|
|
<Input
|
|
id="browseText"
|
|
value={localConfig.browseText || ""}
|
|
onChange={(e) => updateConfig("browseText", e.target.value)}
|
|
placeholder="파일 선택"
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="multiple" className="text-xs">
|
|
다중 파일 선택
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">여러 파일을 한 번에 선택할 수 있습니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="multiple"
|
|
checked={localConfig.multiple || false}
|
|
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="dragAndDrop" className="text-xs">
|
|
드래그 앤 드롭
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">파일을 드래그해서 업로드할 수 있습니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="dragAndDrop"
|
|
checked={localConfig.dragAndDrop !== false}
|
|
onCheckedChange={(checked) => updateConfig("dragAndDrop", checked)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 파일 제한 설정 */}
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium">파일 제한</h4>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxFileSize" className="text-xs">
|
|
최대 파일 크기: {formatFileSize(localConfig.maxFileSize || 10)}
|
|
</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
id="maxFileSize"
|
|
type="number"
|
|
value={localConfig.maxFileSize || 10}
|
|
onChange={(e) => updateConfig("maxFileSize", parseFloat(e.target.value))}
|
|
min={0.1}
|
|
max={1024}
|
|
step={0.1}
|
|
className="flex-1 text-xs"
|
|
/>
|
|
<span className="text-muted-foreground text-xs">MB</span>
|
|
</div>
|
|
</div>
|
|
|
|
{localConfig.multiple && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxFiles" className="text-xs">
|
|
최대 파일 개수
|
|
</Label>
|
|
<Input
|
|
id="maxFiles"
|
|
type="number"
|
|
value={localConfig.maxFiles || 1}
|
|
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
|
|
min={1}
|
|
max={100}
|
|
className="text-xs"
|
|
/>
|
|
</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 size="sm" variant="outline" onClick={() => applyFileTypeSet("images")} className="text-xs">
|
|
이미지
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => applyFileTypeSet("documents")} className="text-xs">
|
|
문서
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => applyFileTypeSet("archives")} className="text-xs">
|
|
압축파일
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => applyFileTypeSet("media")} className="text-xs">
|
|
미디어
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => applyFileTypeSet("web")} className="text-xs">
|
|
웹파일
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => applyFileTypeSet("all")} className="text-xs">
|
|
모든 파일
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 개별 파일 타입 추가 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">개별 타입 추가</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={newFileType}
|
|
onChange={(e) => setNewFileType(e.target.value)}
|
|
placeholder=".pdf 또는 pdf"
|
|
className="flex-1 text-xs"
|
|
/>
|
|
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">
|
|
추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 현재 허용된 타입 목록 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">현재 허용된 타입 ({localConfig.acceptedTypes.length}개)</Label>
|
|
<div className="flex min-h-8 flex-wrap gap-1 rounded-md border p-2">
|
|
{localConfig.acceptedTypes.length === 0 ? (
|
|
<span className="text-muted-foreground text-xs">모든 파일 타입 허용</span>
|
|
) : (
|
|
localConfig.acceptedTypes.map((type, index) => (
|
|
<Badge key={index} variant="secondary" className="text-xs">
|
|
{type}
|
|
<button type="button" onClick={() => removeFileType(type)} className="hover:text-destructive ml-1">
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</Badge>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* UI 설정 */}
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium">UI 설정</h4>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="showPreview" className="text-xs">
|
|
파일 미리보기
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">선택된 파일의 미리보기를 표시합니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="showPreview"
|
|
checked={localConfig.showPreview !== false}
|
|
onCheckedChange={(checked) => updateConfig("showPreview", checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="showProgress" className="text-xs">
|
|
업로드 진행률
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">업로드 진행 상황을 표시합니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="showProgress"
|
|
checked={localConfig.showProgress !== false}
|
|
onCheckedChange={(checked) => updateConfig("showProgress", checked)}
|
|
/>
|
|
</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">
|
|
<div
|
|
className={`space-y-2 rounded-md border-2 border-dashed p-4 text-center ${
|
|
localConfig.dragAndDrop && !localConfig.readonly
|
|
? "border-gray-300 hover:border-gray-400"
|
|
: "border-gray-200"
|
|
}`}
|
|
>
|
|
<File className="mx-auto h-8 w-8 text-gray-400" />
|
|
<p className="text-xs text-gray-600">{localConfig.uploadText}</p>
|
|
<Button size="sm" variant="outline" disabled={localConfig.readonly} className="text-xs">
|
|
{localConfig.browseText}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="text-muted-foreground mt-2 space-y-1 text-xs">
|
|
<div>
|
|
최대 크기: {formatFileSize(localConfig.maxFileSize || 10)}
|
|
{localConfig.multiple && ` • 최대 ${localConfig.maxFiles}개 파일`}
|
|
</div>
|
|
<div>
|
|
허용 타입:{" "}
|
|
{localConfig.acceptedTypes.length === 0
|
|
? "모든 파일"
|
|
: localConfig.acceptedTypes.slice(0, 3).join(", ") +
|
|
(localConfig.acceptedTypes.length > 3 ? ` 외 ${localConfig.acceptedTypes.length - 3}개` : "")}
|
|
</div>
|
|
<div>
|
|
{localConfig.dragAndDrop && "드래그 앤 드롭 • "}
|
|
{localConfig.showPreview && "미리보기 • "}
|
|
{localConfig.showProgress && "진행률 표시 • "}
|
|
{localConfig.required && "필수"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
FileConfigPanel.displayName = "FileConfigPanel";
|
|
|
|
|