[agent-pipeline] pipe-20260311071246-rhvz round-3

This commit is contained in:
DDD1542 2026-03-11 16:27:18 +09:00
parent d4f2a3cf04
commit d1d5f651cc
1 changed files with 355 additions and 363 deletions

View File

@ -9,7 +9,6 @@ import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
@ -19,44 +18,33 @@ import { NumberingRuleConfig } from "@/types/numbering-rule";
interface V2InputConfigPanelProps { interface V2InputConfigPanelProps {
config: Record<string, any>; config: Record<string, any>;
onChange: (config: Record<string, any>) => void; onChange: (config: Record<string, any>) => void;
menuObjid?: number; // 메뉴 OBJID (채번 규칙 필터링용) menuObjid?: number;
} }
export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config, onChange, menuObjid }) => { export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config, onChange, menuObjid }) => {
// 채번 규칙 목록 상태
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]); const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingRules, setLoadingRules] = useState(false); const [loadingRules, setLoadingRules] = useState(false);
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
const [parentMenus, setParentMenus] = useState<any[]>([]); const [parentMenus, setParentMenus] = useState<any[]>([]);
const [loadingMenus, setLoadingMenus] = useState(false); const [loadingMenus, setLoadingMenus] = useState(false);
// 선택된 메뉴 OBJID
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => { const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
return config.autoGeneration?.selectedMenuObjid || menuObjid; return config.autoGeneration?.selectedMenuObjid || menuObjid;
}); });
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => { const updateConfig = (field: string, value: any) => {
onChange({ ...config, [field]: value }); onChange({ ...config, [field]: value });
}; };
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
useEffect(() => { useEffect(() => {
const loadMenus = async () => { const loadMenus = async () => {
setLoadingMenus(true); setLoadingMenus(true);
try { try {
const { apiClient } = await import("@/lib/api/client"); const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get("/admin/menus"); const response = await apiClient.get("/admin/menus");
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {
const allMenus = response.data.data; const allMenus = response.data.data;
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
const level2UserMenus = allMenus.filter((menu: any) => const level2UserMenus = allMenus.filter((menu: any) =>
menu.menu_type === '1' && menu.lev === 2 menu.menu_type === '1' && menu.lev === 2
); );
setParentMenus(level2UserMenus); setParentMenus(level2UserMenus);
} }
} catch (error) { } catch (error) {
@ -68,22 +56,13 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
loadMenus(); loadMenus();
}, []); }, []);
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
useEffect(() => { useEffect(() => {
const loadRules = async () => { const loadRules = async () => {
if (config.autoGeneration?.type !== "numbering_rule") { if (config.autoGeneration?.type !== "numbering_rule") return;
return; if (!selectedMenuObjid) { setNumberingRules([]); return; }
}
if (!selectedMenuObjid) {
setNumberingRules([]);
return;
}
setLoadingRules(true); setLoadingRules(true);
try { try {
const response = await getAvailableNumberingRules(selectedMenuObjid); const response = await getAvailableNumberingRules(selectedMenuObjid);
if (response.success && response.data) { if (response.success && response.data) {
setNumberingRules(response.data); setNumberingRules(response.data);
} }
@ -94,20 +73,22 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
setLoadingRules(false); setLoadingRules(false);
} }
}; };
loadRules(); loadRules();
}, [selectedMenuObjid, config.autoGeneration?.type]); }, [selectedMenuObjid, config.autoGeneration?.type]);
return ( return (
<div className="space-y-4"> <div className="space-y-1">
{/* 입력 타입 */} {/* INPUT TYPE 섹션 */}
<div className="space-y-2"> <div className="border-b border-border/50 pb-3 mb-3">
<Label className="text-xs font-medium"> </Label> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">INPUT TYPE</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<div className="w-[140px]">
<Select <Select
value={config.inputType || config.type || "text"} value={config.inputType || config.type || "text"}
onValueChange={(value) => updateConfig("inputType", value)} onValueChange={(value) => updateConfig("inputType", value)}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="입력 타입 선택" /> <SelectValue placeholder="입력 타입 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -121,34 +102,27 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
</div>
{/* 채번 타입 전용 설정 */} {/* NUMBERING 섹션 - 채번 타입 전용 */}
{config.inputType === "numbering" && ( {config.inputType === "numbering" && (
<div className="space-y-3"> <div className="border-b border-border/50 pb-3 mb-3">
<Separator /> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">NUMBERING</h4>
<div className="rounded-md border border-primary/20 bg-primary/10 p-3"> <div className="rounded-md border border-primary/20 bg-primary/5 p-2 mb-2">
<p className="text-xs font-medium text-primary"> </p> <p className="text-[10px] text-primary">
<p className="mt-1 text-[10px] text-primary">
<strong> </strong> . <strong> </strong> .
<br />
. .
</p> </p>
</div> </div>
<div className="flex items-center justify-between py-1.5">
{/* 채번 필드는 기본적으로 읽기전용 */} <span className="text-xs text-muted-foreground"> ()</span>
<div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="numberingReadonly"
checked={config.readonly !== false} checked={config.readonly !== false}
onCheckedChange={(checked) => { onCheckedChange={(checked) => updateConfig("readonly", checked)}
updateConfig("readonly", checked);
}}
/> />
<Label htmlFor="numberingReadonly" className="text-xs font-medium cursor-pointer">
()
</Label>
</div> </div>
<p className="text-muted-foreground text-[10px] pl-6"> <p className="text-muted-foreground text-[10px] mt-0.5">
</p> </p>
</div> </div>
@ -157,14 +131,15 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
{/* 채번 타입이 아닌 경우에만 추가 설정 표시 */} {/* 채번 타입이 아닌 경우에만 추가 설정 표시 */}
{config.inputType !== "numbering" && ( {config.inputType !== "numbering" && (
<> <>
<Separator /> {/* FORMAT 섹션 */}
{/* 형식 (텍스트/숫자용) */}
{(config.inputType === "text" || !config.inputType) && ( {(config.inputType === "text" || !config.inputType) && (
<div className="space-y-2"> <div className="border-b border-border/50 pb-3 mb-3">
<Label className="text-xs font-medium"> </Label> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">FORMAT</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<div className="w-[140px]">
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}> <Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="형식 선택" /> <SelectValue placeholder="형식 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -177,92 +152,108 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
</div>
)} )}
{/* 플레이스홀더 */} {/* PLACEHOLDER 섹션 */}
<div className="space-y-2"> <div className="border-b border-border/50 pb-3 mb-3">
<Label className="text-xs font-medium"></Label> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">PLACEHOLDER</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<div className="w-[140px]">
<Input <Input
value={config.placeholder || ""} value={config.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="입력 안내 텍스트" placeholder="입력 안내"
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
</div>
{/* 숫자/슬라이더 전용 설정 */} {/* RANGE 섹션 - 숫자/슬라이더 전용 */}
{(config.inputType === "number" || config.inputType === "slider") && ( {(config.inputType === "number" || config.inputType === "slider") && (
<> <div className="border-b border-border/50 pb-3 mb-3">
<Separator /> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">RANGE</h4>
<div className="grid grid-cols-3 gap-2"> <div className="flex gap-2">
<div className="space-y-2"> <div className="flex-1">
<Label className="text-xs font-medium"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.min ?? ""} value={config.min ?? ""}
onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)} onChange={(e) => updateConfig("min", e.target.value ? Number(e.target.value) : undefined)}
placeholder="0" placeholder="0"
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="flex-1">
<Label className="text-xs font-medium"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.max ?? ""} value={config.max ?? ""}
onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)} onChange={(e) => updateConfig("max", e.target.value ? Number(e.target.value) : undefined)}
placeholder="100" placeholder="100"
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
<div className="space-y-2"> <div className="flex-1">
<Label className="text-xs font-medium"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.step ?? ""} value={config.step ?? ""}
onChange={(e) => updateConfig("step", e.target.value ? Number(e.target.value) : undefined)} onChange={(e) => updateConfig("step", e.target.value ? Number(e.target.value) : undefined)}
placeholder="1" placeholder="1"
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
</> </div>
)} )}
{/* 여러 줄 텍스트 전용 설정 */} {/* TEXTAREA 섹션 */}
{config.inputType === "textarea" && ( {config.inputType === "textarea" && (
<div className="space-y-2"> <div className="border-b border-border/50 pb-3 mb-3">
<Label className="text-xs font-medium"> </Label> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">TEXTAREA</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"> </span>
<div className="w-[140px]">
<Input <Input
type="number" type="number"
value={config.rows || 3} value={config.rows || 3}
onChange={(e) => updateConfig("rows", parseInt(e.target.value) || 3)} onChange={(e) => updateConfig("rows", parseInt(e.target.value) || 3)}
min={2} min={2}
max={20} max={20}
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
</div>
)} )}
{/* 마스크 입력 (선택) */} {/* INPUT MASK 섹션 */}
<div className="space-y-2"> <div className="border-b border-border/50 pb-3 mb-3">
<Label className="text-xs font-medium"> ()</Label> <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">INPUT MASK</h4>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"></span>
<div className="w-[140px]">
<Input <Input
value={config.mask || ""} value={config.mask || ""}
onChange={(e) => updateConfig("mask", e.target.value)} onChange={(e) => updateConfig("mask", e.target.value)}
placeholder="예: ###-####-####" placeholder="###-####-####"
className="h-8 text-xs" className="h-7 text-xs"
/> />
<p className="text-muted-foreground text-[10px]"># = , A = , * = </p> </div>
</div>
<p className="text-muted-foreground text-[10px] mt-0.5"># = , A = , * = </p>
</div> </div>
<Separator /> {/* AUTO GENERATION 섹션 */}
<div className="border-b border-border/50 pb-3 mb-3">
{/* 자동생성 기능 */} <h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground py-2">AUTO GENERATION</h4>
<div className="space-y-3"> <div className="flex items-center justify-between py-1.5">
<div className="flex items-center space-x-2"> <span className="text-xs text-muted-foreground"> </span>
<Checkbox <Checkbox
id="autoGenerationEnabled"
checked={config.autoGeneration?.enabled || false} checked={config.autoGeneration?.enabled || false}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false }; const currentConfig = config.autoGeneration || { type: "none", enabled: false };
@ -272,16 +263,14 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}); });
}} }}
/> />
<Label htmlFor="autoGenerationEnabled" className="text-xs font-medium cursor-pointer">
</Label>
</div> </div>
{/* 자동생성 타입 선택 */}
{config.autoGeneration?.enabled && ( {config.autoGeneration?.enabled && (
<div className="space-y-3 pl-6"> <div className="mt-1 space-y-1">
<div className="space-y-2"> {/* 자동생성 타입 */}
<Label className="text-xs font-medium"> </Label> <div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground"></span>
<div className="w-[140px]">
<Select <Select
value={config.autoGeneration?.type || "none"} value={config.autoGeneration?.type || "none"}
onValueChange={(value: AutoGenerationType) => { onValueChange={(value: AutoGenerationType) => {
@ -292,7 +281,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}); });
}} }}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="자동생성 타입 선택" /> <SelectValue placeholder="자동생성 타입 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -308,29 +297,28 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
<SelectItem value="department"> </SelectItem> <SelectItem value="department"> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
</div>
{/* 선택된 타입 설명 */}
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && ( {config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
<p className="text-muted-foreground text-[10px]"> <p className="text-muted-foreground text-[10px] mt-0.5">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)} {AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
</p> </p>
)} )}
</div>
{/* 채번 규칙 선택 */} {/* 채번 규칙 선택 */}
{config.autoGeneration?.type === "numbering_rule" && ( {config.autoGeneration?.type === "numbering_rule" && (
<> <div className="mt-1 space-y-1">
{/* 부모 메뉴 선택 */} <div className="flex items-center justify-between py-1.5">
<div className="space-y-2"> <span className="text-xs text-muted-foreground">
<Label className="text-xs font-medium">
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </span>
<div className="w-[140px]">
<Select <Select
value={selectedMenuObjid?.toString() || ""} value={selectedMenuObjid?.toString() || ""}
onValueChange={(value) => { onValueChange={(value) => {
const menuId = parseInt(value); const menuId = parseInt(value);
setSelectedMenuObjid(menuId); setSelectedMenuObjid(menuId);
updateConfig("autoGeneration", { updateConfig("autoGeneration", {
...config.autoGeneration, ...config.autoGeneration,
selectedMenuObjid: menuId, selectedMenuObjid: menuId,
@ -338,8 +326,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}} }}
disabled={loadingMenus} disabled={loadingMenus}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} /> <SelectValue placeholder={loadingMenus ? "로딩 중..." : "메뉴 선택"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{parentMenus.length === 0 ? ( {parentMenus.length === 0 ? (
@ -356,13 +344,14 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
{/* 채번 규칙 선택 */}
{selectedMenuObjid ? ( {selectedMenuObjid ? (
<div className="space-y-2"> <div className="flex items-center justify-between py-1.5">
<Label className="text-xs font-medium"> <span className="text-xs text-muted-foreground">
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </span>
<div className="w-[140px]">
<Select <Select
value={config.autoGeneration?.options?.numberingRuleId || ""} value={config.autoGeneration?.options?.numberingRuleId || ""}
onValueChange={(value) => { onValueChange={(value) => {
@ -376,8 +365,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}} }}
disabled={loadingRules} disabled={loadingRules}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} /> <SelectValue placeholder={loadingRules ? "로딩 중..." : "규칙 선택"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{numberingRules.length === 0 ? ( {numberingRules.length === 0 ? (
@ -394,22 +383,23 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
) : ( ) : (
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800"> <div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-[10px] text-amber-800">
</div> </div>
)} )}
</> </div>
)} )}
{/* 자동생성 옵션 (랜덤/순차용) */} {/* 자동생성 옵션 (랜덤/순차용) */}
{config.autoGeneration?.type && {config.autoGeneration?.type &&
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && ( ["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
<div className="space-y-2"> <div className="mt-1 space-y-1">
{/* 길이 설정 */}
{["random_string", "random_number"].includes(config.autoGeneration.type) && ( {["random_string", "random_number"].includes(config.autoGeneration.type) && (
<div className="space-y-1"> <div className="flex items-center justify-between py-1.5">
<Label className="text-xs font-medium"></Label> <span className="text-xs text-muted-foreground"></span>
<div className="w-[140px]">
<Input <Input
type="number" type="number"
min="1" min="1"
@ -424,14 +414,15 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}, },
}); });
}} }}
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
)} )}
{/* 접두사 */} <div className="flex items-center justify-between py-1.5">
<div className="space-y-1"> <span className="text-xs text-muted-foreground"></span>
<Label className="text-xs font-medium"></Label> <div className="w-[140px]">
<Input <Input
value={config.autoGeneration?.options?.prefix || ""} value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => { onChange={(e) => {
@ -444,13 +435,14 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}); });
}} }}
placeholder="예: INV-" placeholder="예: INV-"
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
{/* 접미사 */} <div className="flex items-center justify-between py-1.5">
<div className="space-y-1"> <span className="text-xs text-muted-foreground"></span>
<Label className="text-xs font-medium"></Label> <div className="w-[140px]">
<Input <Input
value={config.autoGeneration?.options?.suffix || ""} value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => { onChange={(e) => {
@ -462,14 +454,14 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
}, },
}); });
}} }}
className="h-8 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
{/* 미리보기 */} <div className="py-1.5">
<div className="space-y-1"> <span className="text-[10px] text-muted-foreground"></span>
<Label className="text-xs font-medium"></Label> <div className="mt-1 rounded border bg-muted p-1.5 text-xs font-mono">
<div className="rounded border bg-muted p-2 text-xs font-mono">
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)} {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div> </div>
</div> </div>