442 lines
18 KiB
TypeScript
442 lines
18 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Database, Search, Info } from "lucide-react";
|
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
|
import { WidgetComponent, EntityTypeConfig } from "@/types/screen";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
|
|
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|
component,
|
|
onUpdateComponent,
|
|
onUpdateProperty,
|
|
}) => {
|
|
const widget = component as WidgetComponent;
|
|
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
|
|
|
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
|
const [referenceInfo, setReferenceInfo] = useState<{
|
|
referenceTable: string;
|
|
referenceColumn: string;
|
|
displayColumn: string;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}>({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: true,
|
|
error: null,
|
|
});
|
|
|
|
// 로컬 상태 (UI 관련 설정만)
|
|
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
|
entityType: config.entityType || "",
|
|
displayFields: config.displayFields || [],
|
|
searchFields: config.searchFields || [],
|
|
valueField: config.valueField || "",
|
|
labelField: config.labelField || "",
|
|
multiple: config.multiple || false,
|
|
searchable: config.searchable !== false,
|
|
placeholder: config.placeholder || "항목을 선택하세요",
|
|
emptyMessage: config.emptyMessage || "검색 결과가 없습니다",
|
|
pageSize: config.pageSize || 20,
|
|
minSearchLength: config.minSearchLength || 1,
|
|
defaultValue: config.defaultValue || "",
|
|
required: config.required || false,
|
|
readonly: config.readonly || false,
|
|
apiEndpoint: config.apiEndpoint || "",
|
|
filters: config.filters || {},
|
|
});
|
|
|
|
// 테이블 타입 관리에서 설정된 참조 테이블 정보 로드
|
|
useEffect(() => {
|
|
const loadReferenceInfo = async () => {
|
|
// 컴포넌트의 테이블명과 컬럼명이 있는 경우에만 조회
|
|
const tableName = widget.tableName;
|
|
const columnName = widget.columnName;
|
|
|
|
if (!tableName || !columnName) {
|
|
setReferenceInfo({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
error: "테이블 또는 컬럼 정보가 없습니다.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 테이블 타입 관리에서 컬럼 정보 조회
|
|
const columns = await tableTypeApi.getColumns(tableName);
|
|
const columnInfo = columns.find((col: any) =>
|
|
(col.columnName || col.column_name) === columnName
|
|
);
|
|
|
|
if (columnInfo) {
|
|
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
|
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
|
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
|
|
|
// detailSettings에서도 정보 확인 (JSON 파싱)
|
|
let detailSettings: any = {};
|
|
if (columnInfo.detailSettings) {
|
|
try {
|
|
if (typeof columnInfo.detailSettings === 'string') {
|
|
detailSettings = JSON.parse(columnInfo.detailSettings);
|
|
} else {
|
|
detailSettings = columnInfo.detailSettings;
|
|
}
|
|
} catch {
|
|
// JSON 파싱 실패 시 무시
|
|
}
|
|
}
|
|
|
|
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
|
const finalRefColumn = refColumn || detailSettings.referenceColumn || "";
|
|
const finalDispColumn = dispColumn || detailSettings.displayColumn || "";
|
|
|
|
setReferenceInfo({
|
|
referenceTable: finalRefTable,
|
|
referenceColumn: finalRefColumn,
|
|
displayColumn: finalDispColumn,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
// webTypeConfig에 참조 테이블 정보 자동 설정
|
|
if (finalRefTable) {
|
|
const newConfig = {
|
|
...localConfig,
|
|
valueField: finalRefColumn || "id",
|
|
labelField: finalDispColumn || "name",
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|
}
|
|
} else {
|
|
setReferenceInfo({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
error: "컬럼 정보를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("참조 테이블 정보 로드 실패:", error);
|
|
setReferenceInfo({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
error: "참조 테이블 정보 로드 실패",
|
|
});
|
|
}
|
|
};
|
|
|
|
loadReferenceInfo();
|
|
}, [widget.tableName, widget.columnName]);
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
useEffect(() => {
|
|
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
|
setLocalConfig({
|
|
entityType: currentConfig.entityType || "",
|
|
displayFields: currentConfig.displayFields || [],
|
|
searchFields: currentConfig.searchFields || [],
|
|
valueField: currentConfig.valueField || referenceInfo.referenceColumn || "",
|
|
labelField: currentConfig.labelField || referenceInfo.displayColumn || "",
|
|
multiple: currentConfig.multiple || false,
|
|
searchable: currentConfig.searchable !== false,
|
|
placeholder: currentConfig.placeholder || "항목을 선택하세요",
|
|
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
|
pageSize: currentConfig.pageSize || 20,
|
|
minSearchLength: currentConfig.minSearchLength || 1,
|
|
defaultValue: currentConfig.defaultValue || "",
|
|
required: currentConfig.required || false,
|
|
readonly: currentConfig.readonly || false,
|
|
apiEndpoint: currentConfig.apiEndpoint || "",
|
|
filters: currentConfig.filters || {},
|
|
});
|
|
}, [widget.webTypeConfig, referenceInfo.referenceColumn, referenceInfo.displayColumn]);
|
|
|
|
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
|
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
|
const newConfig = { ...localConfig, [field]: value };
|
|
setLocalConfig(newConfig);
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|
};
|
|
|
|
// 입력 필드용 업데이트 (로컬 상태만)
|
|
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
|
setLocalConfig({ ...localConfig, [field]: value });
|
|
};
|
|
|
|
// 입력 완료 시 부모에게 전달
|
|
const handleInputBlur = () => {
|
|
onUpdateProperty("webTypeConfig", localConfig);
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-xs">
|
|
<Database 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 flex items-center gap-2">
|
|
참조 테이블 정보
|
|
<span className="bg-muted text-muted-foreground px-1.5 py-0.5 rounded text-[10px]">
|
|
테이블 타입 관리에서 설정
|
|
</span>
|
|
</h4>
|
|
|
|
{referenceInfo.isLoading ? (
|
|
<div className="bg-muted/50 rounded-md border p-3">
|
|
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
|
</div>
|
|
) : referenceInfo.error ? (
|
|
<div className="bg-destructive/10 rounded-md border border-destructive/20 p-3">
|
|
<p className="text-xs text-destructive flex items-center gap-1">
|
|
<Info className="h-3 w-3" />
|
|
{referenceInfo.error}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
|
</p>
|
|
</div>
|
|
) : !referenceInfo.referenceTable ? (
|
|
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
|
<p className="text-xs text-amber-700 flex items-center gap-1">
|
|
<Info className="h-3 w-3" />
|
|
참조 테이블이 설정되지 않았습니다.
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="bg-muted/50 rounded-md border p-3 space-y-2">
|
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
<div>
|
|
<span className="text-muted-foreground">참조 테이블:</span>
|
|
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">참조 컬럼:</span>
|
|
<div className="font-medium">{referenceInfo.referenceColumn || "-"}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">표시 컬럼:</span>
|
|
<div className="font-medium">{referenceInfo.displayColumn || "-"}</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
이 정보는 테이블 타입 관리에서 변경할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* UI 모드 설정 */}
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium">UI 설정</h4>
|
|
|
|
{/* UI 모드 선택 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="uiMode" className="text-xs">
|
|
UI 모드
|
|
</Label>
|
|
<Select
|
|
value={(localConfig as any).uiMode || "combo"}
|
|
onValueChange={(value) => updateConfig("uiMode" as any, value)}
|
|
>
|
|
<SelectTrigger className="text-xs">
|
|
<SelectValue placeholder="모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="select">드롭다운 (Select)</SelectItem>
|
|
<SelectItem value="modal">모달 팝업 (Modal)</SelectItem>
|
|
<SelectItem value="combo">입력 + 모달 버튼 (Combo)</SelectItem>
|
|
<SelectItem value="autocomplete">자동완성 (Autocomplete)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{(localConfig as any).uiMode === "select" && "검색 가능한 드롭다운 형태로 표시됩니다."}
|
|
{(localConfig as any).uiMode === "modal" && "모달 팝업에서 데이터를 검색하고 선택합니다."}
|
|
{((localConfig as any).uiMode === "combo" || !(localConfig as any).uiMode) && "입력 필드와 검색 버튼이 함께 표시됩니다."}
|
|
{(localConfig as any).uiMode === "autocomplete" && "입력하면서 자동완성 목록이 표시됩니다."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="placeholder" className="text-xs">
|
|
플레이스홀더
|
|
</Label>
|
|
<Input
|
|
id="placeholder"
|
|
value={localConfig.placeholder || ""}
|
|
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
|
onBlur={handleInputBlur}
|
|
placeholder="항목을 선택하세요"
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="emptyMessage" className="text-xs">
|
|
빈 결과 메시지
|
|
</Label>
|
|
<Input
|
|
id="emptyMessage"
|
|
value={localConfig.emptyMessage || ""}
|
|
onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
|
|
onBlur={handleInputBlur}
|
|
placeholder="검색 결과가 없습니다"
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 검색 설정 */}
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium">검색 설정</h4>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="minSearchLength" className="text-xs">
|
|
최소 검색 길이
|
|
</Label>
|
|
<Input
|
|
id="minSearchLength"
|
|
type="number"
|
|
value={localConfig.minSearchLength || 1}
|
|
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
|
|
min={0}
|
|
max={10}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="pageSize" className="text-xs">
|
|
페이지 크기
|
|
</Label>
|
|
<Input
|
|
id="pageSize"
|
|
type="number"
|
|
value={localConfig.pageSize || 20}
|
|
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
|
|
min={5}
|
|
max={100}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="searchable" className="text-xs">
|
|
검색 가능
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">항목을 검색할 수 있습니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="searchable"
|
|
checked={localConfig.searchable !== false}
|
|
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
|
/>
|
|
</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>
|
|
|
|
{/* 상태 설정 */}
|
|
<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">
|
|
<div className="flex items-center gap-2 rounded border bg-white p-2">
|
|
<Database className="h-4 w-4 text-gray-400" />
|
|
<span className="flex-1 text-xs text-muted-foreground">
|
|
{localConfig.placeholder || "항목을 선택하세요"}
|
|
</span>
|
|
{localConfig.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
|
</div>
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
<div>테이블: {referenceInfo.referenceTable || "미설정"}</div>
|
|
<div>값 필드: {localConfig.valueField || referenceInfo.referenceColumn || "-"}</div>
|
|
<div>표시 필드: {localConfig.labelField || referenceInfo.displayColumn || "-"}</div>
|
|
{localConfig.multiple && <span> / 다중선택</span>}
|
|
{localConfig.required && <span> / 필수</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
EntityConfigPanel.displayName = "EntityConfigPanel";
|