339 lines
13 KiB
TypeScript
339 lines
13 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useEffect, useCallback } from "react";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { FileComponent } from "@/types/screen";
|
||
|
|
import { Plus, X } from "lucide-react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
|
||
|
|
interface FileComponentConfigPanelProps {
|
||
|
|
component: FileComponent;
|
||
|
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
||
|
|
// 로컬 상태
|
||
|
|
const [localInputs, setLocalInputs] = useState({
|
||
|
|
docType: component.fileConfig.docType || "DOCUMENT",
|
||
|
|
docTypeName: component.fileConfig.docTypeName || "일반 문서",
|
||
|
|
dragDropText: component.fileConfig.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요",
|
||
|
|
maxSize: component.fileConfig.maxSize || 10,
|
||
|
|
maxFiles: component.fileConfig.maxFiles || 5,
|
||
|
|
newAcceptType: "", // 새 파일 타입 추가용
|
||
|
|
});
|
||
|
|
|
||
|
|
const [localValues, setLocalValues] = useState({
|
||
|
|
multiple: component.fileConfig.multiple ?? true,
|
||
|
|
showPreview: component.fileConfig.showPreview ?? true,
|
||
|
|
showProgress: component.fileConfig.showProgress ?? true,
|
||
|
|
});
|
||
|
|
|
||
|
|
const [acceptTypes, setAcceptTypes] = useState<string[]>(component.fileConfig.accept || []);
|
||
|
|
|
||
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||
|
|
useEffect(() => {
|
||
|
|
setLocalInputs({
|
||
|
|
docType: component.fileConfig.docType || "DOCUMENT",
|
||
|
|
docTypeName: component.fileConfig.docTypeName || "일반 문서",
|
||
|
|
dragDropText: component.fileConfig.dragDropText || "파일을 드래그하거나 클릭하여 업로드하세요",
|
||
|
|
maxSize: component.fileConfig.maxSize || 10,
|
||
|
|
maxFiles: component.fileConfig.maxFiles || 5,
|
||
|
|
newAcceptType: "",
|
||
|
|
});
|
||
|
|
|
||
|
|
setLocalValues({
|
||
|
|
multiple: component.fileConfig.multiple ?? true,
|
||
|
|
showPreview: component.fileConfig.showPreview ?? true,
|
||
|
|
showProgress: component.fileConfig.showProgress ?? true,
|
||
|
|
});
|
||
|
|
|
||
|
|
setAcceptTypes(component.fileConfig.accept || []);
|
||
|
|
}, [component.fileConfig]);
|
||
|
|
|
||
|
|
// 미리 정의된 문서 타입들
|
||
|
|
const docTypeOptions = [
|
||
|
|
{ value: "CONTRACT", label: "계약서" },
|
||
|
|
{ value: "DRAWING", label: "도면" },
|
||
|
|
{ value: "PHOTO", label: "사진" },
|
||
|
|
{ value: "DOCUMENT", label: "일반 문서" },
|
||
|
|
{ value: "REPORT", label: "보고서" },
|
||
|
|
{ value: "SPECIFICATION", label: "사양서" },
|
||
|
|
{ value: "MANUAL", label: "매뉴얼" },
|
||
|
|
{ value: "CERTIFICATE", label: "인증서" },
|
||
|
|
{ value: "OTHER", label: "기타" },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 미리 정의된 파일 타입들
|
||
|
|
const commonFileTypes = [
|
||
|
|
{ value: "image/*", label: "모든 이미지 파일" },
|
||
|
|
{ value: ".pdf", label: "PDF 파일" },
|
||
|
|
{ value: ".doc,.docx", label: "Word 문서" },
|
||
|
|
{ value: ".xls,.xlsx", label: "Excel 파일" },
|
||
|
|
{ value: ".ppt,.pptx", label: "PowerPoint 파일" },
|
||
|
|
{ value: ".txt", label: "텍스트 파일" },
|
||
|
|
{ value: ".zip,.rar", label: "압축 파일" },
|
||
|
|
{ value: ".dwg,.dxf", label: "CAD 파일" },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 파일 타입 추가
|
||
|
|
const addAcceptType = useCallback(() => {
|
||
|
|
const newType = localInputs.newAcceptType.trim();
|
||
|
|
if (newType && !acceptTypes.includes(newType)) {
|
||
|
|
const newAcceptTypes = [...acceptTypes, newType];
|
||
|
|
setAcceptTypes(newAcceptTypes);
|
||
|
|
onUpdateProperty(component.id, "fileConfig.accept", newAcceptTypes);
|
||
|
|
setLocalInputs((prev) => ({ ...prev, newAcceptType: "" }));
|
||
|
|
}
|
||
|
|
}, [localInputs.newAcceptType, acceptTypes, component.id, onUpdateProperty]);
|
||
|
|
|
||
|
|
// 파일 타입 제거
|
||
|
|
const removeAcceptType = useCallback(
|
||
|
|
(typeToRemove: string) => {
|
||
|
|
const newAcceptTypes = acceptTypes.filter((type) => type !== typeToRemove);
|
||
|
|
setAcceptTypes(newAcceptTypes);
|
||
|
|
onUpdateProperty(component.id, "fileConfig.accept", newAcceptTypes);
|
||
|
|
},
|
||
|
|
[acceptTypes, component.id, onUpdateProperty],
|
||
|
|
);
|
||
|
|
|
||
|
|
// 미리 정의된 파일 타입 추가
|
||
|
|
const addCommonFileType = useCallback(
|
||
|
|
(fileType: string) => {
|
||
|
|
const types = fileType.split(",");
|
||
|
|
const newAcceptTypes = [...acceptTypes];
|
||
|
|
|
||
|
|
types.forEach((type) => {
|
||
|
|
if (!newAcceptTypes.includes(type.trim())) {
|
||
|
|
newAcceptTypes.push(type.trim());
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
setAcceptTypes(newAcceptTypes);
|
||
|
|
onUpdateProperty(component.id, "fileConfig.accept", newAcceptTypes);
|
||
|
|
},
|
||
|
|
[acceptTypes, component.id, onUpdateProperty],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* 문서 분류 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium text-gray-900">문서 분류 설정</h4>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="docType">문서 타입</Label>
|
||
|
|
<Select
|
||
|
|
value={localInputs.docType}
|
||
|
|
onValueChange={(value) => {
|
||
|
|
setLocalInputs((prev) => ({ ...prev, docType: value }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.docType", value);
|
||
|
|
|
||
|
|
// 문서 타입 변경 시 자동으로 타입명도 업데이트
|
||
|
|
const selectedOption = docTypeOptions.find((opt) => opt.value === value);
|
||
|
|
if (selectedOption) {
|
||
|
|
setLocalInputs((prev) => ({ ...prev, docTypeName: selectedOption.label }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.docTypeName", selectedOption.label);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue placeholder="문서 타입을 선택하세요" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{docTypeOptions.map((option) => (
|
||
|
|
<SelectItem key={option.value} value={option.value}>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="docTypeName">문서 타입 표시명</Label>
|
||
|
|
<Input
|
||
|
|
id="docTypeName"
|
||
|
|
value={localInputs.docTypeName}
|
||
|
|
onChange={(e) => {
|
||
|
|
const newValue = e.target.value;
|
||
|
|
setLocalInputs((prev) => ({ ...prev, docTypeName: newValue }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.docTypeName", newValue);
|
||
|
|
}}
|
||
|
|
placeholder="문서 타입 표시명"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 파일 업로드 제한 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium text-gray-900">파일 업로드 제한</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="maxSize">최대 파일 크기 (MB)</Label>
|
||
|
|
<Input
|
||
|
|
id="maxSize"
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="100"
|
||
|
|
value={localInputs.maxSize}
|
||
|
|
onChange={(e) => {
|
||
|
|
const newValue = parseInt(e.target.value) || 10;
|
||
|
|
setLocalInputs((prev) => ({ ...prev, maxSize: newValue }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.maxSize", newValue);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="maxFiles">최대 파일 개수</Label>
|
||
|
|
<Input
|
||
|
|
id="maxFiles"
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="20"
|
||
|
|
value={localInputs.maxFiles}
|
||
|
|
onChange={(e) => {
|
||
|
|
const newValue = parseInt(e.target.value) || 5;
|
||
|
|
setLocalInputs((prev) => ({ ...prev, maxFiles: newValue }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.maxFiles", newValue);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="multiple"
|
||
|
|
checked={localValues.multiple}
|
||
|
|
onCheckedChange={(checked) => {
|
||
|
|
setLocalValues((prev) => ({ ...prev, multiple: checked as boolean }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.multiple", checked);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="multiple">다중 파일 선택 허용</Label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 허용 파일 타입 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium text-gray-900">허용 파일 타입</h4>
|
||
|
|
|
||
|
|
{/* 미리 정의된 파일 타입 버튼들 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>빠른 추가</Label>
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{commonFileTypes.map((fileType) => (
|
||
|
|
<Button
|
||
|
|
key={fileType.value}
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => addCommonFileType(fileType.value)}
|
||
|
|
className="text-xs"
|
||
|
|
>
|
||
|
|
<Plus className="mr-1 h-3 w-3" />
|
||
|
|
{fileType.label}
|
||
|
|
</Button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 현재 설정된 파일 타입들 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>현재 허용 타입</Label>
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{acceptTypes.map((type, index) => (
|
||
|
|
<Badge key={index} variant="secondary" className="flex items-center space-x-1">
|
||
|
|
<span>{type}</span>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => removeAcceptType(type)}
|
||
|
|
className="h-4 w-4 p-0 hover:bg-transparent"
|
||
|
|
>
|
||
|
|
<X className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
</Badge>
|
||
|
|
))}
|
||
|
|
{acceptTypes.length === 0 && <span className="text-sm text-gray-500">모든 파일 타입 허용</span>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 사용자 정의 파일 타입 추가 */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="newAcceptType">사용자 정의 타입 추가</Label>
|
||
|
|
<div className="flex space-x-2">
|
||
|
|
<Input
|
||
|
|
id="newAcceptType"
|
||
|
|
value={localInputs.newAcceptType}
|
||
|
|
onChange={(e) => {
|
||
|
|
setLocalInputs((prev) => ({ ...prev, newAcceptType: e.target.value }));
|
||
|
|
}}
|
||
|
|
placeholder="예: .dwg, image/*, .custom"
|
||
|
|
onKeyPress={(e) => {
|
||
|
|
if (e.key === "Enter") {
|
||
|
|
addAcceptType();
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Button onClick={addAcceptType} disabled={!localInputs.newAcceptType.trim()} size="sm">
|
||
|
|
추가
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* UI 설정 */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
<h4 className="text-sm font-medium text-gray-900">UI 설정</h4>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="dragDropText">드래그앤드롭 안내 텍스트</Label>
|
||
|
|
<Textarea
|
||
|
|
id="dragDropText"
|
||
|
|
value={localInputs.dragDropText}
|
||
|
|
onChange={(e) => {
|
||
|
|
const newValue = e.target.value;
|
||
|
|
setLocalInputs((prev) => ({ ...prev, dragDropText: newValue }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.dragDropText", newValue);
|
||
|
|
}}
|
||
|
|
placeholder="파일을 드래그하거나 클릭하여 업로드하세요"
|
||
|
|
rows={2}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showPreview"
|
||
|
|
checked={localValues.showPreview}
|
||
|
|
onCheckedChange={(checked) => {
|
||
|
|
setLocalValues((prev) => ({ ...prev, showPreview: checked as boolean }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.showPreview", checked);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showPreview">파일 미리보기 표시</Label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Checkbox
|
||
|
|
id="showProgress"
|
||
|
|
checked={localValues.showProgress}
|
||
|
|
onCheckedChange={(checked) => {
|
||
|
|
setLocalValues((prev) => ({ ...prev, showProgress: checked as boolean }));
|
||
|
|
onUpdateProperty(component.id, "fileConfig.showProgress", checked);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Label htmlFor="showProgress">업로드 진행률 표시</Label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|