ERP-node/frontend/components/screen/panels/FileComponentConfigPanel.tsx

460 lines
18 KiB
TypeScript
Raw Normal View History

2025-09-05 21:52:19 +09:00
"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, TableInfo } from "@/types/screen";
2025-09-05 21:52:19 +09:00
import { Plus, X } from "lucide-react";
import { Button } from "@/components/ui/button";
interface FileComponentConfigPanelProps {
component: FileComponent;
onUpdateProperty: (componentId: string, path: string, value: any) => void;
currentTable?: TableInfo; // 현재 화면의 테이블 정보
currentTableName?: string; // 현재 화면의 테이블명
2025-09-05 21:52:19 +09:00
}
export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> = ({
component,
onUpdateProperty,
currentTable,
currentTableName,
}) => {
2025-09-05 21:52:19 +09:00
// 로컬 상태
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: "", // 새 파일 타입 추가용
linkedTable: component.fileConfig.linkedTable || "", // 연결 테이블
linkedField: component.fileConfig.linkedField || "", // 연결 필드
2025-09-05 21:52:19 +09:00
});
const [localValues, setLocalValues] = useState({
multiple: component.fileConfig.multiple ?? true,
showPreview: component.fileConfig.showPreview ?? true,
showProgress: component.fileConfig.showProgress ?? true,
autoLink: component.fileConfig.autoLink ?? false, // 자동 연결
2025-09-05 21:52:19 +09:00
});
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: "",
linkedTable: component.fileConfig.linkedTable || "",
linkedField: component.fileConfig.linkedField || "",
2025-09-05 21:52:19 +09:00
});
setLocalValues({
multiple: component.fileConfig.multiple ?? true,
showPreview: component.fileConfig.showPreview ?? true,
showProgress: component.fileConfig.showProgress ?? true,
autoLink: component.fileConfig.autoLink ?? false,
2025-09-05 21:52:19 +09:00
});
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 className="mt-6 rounded-lg border bg-blue-50 p-4">
<h4 className="mb-3 text-sm font-semibold text-blue-900">📎 </h4>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="autoLink"
checked={localValues.autoLink}
onCheckedChange={(checked) => {
setLocalValues((prev) => ({ ...prev, autoLink: checked as boolean }));
onUpdateProperty(component.id, "fileConfig.autoLink", checked);
// 자동 연결이 활성화되면 현재 화면의 테이블 정보를 자동 설정
if (checked && currentTableName && currentTable) {
// 기본키 추정 로직 (일반적인 패턴들)
const primaryKeyGuesses = [
`${currentTableName}_id`, // table_name + "_id"
`${currentTableName.replace(/_/g, "")}_id`, // undercore 제거 + "_id"
currentTableName.endsWith("_info") || currentTableName.endsWith("_mng")
? currentTableName.replace(/_(info|mng)$/, "_code") // _info, _mng -> _code
: `${currentTableName}_code`, // table_name + "_code"
"id", // 단순 "id"
"objid", // "objid"
];
// 실제 테이블 컬럼에서 기본키로 추정되는 컬럼 찾기
let detectedPrimaryKey = "";
for (const guess of primaryKeyGuesses) {
const foundColumn = currentTable.columns.find(
(col) => col.columnName.toLowerCase() === guess.toLowerCase(),
);
if (foundColumn) {
detectedPrimaryKey = foundColumn.columnName;
break;
}
}
// 찾지 못한 경우 첫 번째 컬럼을 기본키로 사용
if (!detectedPrimaryKey && currentTable.columns.length > 0) {
detectedPrimaryKey = currentTable.columns[0].columnName;
}
console.log("🔗 자동 테이블 연결 설정:", {
tableName: currentTableName,
detectedPrimaryKey,
availableColumns: currentTable.columns.map((c) => c.columnName),
});
// 자동으로 테이블명과 기본키 설정
setLocalInputs((prev) => ({
...prev,
linkedTable: currentTableName,
linkedField: detectedPrimaryKey,
}));
onUpdateProperty(component.id, "fileConfig.linkedTable", currentTableName);
onUpdateProperty(component.id, "fileConfig.linkedField", detectedPrimaryKey);
}
}}
/>
<Label htmlFor="autoLink"> </Label>
</div>
{localValues.autoLink && (
<>
<div className="space-y-2">
<Label htmlFor="linkedTable"> </Label>
<Input
id="linkedTable"
value={localInputs.linkedTable}
readOnly
className="bg-gray-50 text-gray-700"
placeholder="자동으로 설정됩니다"
/>
<div className="text-xs text-green-600"> </div>
</div>
<div className="space-y-2">
<Label htmlFor="linkedField"> ()</Label>
<Input
id="linkedField"
value={localInputs.linkedField}
readOnly
className="bg-gray-50 text-gray-700"
placeholder="자동으로 감지됩니다"
/>
<div className="text-xs text-green-600"> </div>
</div>
<div className="rounded bg-blue-100 p-2 text-xs text-blue-600">
💡 .
<br />
{currentTableName && localInputs.linkedField ? (
<>
: {currentTableName} {localInputs.linkedField} "값123"
<br />
target_objid가 "{currentTableName}:값123" .
</>
) : (
<> .</>
)}
</div>
</>
)}
</div>
</div>
2025-09-05 21:52:19 +09:00
</div>
</div>
);
};