ERP-node/frontend/components/v2/config-panels/V2InputConfigPanel.tsx

686 lines
26 KiB
TypeScript

"use client";
/**
* V2Input 설정 패널
* 통합 입력 컴포넌트의 세부 설정을 관리합니다.
*/
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
interface V2InputConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
menuObjid?: number;
allComponents?: any[];
}
export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config, onChange, menuObjid, allComponents = [] }) => {
// 채번 규칙 목록 상태
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingRules, setLoadingRules] = useState(false);
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
const [parentMenus, setParentMenus] = useState<any[]>([]);
const [loadingMenus, setLoadingMenus] = useState(false);
// 선택된 메뉴 OBJID
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
return config.autoGeneration?.selectedMenuObjid || menuObjid;
});
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value });
};
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
useEffect(() => {
const loadMenus = async () => {
setLoadingMenus(true);
try {
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get("/admin/menus");
if (response.data.success && response.data.data) {
const allMenus = response.data.data;
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
const level2UserMenus = allMenus.filter((menu: any) =>
menu.menu_type === '1' && menu.lev === 2
);
setParentMenus(level2UserMenus);
}
} catch (error) {
console.error("부모 메뉴 로드 실패:", error);
} finally {
setLoadingMenus(false);
}
};
loadMenus();
}, []);
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
useEffect(() => {
const loadRules = async () => {
if (config.autoGeneration?.type !== "numbering_rule") {
return;
}
if (!selectedMenuObjid) {
setNumberingRules([]);
return;
}
setLoadingRules(true);
try {
const response = await getAvailableNumberingRules(selectedMenuObjid);
if (response.success && response.data) {
setNumberingRules(response.data);
}
} catch (error) {
console.error("채번 규칙 목록 로드 실패:", error);
setNumberingRules([]);
} finally {
setLoadingRules(false);
}
};
loadRules();
}, [selectedMenuObjid, config.autoGeneration?.type]);
return (
<div className="space-y-4">
{/* 입력 타입 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select
value={config.inputType || config.type || "text"}
onValueChange={(value) => updateConfig("inputType", value)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="입력 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="password"></SelectItem>
<SelectItem value="textarea"> </SelectItem>
<SelectItem value="slider"></SelectItem>
<SelectItem value="color"> </SelectItem>
<SelectItem value="numbering"> ()</SelectItem>
</SelectContent>
</Select>
</div>
{/* 채번 타입 전용 설정 */}
{config.inputType === "numbering" && (
<div className="space-y-3">
<Separator />
<div className="rounded-md border border-primary/20 bg-primary/10 p-3">
<p className="text-xs font-medium text-primary"> </p>
<p className="mt-1 text-[10px] text-primary">
<strong> </strong> .
<br />
.
</p>
</div>
{/* 채번 필드는 기본적으로 읽기전용 */}
<div className="flex items-center space-x-2">
<Checkbox
id="numberingReadonly"
checked={config.readonly !== false}
onCheckedChange={(checked) => {
updateConfig("readonly", checked);
}}
/>
<Label htmlFor="numberingReadonly" className="text-xs font-medium cursor-pointer">
()
</Label>
</div>
<p className="text-muted-foreground text-[10px] pl-6">
</p>
</div>
)}
{/* 채번 타입이 아닌 경우에만 추가 설정 표시 */}
{config.inputType !== "numbering" && (
<>
<Separator />
{/* 형식 (텍스트/숫자용) */}
{(config.inputType === "text" || !config.inputType) && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="형식 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="email"></SelectItem>
<SelectItem value="tel"></SelectItem>
<SelectItem value="url">URL</SelectItem>
<SelectItem value="currency"></SelectItem>
<SelectItem value="biz_no"></SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* 플레이스홀더 */}
<div className="space-y-2">
<Label className="text-xs font-medium"></Label>
<Input
value={config.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
className="h-8 text-xs"
/>
</div>
{/* 숫자/슬라이더 전용 설정 */}
{(config.inputType === "number" || config.inputType === "slider") && (
<>
<Separator />
<div className="grid grid-cols-3 gap-2">
<div className="space-y-2">
<Label className="text-xs font-medium"></Label>
<Input
type="number"
value={config.min ?? ""}
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
placeholder="0"
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium"></Label>
<Input
type="number"
value={config.max ?? ""}
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
placeholder="100"
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium"></Label>
<Input
type="number"
value={config.step ?? ""}
onChange={(e) => updateConfig("step", e.target.value ? Number(e.target.value) : undefined)}
placeholder="1"
className="h-8 text-xs"
/>
</div>
</div>
</>
)}
{/* 여러 줄 텍스트 전용 설정 */}
{config.inputType === "textarea" && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Input
type="number"
value={config.rows || 3}
onChange={(e) => updateConfig("rows", parseInt(e.target.value) || 3)}
min={2}
max={20}
className="h-8 text-xs"
/>
</div>
)}
{/* 마스크 입력 (선택) */}
<div className="space-y-2">
<Label className="text-xs font-medium"> ()</Label>
<Input
value={config.mask || ""}
onChange={(e) => updateConfig("mask", e.target.value)}
placeholder="예: ###-####-####"
className="h-8 text-xs"
/>
<p className="text-muted-foreground text-[10px]"># = , A = , * = </p>
</div>
<Separator />
{/* 자동생성 기능 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="autoGenerationEnabled"
checked={config.autoGeneration?.enabled || false}
onCheckedChange={(checked) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
updateConfig("autoGeneration", {
...currentConfig,
enabled: checked as boolean,
});
}}
/>
<Label htmlFor="autoGenerationEnabled" className="text-xs font-medium cursor-pointer">
</Label>
</div>
{/* 자동생성 타입 선택 */}
{config.autoGeneration?.enabled && (
<div className="space-y-3 pl-6">
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select
value={config.autoGeneration?.type || "none"}
onValueChange={(value: AutoGenerationType) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
updateConfig("autoGeneration", {
...currentConfig,
type: value,
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="자동생성 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="uuid">UUID </SelectItem>
<SelectItem value="current_user"> ID</SelectItem>
<SelectItem value="current_time"> </SelectItem>
<SelectItem value="sequence"> </SelectItem>
<SelectItem value="numbering_rule"> </SelectItem>
<SelectItem value="random_string"> </SelectItem>
<SelectItem value="random_number"> </SelectItem>
<SelectItem value="company_code"> </SelectItem>
<SelectItem value="department"> </SelectItem>
</SelectContent>
</Select>
{/* 선택된 타입 설명 */}
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
<p className="text-muted-foreground text-[10px]">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
</p>
)}
</div>
{/* 채번 규칙 선택 */}
{config.autoGeneration?.type === "numbering_rule" && (
<>
{/* 부모 메뉴 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium">
<span className="text-destructive">*</span>
</Label>
<Select
value={selectedMenuObjid?.toString() || ""}
onValueChange={(value) => {
const menuId = parseInt(value);
setSelectedMenuObjid(menuId);
updateConfig("autoGeneration", {
...config.autoGeneration,
selectedMenuObjid: menuId,
});
}}
disabled={loadingMenus}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
</SelectTrigger>
<SelectContent>
{parentMenus.length === 0 ? (
<SelectItem value="no-menus" disabled>
</SelectItem>
) : (
parentMenus.map((menu) => (
<SelectItem key={menu.objid} value={menu.objid.toString()}>
{menu.menu_name_kor}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* 채번 규칙 선택 */}
{selectedMenuObjid ? (
<div className="space-y-2">
<Label className="text-xs font-medium">
<span className="text-destructive">*</span>
</Label>
<Select
value={config.autoGeneration?.options?.numberingRuleId || ""}
onValueChange={(value) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
numberingRuleId: value,
},
});
}}
disabled={loadingRules}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
</SelectTrigger>
<SelectContent>
{numberingRules.length === 0 ? (
<SelectItem value="no-rules" disabled>
</SelectItem>
) : (
numberingRules.map((rule) => (
<SelectItem key={rule.ruleId} value={rule.ruleId}>
{rule.ruleName}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : (
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800">
</div>
)}
</>
)}
{/* 자동생성 옵션 (랜덤/순차용) */}
{config.autoGeneration?.type &&
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
<div className="space-y-2">
{/* 길이 설정 */}
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
type="number"
min="1"
max="50"
value={config.autoGeneration?.options?.length || 8}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
length: parseInt(e.target.value) || 8,
},
});
}}
className="h-8 text-xs"
/>
</div>
)}
{/* 접두사 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
prefix: e.target.value,
},
});
}}
placeholder="예: INV-"
className="h-8 text-xs"
/>
</div>
{/* 접미사 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<Input
value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => {
updateConfig("autoGeneration", {
...config.autoGeneration,
options: {
...config.autoGeneration?.options,
suffix: e.target.value,
},
});
}}
className="h-8 text-xs"
/>
</div>
{/* 미리보기 */}
<div className="space-y-1">
<Label className="text-xs font-medium"></Label>
<div className="rounded border bg-muted p-2 text-xs font-mono">
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
)}
{/* 데이터 바인딩 설정 */}
<Separator className="my-2" />
<DataBindingSection config={config} onChange={onChange} allComponents={allComponents} />
</div>
);
};
V2InputConfigPanel.displayName = "V2InputConfigPanel";
/**
* 데이터 바인딩 설정 섹션
* 같은 화면의 v2-table-list 컴포넌트를 자동 감지하여 드롭다운으로 표시
*/
function DataBindingSection({
config,
onChange,
allComponents,
}: {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
allComponents: any[];
}) {
const [tableColumns, setTableColumns] = useState<string[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
// 같은 화면의 v2-table-list 컴포넌트만 필터링
const tableListComponents = React.useMemo(() => {
return allComponents.filter((comp) => {
const type =
comp.componentType ||
comp.widgetType ||
comp.componentConfig?.type ||
(comp.url && comp.url.split("/").pop());
return type === "v2-table-list";
});
}, [allComponents]);
// 선택된 테이블 컴포넌트의 테이블명 추출
const selectedTableComponent = React.useMemo(() => {
if (!config.dataBinding?.sourceComponentId) return null;
return tableListComponents.find((comp) => comp.id === config.dataBinding.sourceComponentId);
}, [tableListComponents, config.dataBinding?.sourceComponentId]);
const selectedTableName = React.useMemo(() => {
if (!selectedTableComponent) return null;
return (
selectedTableComponent.componentConfig?.selectedTable ||
selectedTableComponent.selectedTable ||
null
);
}, [selectedTableComponent]);
// 선택된 테이블의 컬럼 목록 로드
useEffect(() => {
if (!selectedTableName) {
setTableColumns([]);
return;
}
const loadColumns = async () => {
setLoadingColumns(true);
try {
const { tableTypeApi } = await import("@/lib/api/screen");
const response = await tableTypeApi.getTableTypeColumns(selectedTableName);
if (response.success && response.data) {
const cols = response.data.map((col: any) => col.column_name).filter(Boolean);
setTableColumns(cols);
}
} catch {
// 컬럼 정보를 못 가져오면 테이블 컴포넌트의 columns에서 추출
const configColumns = selectedTableComponent?.componentConfig?.columns;
if (Array.isArray(configColumns)) {
setTableColumns(configColumns.map((c: any) => c.columnName).filter(Boolean));
}
} finally {
setLoadingColumns(false);
}
};
loadColumns();
}, [selectedTableName, selectedTableComponent]);
const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value });
};
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Checkbox
id="dataBindingEnabled"
checked={!!config.dataBinding?.sourceComponentId}
onCheckedChange={(checked) => {
if (checked) {
const firstTable = tableListComponents[0];
updateConfig("dataBinding", {
sourceComponentId: firstTable?.id || "",
sourceColumn: "",
});
} else {
updateConfig("dataBinding", undefined);
}
}}
/>
<Label htmlFor="dataBindingEnabled" className="text-xs font-semibold">
</Label>
</div>
{config.dataBinding && (
<div className="space-y-2 rounded border p-2">
<p className="text-[10px] text-muted-foreground">
</p>
{/* 소스 테이블 컴포넌트 선택 */}
<div className="space-y-1">
<Label className="text-xs font-medium"> </Label>
{tableListComponents.length === 0 ? (
<p className="text-[10px] text-amber-500"> v2-table-list </p>
) : (
<Select
value={config.dataBinding?.sourceComponentId || ""}
onValueChange={(value) => {
updateConfig("dataBinding", {
...config.dataBinding,
sourceComponentId: value,
sourceColumn: "",
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tableListComponents.map((comp) => {
const tblName =
comp.componentConfig?.selectedTable || comp.selectedTable || "";
const label = comp.componentConfig?.label || comp.label || comp.id;
return (
<SelectItem key={comp.id} value={comp.id}>
{label} ({tblName || comp.id})
</SelectItem>
);
})}
</SelectContent>
</Select>
)}
</div>
{/* 소스 컬럼 선택 */}
{config.dataBinding?.sourceComponentId && (
<div className="space-y-1">
<Label className="text-xs font-medium"> </Label>
{loadingColumns ? (
<p className="text-[10px] text-muted-foreground"> ...</p>
) : tableColumns.length === 0 ? (
<>
<Input
value={config.dataBinding?.sourceColumn || ""}
onChange={(e) => {
updateConfig("dataBinding", {
...config.dataBinding,
sourceColumn: e.target.value,
});
}}
placeholder="컬럼명 직접 입력"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground"> </p>
</>
) : (
<Select
value={config.dataBinding?.sourceColumn || ""}
onValueChange={(value) => {
updateConfig("dataBinding", {
...config.dataBinding,
sourceColumn: value,
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col) => (
<SelectItem key={col} value={col}>
{col}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)}
</div>
)}
</div>
);
}
export default V2InputConfigPanel;