833 lines
36 KiB
TypeScript
833 lines
36 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* V2Input 설정 패널
|
|
* 토스식 단계별 UX: 기본 설정 -> 타입별 설정 -> 고급 설정(접힘)
|
|
*/
|
|
|
|
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 { Switch } from "@/components/ui/switch";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import {
|
|
Settings,
|
|
ChevronDown,
|
|
Loader2,
|
|
Type,
|
|
Hash,
|
|
Lock,
|
|
AlignLeft,
|
|
SlidersHorizontal,
|
|
Palette,
|
|
ListOrdered,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
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);
|
|
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
|
|
return config.autoGeneration?.selectedMenuObjid || menuObjid;
|
|
});
|
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
|
|
const updateConfig = (field: string, value: any) => {
|
|
onChange({ ...config, [field]: value });
|
|
};
|
|
|
|
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;
|
|
const userMenus = allMenus.filter((menu: any) => {
|
|
const menuType = menu.menu_type || menu.menuType;
|
|
const level = menu.level || menu.lev || menu.LEVEL;
|
|
return menuType === "1" && (level === 2 || level === 3 || level === "2" || level === "3");
|
|
});
|
|
setParentMenus(userMenus);
|
|
}
|
|
} catch (error) {
|
|
console.error("부모 메뉴 로드 실패:", error);
|
|
} finally {
|
|
setLoadingMenus(false);
|
|
}
|
|
};
|
|
loadMenus();
|
|
}, []);
|
|
|
|
const inputType = config.inputType || config.type || "text";
|
|
|
|
useEffect(() => {
|
|
const loadRules = async () => {
|
|
const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule";
|
|
if (!isNumbering) 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, inputType]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Type className="text-muted-foreground h-4 w-4" />
|
|
<p className="text-sm font-medium">입력 타입</p>
|
|
</div>
|
|
<p className="text-muted-foreground text-[11px]">입력 필드의 종류를 선택해요</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{[
|
|
{ value: "text", icon: Type, label: "텍스트", desc: "일반 텍스트 입력" },
|
|
{ value: "number", icon: Hash, label: "숫자", desc: "숫자만 입력" },
|
|
{ value: "password", icon: Lock, label: "비밀번호", desc: "마스킹 처리" },
|
|
{ value: "textarea", icon: AlignLeft, label: "여러 줄", desc: "긴 텍스트 입력" },
|
|
{ value: "slider", icon: SlidersHorizontal, label: "슬라이더", desc: "범위 선택" },
|
|
{ value: "color", icon: Palette, label: "색상", desc: "색상 선택기" },
|
|
{ value: "numbering", icon: ListOrdered, label: "채번", desc: "자동 번호 생성" },
|
|
].map((item) => (
|
|
<button
|
|
key={item.value}
|
|
type="button"
|
|
onClick={() => {
|
|
if (item.value === "numbering") {
|
|
const autoMenuObjid = selectedMenuObjid || menuObjid;
|
|
onChange({
|
|
...config,
|
|
inputType: "numbering",
|
|
autoGeneration: {
|
|
...config.autoGeneration,
|
|
type: "numbering_rule" as AutoGenerationType,
|
|
selectedMenuObjid: autoMenuObjid,
|
|
},
|
|
readonly: config.readonly ?? true,
|
|
});
|
|
if (autoMenuObjid) setSelectedMenuObjid(autoMenuObjid);
|
|
} else {
|
|
updateConfig("inputType", item.value);
|
|
}
|
|
}}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all",
|
|
inputType === item.value
|
|
? "border-primary bg-primary/5 ring-primary/20 ring-1"
|
|
: "border-border hover:border-primary/30 hover:bg-muted/30",
|
|
)}
|
|
>
|
|
<item.icon
|
|
className={cn("h-4 w-4 shrink-0", inputType === item.value ? "text-primary" : "text-muted-foreground")}
|
|
/>
|
|
<div className="min-w-0">
|
|
<span
|
|
className={cn(
|
|
"block text-xs font-medium",
|
|
inputType === item.value ? "text-primary" : "text-foreground",
|
|
)}
|
|
>
|
|
{item.label}
|
|
</span>
|
|
<span className="text-muted-foreground block truncate text-[10px]">{item.desc}</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* ─── 채번 타입 전용 설정 ─── */}
|
|
{inputType === "numbering" && (
|
|
<div className="bg-muted/30 space-y-3 rounded-lg border p-4">
|
|
<div className="flex items-center gap-2">
|
|
<ListOrdered className="text-primary h-4 w-4" />
|
|
<span className="text-sm font-medium">채번 규칙</span>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-muted-foreground mb-1.5 text-xs">적용할 메뉴</p>
|
|
{menuObjid && selectedMenuObjid === menuObjid ? (
|
|
<div className="bg-background rounded-md border p-2">
|
|
<p className="text-muted-foreground text-xs">현재 화면 메뉴 사용 중</p>
|
|
<div className="mt-1 flex items-center justify-between">
|
|
<p className="text-sm font-medium">
|
|
{parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor ||
|
|
parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name ||
|
|
`메뉴 #${menuObjid}`}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setSelectedMenuObjid(undefined)}
|
|
className="text-muted-foreground hover:text-foreground text-[10px]"
|
|
>
|
|
변경
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : loadingMenus ? (
|
|
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
메뉴 목록 로딩 중...
|
|
</div>
|
|
) : (
|
|
<Select
|
|
value={selectedMenuObjid ? String(selectedMenuObjid) : ""}
|
|
onValueChange={(v) => {
|
|
const objid = Number(v);
|
|
setSelectedMenuObjid(objid);
|
|
onChange({
|
|
...config,
|
|
autoGeneration: {
|
|
...config.autoGeneration,
|
|
type: "numbering_rule" as AutoGenerationType,
|
|
selectedMenuObjid: objid,
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-sm">
|
|
<SelectValue placeholder="메뉴를 선택해주세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{parentMenus.map((menu: any) => (
|
|
<SelectItem key={menu.objid} value={String(menu.objid)}>
|
|
{menu.menu_name_kor || menu.translated_name || menu.menu_name || `메뉴 ${menu.objid}`}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
|
|
{selectedMenuObjid && (
|
|
<div>
|
|
<p className="text-muted-foreground mb-1.5 text-xs">채번 규칙</p>
|
|
{loadingRules ? (
|
|
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
채번 규칙 로딩 중...
|
|
</div>
|
|
) : numberingRules.length > 0 ? (
|
|
<Select
|
|
value={config.autoGeneration?.numberingRuleId ? String(config.autoGeneration.numberingRuleId) : ""}
|
|
onValueChange={(v) => {
|
|
onChange({
|
|
...config,
|
|
autoGeneration: {
|
|
...config.autoGeneration,
|
|
type: "numbering_rule" as AutoGenerationType,
|
|
numberingRuleId: Number(v),
|
|
selectedMenuObjid,
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-sm">
|
|
<SelectValue placeholder="채번 규칙 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{numberingRules.map((rule) => (
|
|
<SelectItem key={rule.ruleId} value={String(rule.ruleId)}>
|
|
{rule.ruleName} ({rule.separator || "-"}
|
|
{"{번호}"})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<p className="text-muted-foreground text-xs">선택한 메뉴에 등록된 채번 규칙이 없어요</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">읽기전용</p>
|
|
<p className="text-muted-foreground text-[11px]">채번 필드는 자동 생성되므로 읽기전용을 권장해요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.readonly !== false}
|
|
onCheckedChange={(checked) => updateConfig("readonly", checked)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ─── 채번 타입이 아닌 경우: 기본 설정 ─── */}
|
|
{inputType !== "numbering" && (
|
|
<>
|
|
{/* 기본 설정 영역 */}
|
|
<div className="bg-muted/30 space-y-3 rounded-lg border p-4">
|
|
{/* 안내 텍스트 (placeholder) */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">안내 텍스트</span>
|
|
<Input
|
|
value={config.placeholder || ""}
|
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
|
placeholder="입력 안내"
|
|
className="h-7 w-[160px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 입력 형식 - 텍스트 타입 전용 */}
|
|
{(inputType === "text" || !config.inputType) && (
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">입력 형식</span>
|
|
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
|
|
<SelectTrigger className="h-7 w-[160px] 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="flex items-center justify-between py-1">
|
|
<div>
|
|
<span className="text-muted-foreground text-xs">입력 마스크</span>
|
|
<p className="text-muted-foreground mt-0.5 text-[10px]"># = 숫자, A = 문자, * = 모두</p>
|
|
</div>
|
|
<Input
|
|
value={config.mask || ""}
|
|
onChange={(e) => updateConfig("mask", e.target.value)}
|
|
placeholder="###-####-####"
|
|
className="h-7 w-[160px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 숫자/슬라이더: 범위 설정 */}
|
|
{(inputType === "number" || inputType === "slider") && (
|
|
<div className="space-y-2 pt-1">
|
|
<p className="text-muted-foreground text-xs">값 범위</p>
|
|
<div className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">최소값</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.min ?? ""}
|
|
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
|
|
placeholder="0"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">최대값</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.max ?? ""}
|
|
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
|
|
placeholder="100"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Label className="text-muted-foreground text-[10px]">단계</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.step ?? ""}
|
|
onChange={(e) => updateConfig("step", e.target.value ? Number(e.target.value) : undefined)}
|
|
placeholder="1"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 여러 줄 텍스트: 줄 수 */}
|
|
{inputType === "textarea" && (
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">줄 수</span>
|
|
<Input
|
|
type="number"
|
|
value={config.rows || 3}
|
|
onChange={(e) => updateConfig("rows", parseInt(e.target.value) || 3)}
|
|
min={2}
|
|
max={20}
|
|
className="h-7 w-[160px] text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ─── 고급 설정: 자동 생성 (Collapsible) ─── */}
|
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Settings className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-sm font-medium">고급 설정</span>
|
|
</div>
|
|
<ChevronDown
|
|
className={cn(
|
|
"text-muted-foreground h-4 w-4 transition-transform duration-200",
|
|
advancedOpen && "rotate-180",
|
|
)}
|
|
/>
|
|
</button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
|
{/* 자동 생성 토글 */}
|
|
<div className="flex items-center justify-between py-1">
|
|
<div>
|
|
<p className="text-sm">자동 생성</p>
|
|
<p className="text-muted-foreground text-[11px]">값이 자동으로 채워져요</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.autoGeneration?.enabled || false}
|
|
onCheckedChange={(checked) => {
|
|
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
|
|
updateConfig("autoGeneration", {
|
|
...currentConfig,
|
|
enabled: checked as boolean,
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{config.autoGeneration?.enabled && (
|
|
<div className="border-primary/20 ml-1 space-y-3 border-l-2 pl-3">
|
|
{/* 자동 생성 타입 */}
|
|
<div>
|
|
<p className="text-muted-foreground mb-1.5 text-xs">생성 방식</p>
|
|
<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-sm">
|
|
<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>
|
|
</div>
|
|
|
|
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
|
|
<p className="text-muted-foreground text-[11px]">
|
|
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
|
</p>
|
|
)}
|
|
|
|
{/* 채번 규칙 선택 */}
|
|
{config.autoGeneration?.type === "numbering_rule" && (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<p className="text-muted-foreground mb-1.5 text-xs">
|
|
대상 메뉴 <span className="text-destructive">*</span>
|
|
</p>
|
|
<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-sm">
|
|
<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>
|
|
<p className="text-muted-foreground mb-1.5 text-xs">
|
|
채번 규칙 <span className="text-destructive">*</span>
|
|
</p>
|
|
{loadingRules ? (
|
|
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
규칙 로딩 중...
|
|
</div>
|
|
) : (
|
|
<Select
|
|
value={config.autoGeneration?.options?.numberingRuleId || ""}
|
|
onValueChange={(value) => {
|
|
updateConfig("autoGeneration", {
|
|
...config.autoGeneration,
|
|
options: {
|
|
...config.autoGeneration?.options,
|
|
numberingRuleId: value,
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-sm">
|
|
<SelectValue placeholder="규칙 선택" />
|
|
</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.5 text-xs text-amber-800">
|
|
먼저 대상 메뉴를 선택하세요
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 랜덤/순차 옵션 */}
|
|
{config.autoGeneration?.type &&
|
|
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
|
|
<div className="space-y-3">
|
|
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">길이</span>
|
|
<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-7 w-[120px] text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">접두사</span>
|
|
<Input
|
|
value={config.autoGeneration?.options?.prefix || ""}
|
|
onChange={(e) => {
|
|
updateConfig("autoGeneration", {
|
|
...config.autoGeneration,
|
|
options: {
|
|
...config.autoGeneration?.options,
|
|
prefix: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
placeholder="예: INV-"
|
|
className="h-7 w-[120px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
<span className="text-muted-foreground text-xs">접미사</span>
|
|
<Input
|
|
value={config.autoGeneration?.options?.suffix || ""}
|
|
onChange={(e) => {
|
|
updateConfig("autoGeneration", {
|
|
...config.autoGeneration,
|
|
options: {
|
|
...config.autoGeneration?.options,
|
|
suffix: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
className="h-7 w-[120px] text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-muted-foreground text-xs">미리보기</span>
|
|
<div className="bg-muted mt-1 rounded-md border p-2 font-mono text-xs">
|
|
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</>
|
|
)}
|
|
|
|
{/* 데이터 바인딩 설정 */}
|
|
<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-muted-foreground text-[10px]">테이블에서 행 선택 시 해당 컬럼 값이 자동으로 채워집니다</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-muted-foreground text-[10px]">컬럼 로딩 중...</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-muted-foreground text-[10px]">컬럼 정보를 불러올 수 없어 직접 입력</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;
|