ERP-node/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputCon...

2076 lines
96 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useMemo, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Plus, X, ChevronDown, ChevronRight } from "lucide-react";
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup, DisplayItem, DisplayItemType, EmptyBehavior, DisplayFieldFormat } from "./types";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { getSecondLevelMenus, getCategoryColumns, getCategoryValues } from "@/lib/api/tableCategoryValue";
2025-11-19 10:03:38 +09:00
import { CalculationBuilder } from "./CalculationBuilder";
export interface SelectedItemsDetailInputConfigPanelProps {
config: SelectedItemsDetailInputConfig;
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼
allTables?: Array<{ tableName: string; displayName?: string }>;
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
onTargetTableChange?: (tableName: string) => void; // 🆕 대상 테이블 변경 콜백 (기존 onTableChange 대체)
}
/**
* SelectedItemsDetailInput
* UI
*/
export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailInputConfigPanelProps> = ({
config,
onChange,
sourceTableColumns = [], // 🆕 원본 테이블 컬럼
targetTableColumns = [], // 🆕 대상 테이블 컬럼
allTables = [],
screenTableName, // 🆕 현재 화면의 테이블명
onSourceTableChange, // 🆕 원본 테이블 변경 콜백
onTargetTableChange, // 🆕 대상 테이블 변경 콜백
}) => {
const [localFields, setLocalFields] = useState<AdditionalFieldDefinition[]>(config.additionalFields || []);
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
// 🆕 필드 그룹 상태
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
// 🆕 그룹별 펼침/접힘 상태
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
// 🆕 원본 테이블 선택 상태
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
// 🆕 대상 테이블 선택 상태 (기존 tableSelectOpen)
const [tableSelectOpen, setTableSelectOpen] = useState(false);
const [tableSearchValue, setTableSearchValue] = useState("");
// 🆕 카테고리 매핑을 위한 상태
const [secondLevelMenus, setSecondLevelMenus] = useState<Array<{ menuObjid: number; menuName: string; parentMenuName: string }>>([]);
const [categoryColumns, setCategoryColumns] = useState<Record<string, Array<{ columnName: string; columnLabel: string }>>>({});
const [categoryValues, setCategoryValues] = useState<Record<string, Array<{ valueCode: string; valueLabel: string }>>>({});
// 🆕 부모 데이터 매핑: 각 매핑별 소스 테이블 컬럼 상태
const [mappingSourceColumns, setMappingSourceColumns] = useState<Record<number, Array<{ columnName: string; columnLabel?: string; dataType?: string }>>>({});
// 🆕 소스 테이블 선택 시 컬럼 로드
const loadMappingSourceColumns = async (tableName: string, mappingIndex: number) => {
try {
console.log(`🔍 [매핑 ${mappingIndex}] 소스 테이블 컬럼 로드:`, tableName);
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getColumnList(tableName);
if (response.success && response.data) {
const columns = response.data.columns || [];
setMappingSourceColumns(prev => ({
...prev,
[mappingIndex]: columns.map((col: any) => ({
columnName: col.columnName,
columnLabel: col.displayName || col.columnLabel || col.columnName,
dataType: col.dataType,
}))
}));
console.log(`✅ [매핑 ${mappingIndex}] 컬럼 로드 성공:`, columns.length);
} else {
console.error(`❌ [매핑 ${mappingIndex}] 컬럼 로드 실패:`, response);
}
} catch (error) {
console.error(`❌ [매핑 ${mappingIndex}] 컬럼 로드 오류:`, error);
}
};
// 2레벨 메뉴 목록 로드
useEffect(() => {
const loadMenus = async () => {
const response = await getSecondLevelMenus();
if (response.success && response.data) {
setSecondLevelMenus(response.data);
}
};
loadMenus();
}, []);
// 메뉴 선택 시 카테고리 목록 로드
const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
if (!config.targetTable) {
console.warn("⚠️ targetTable이 설정되지 않았습니다");
return;
}
console.log("🔍 카테고리 목록 로드 시작", { targetTable: config.targetTable, menuObjid, fieldType });
const response = await getCategoryColumns(config.targetTable);
console.log("📥 getCategoryColumns 응답:", response);
if (response.success && response.data) {
console.log("✅ 카테고리 컬럼 데이터:", response.data);
setCategoryColumns(prev => ({ ...prev, [fieldType]: response.data }));
} else {
console.error("❌ 카테고리 컬럼 로드 실패:", response);
}
// valueMapping 업데이트
handleChange("autoCalculation", {
...config.autoCalculation,
valueMapping: {
...config.autoCalculation.valueMapping,
_selectedMenus: {
...(config.autoCalculation.valueMapping as any)?._selectedMenus,
[fieldType]: menuObjid,
},
},
});
};
// 카테고리 선택 시 카테고리 값 목록 로드
const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
if (!config.targetTable) return;
const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
if (response.success && response.data) {
setCategoryValues(prev => ({ ...prev, [fieldType]: response.data }));
}
// valueMapping 업데이트
handleChange("autoCalculation", {
...config.autoCalculation,
valueMapping: {
...config.autoCalculation.valueMapping,
_selectedCategories: {
...(config.autoCalculation.valueMapping as any)?._selectedCategories,
[fieldType]: columnName,
},
},
});
};
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
React.useEffect(() => {
if (screenTableName && !config.targetTable) {
console.log("✨ 현재 화면 테이블을 저장 대상 테이블로 자동 설정:", screenTableName);
handleChange("targetTable", screenTableName);
// 컬럼도 자동 로드
if (onTargetTableChange) {
onTargetTableChange(screenTableName);
}
}
}, [screenTableName]); // config.targetTable은 의존성에서 제외 (한 번만 실행)
const handleChange = (key: keyof SelectedItemsDetailInputConfig, value: any) => {
// 🔧 기존 config와 병합하여 다른 속성 유지
onChange({ ...config, [key]: value });
};
const handleFieldsChange = (fields: AdditionalFieldDefinition[]) => {
setLocalFields(fields);
handleChange("additionalFields", fields);
};
const handleDisplayColumnsChange = (columns: Array<{ name: string; label: string; width?: string }>) => {
setDisplayColumns(columns);
handleChange("displayColumns", columns);
};
// 필드 추가
const addField = () => {
const newField: AdditionalFieldDefinition = {
name: `field_${localFields.length + 1}`,
label: `필드 ${localFields.length + 1}`,
type: "text",
};
handleFieldsChange([...localFields, newField]);
};
// 필드 제거
const removeField = (index: number) => {
handleFieldsChange(localFields.filter((_, i) => i !== index));
};
// 필드 수정
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
const newFields = [...localFields];
newFields[index] = { ...newFields[index], ...updates };
handleFieldsChange(newFields);
};
// 🆕 필드 그룹 관리
const handleFieldGroupsChange = (groups: FieldGroup[]) => {
setLocalFieldGroups(groups);
handleChange("fieldGroups", groups);
};
const addFieldGroup = () => {
const newGroup: FieldGroup = {
id: `group_${localFieldGroups.length + 1}`,
title: `그룹 ${localFieldGroups.length + 1}`,
order: localFieldGroups.length,
};
handleFieldGroupsChange([...localFieldGroups, newGroup]);
};
const removeFieldGroup = (groupId: string) => {
// 그룹 삭제 시 해당 그룹에 속한 필드들의 groupId도 제거
const updatedFields = localFields.map(field =>
field.groupId === groupId ? { ...field, groupId: undefined } : field
);
setLocalFields(updatedFields);
handleChange("additionalFields", updatedFields);
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
};
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
const newGroups = localFieldGroups.map(g =>
g.id === groupId ? { ...g, ...updates } : g
);
handleFieldGroupsChange(newGroups);
};
// 표시 컬럼 추가
const addDisplayColumn = (columnName: string, columnLabel: string) => {
if (!displayColumns.some(col => col.name === columnName)) {
handleDisplayColumnsChange([...displayColumns, { name: columnName, label: columnLabel }]);
}
};
// 표시 컬럼 제거
const removeDisplayColumn = (columnName: string) => {
handleDisplayColumnsChange(displayColumns.filter((col) => col.name !== columnName));
};
// 🆕 표시 컬럼용: 원본 테이블에서 사용되지 않은 컬럼 목록
const availableColumns = useMemo(() => {
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
return sourceTableColumns.filter((col) => !usedColumns.has(col.columnName));
}, [sourceTableColumns, displayColumns, localFields]);
// 🆕 추가 입력 필드용: 대상 테이블에서 사용되지 않은 컬럼 목록
const availableTargetColumns = useMemo(() => {
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
return targetTableColumns.filter((col) => !usedColumns.has(col.columnName));
}, [targetTableColumns, displayColumns, localFields]);
// 🆕 원본 테이블 필터링
const filteredSourceTables = useMemo(() => {
if (!sourceTableSearchValue) return allTables;
const searchLower = sourceTableSearchValue.toLowerCase();
return allTables.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
);
}, [allTables, sourceTableSearchValue]);
// 🆕 그룹별 항목 표시 설정 핸들러
const addDisplayItemToGroup = (groupId: string, type: DisplayItemType) => {
const newItem: DisplayItem = {
type,
id: `display-${Date.now()}`,
};
if (type === "field") {
// 해당 그룹의 필드만 선택 가능하도록
const groupFields = localFields.filter(f => f.groupId === groupId);
newItem.fieldName = groupFields[0]?.name || "";
newItem.format = "text";
newItem.emptyBehavior = "default";
} else if (type === "icon") {
newItem.icon = "Circle";
} else if (type === "text") {
newItem.value = "텍스트";
}
const updatedGroups = localFieldGroups.map(g => {
if (g.id === groupId) {
return {
...g,
displayItems: [...(g.displayItems || []), newItem]
};
}
return g;
});
setLocalFieldGroups(updatedGroups);
handleChange("fieldGroups", updatedGroups);
};
const removeDisplayItemFromGroup = (groupId: string, itemIndex: number) => {
const updatedGroups = localFieldGroups.map(g => {
if (g.id === groupId) {
return {
...g,
displayItems: (g.displayItems || []).filter((_, i) => i !== itemIndex)
};
}
return g;
});
setLocalFieldGroups(updatedGroups);
handleChange("fieldGroups", updatedGroups);
};
const updateDisplayItemInGroup = (groupId: string, itemIndex: number, updates: Partial<DisplayItem>) => {
const updatedGroups = localFieldGroups.map(g => {
if (g.id === groupId) {
const updatedItems = [...(g.displayItems || [])];
updatedItems[itemIndex] = { ...updatedItems[itemIndex], ...updates };
return {
...g,
displayItems: updatedItems
};
}
return g;
});
setLocalFieldGroups(updatedGroups);
handleChange("fieldGroups", updatedGroups);
};
// 🆕 선택된 원본 테이블 표시명
const selectedSourceTableLabel = useMemo(() => {
if (!config.sourceTable) return "원본 테이블을 선택하세요";
const table = allTables.find((t) => t.tableName === config.sourceTable);
return table ? table.displayName || table.tableName : config.sourceTable;
}, [config.sourceTable, allTables]);
// 대상 테이블 필터링
const filteredTables = useMemo(() => {
if (!tableSearchValue) return allTables;
const searchLower = tableSearchValue.toLowerCase();
return allTables.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
);
}, [allTables, tableSearchValue]);
// 선택된 대상 테이블 표시명
const selectedTableLabel = useMemo(() => {
if (!config.targetTable) return "저장 대상 테이블을 선택하세요";
const table = allTables.find((t) => t.tableName === config.targetTable);
return table ? table.displayName || table.tableName : config.targetTable;
}, [config.targetTable, allTables]);
return (
<div className="space-y-4">
{/* 데이터 소스 ID */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
ID <span className="text-primary font-normal">( )</span>
</Label>
<Input
value={config.dataSourceId || ""}
onChange={(e) => handleChange("dataSourceId", e.target.value)}
placeholder="비워두면 URL 파라미터에서 자동 설정"
className="h-7 text-xs sm:h-8 sm:text-sm"
/>
<p className="text-[10px] text-primary font-medium sm:text-xs">
URL (Button이 )
</p>
<p className="text-[10px] text-gray-500 sm:text-xs">
</p>
</div>
{/* 🆕 원본 데이터 테이블 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Popover open={sourceTableSelectOpen} onOpenChange={setSourceTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={sourceTableSelectOpen}
className="h-8 w-full justify-between text-xs sm:text-sm"
>
{selectedSourceTableLabel}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." value={sourceTableSearchValue} onValueChange={setSourceTableSearchValue} className="h-8 text-xs sm:text-sm" />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{filteredSourceTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
handleChange("sourceTable", currentValue);
setSourceTableSelectOpen(false);
setSourceTableSearchValue("");
if (onSourceTableChange) {
onSourceTableChange(currentValue);
}
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
config.sourceTable === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-gray-500 sm:text-xs">
(: item_info)
</p>
</div>
{/* 저장 대상 테이블 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm">
<span className="text-destructive">*</span>
</Label>
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableSelectOpen}
className="h-7 w-full justify-between text-xs sm:h-8 sm:text-sm"
>
{selectedTableLabel}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput
placeholder="테이블 검색..."
value={tableSearchValue}
onValueChange={setTableSearchValue}
className="text-xs sm:text-sm"
/>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
{filteredTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
handleChange("targetTable", currentValue);
setTableSelectOpen(false);
setTableSearchValue("");
if (onTargetTableChange) {
onTargetTableChange(currentValue);
}
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
config.targetTable === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-gray-500 sm:text-xs"> </p>
</div>
{/* 표시할 원본 데이터 컬럼 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
<div className="space-y-2">
{displayColumns.map((col, index) => (
<div key={index} className="flex items-center gap-2">
<Input value={col.label || col.name} readOnly className="h-7 flex-1 text-xs sm:h-8 sm:text-sm" />
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeDisplayColumn(col.name)}
className="h-6 w-6 text-red-500 hover:bg-red-50 sm:h-7 sm:w-7"
>
<X className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
))}
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 w-full border-dashed text-xs sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs sm:text-sm" />
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
{availableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => addDisplayColumn(column.columnName, column.columnLabel || column.columnName)}
className="text-xs sm:text-sm"
>
<div>
<div className="font-medium">{column.columnLabel || column.columnName}</div>
{column.dataType && <div className="text-[10px] text-gray-500">{column.dataType}</div>}
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<p className="text-[10px] text-gray-500 sm:text-xs">
(: 품목코드, )
</p>
</div>
{/* 추가 입력 필드 정의 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
{localFields.map((field, index) => (
<Card key={index} className="border-2">
<CardContent className="space-y-2 pt-3 sm:space-y-3 sm:pt-4">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-gray-700 sm:text-sm"> {index + 1}</span>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeField(index)}
className="h-5 w-5 text-red-500 hover:bg-red-50 sm:h-6 sm:w-6"
>
<X className="h-2 w-2 sm:h-3 sm:w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Popover
open={fieldPopoverOpen[index] || false}
onOpenChange={(open) => setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: open })}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-6 w-full justify-between text-[10px] sm:h-7 sm:text-xs"
>
{field.name || "컬럼 선택"}
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50 sm:h-3 sm:w-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-0 sm:w-[200px]">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
<CommandEmpty className="text-[10px] sm:text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
{availableTargetColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => {
updateField(index, {
name: column.columnName,
label: column.columnLabel || column.columnName,
inputType: column.inputType || "text", // 🆕 inputType 포함
codeCategory: column.codeCategory, // 🆕 codeCategory 포함
});
setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: false });
}}
className="text-[10px] sm:text-xs"
>
<Check
className={cn(
"mr-1 h-2 w-2 sm:mr-2 sm:h-3 sm:w-3",
field.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
<div>
<div className="font-medium">{column.columnLabel}</div>
<div className="text-[9px] text-gray-500">{column.columnName}</div>
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"></Label>
<Input
value={field.label}
onChange={(e) => updateField(index, { label: e.target.value })}
placeholder="필드 라벨"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Input
value={field.inputType || field.type || "text"}
readOnly
disabled
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs bg-muted"
/>
<p className="text-[9px] text-primary sm:text-[10px]">
</p>
</div>
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs">Placeholder</Label>
<Input
value={field.placeholder || ""}
onChange={(e) => updateField(index, { placeholder: e.target.value })}
placeholder="입력 안내"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/>
</div>
</div>
{/* 🆕 원본 데이터 자동 채우기 */}
<div className="space-y-2">
<Label className="text-[10px] sm:text-xs"> ()</Label>
{/* 테이블명 입력 */}
<Input
value={field.autoFillFromTable || ""}
onChange={(e) => updateField(index, { autoFillFromTable: e.target.value })}
placeholder="비워두면 주 데이터 (예: item_price)"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/>
<p className="text-[9px] text-gray-500 sm:text-[10px]">
</p>
{/* 필드 선택 */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-6 w-full justify-between text-[10px] sm:h-7 sm:text-xs"
>
{field.autoFillFrom
? sourceTableColumns.find(c => c.columnName === field.autoFillFrom)?.columnLabel || field.autoFillFrom
: "필드 선택 안 함"}
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50 sm:h-3 sm:w-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-0 sm:w-[200px]">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
<CommandEmpty className="text-[10px] sm:text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
<CommandItem
value=""
onSelect={() => updateField(index, { autoFillFrom: undefined })}
className="text-[10px] sm:text-xs"
>
<Check
className={cn(
"mr-1 h-2 w-2 sm:mr-2 sm:h-3 sm:w-3",
!field.autoFillFrom ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
{sourceTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => updateField(index, { autoFillFrom: column.columnName })}
className="text-[10px] sm:text-xs"
>
<Check
className={cn(
"mr-1 h-2 w-2 sm:mr-2 sm:h-3 sm:w-3",
field.autoFillFrom === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
<div>
<div className="font-medium">{column.columnLabel}</div>
<div className="text-[9px] text-gray-500">{column.columnName}</div>
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-[9px] text-primary sm:text-[10px]">
{field.autoFillFromTable
? `"${field.autoFillFromTable}" 테이블에서 자동 채우기`
: "주 데이터 소스에서 자동 채우기 (수정 가능)"
}
</p>
</div>
{/* 🆕 필드 그룹 선택 */}
{localFieldGroups.length > 0 && (
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Select
value={field.groupId || "none"}
onValueChange={(value) => updateField(index, { groupId: value === "none" ? undefined : value })}
>
<SelectTrigger className="h-6 text-[10px] sm:h-7 sm:text-xs">
<SelectValue placeholder="그룹 없음" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="text-xs"> </SelectItem>
{localFieldGroups.map((group) => (
<SelectItem key={group.id} value={group.id} className="text-xs">
{group.title} ({group.id})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[9px] text-gray-500 sm:text-[10px]">
ID를
</p>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id={`required-${index}`}
checked={field.required ?? false}
onCheckedChange={(checked) => updateField(index, { required: checked as boolean })}
/>
<Label htmlFor={`required-${index}`} className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
</CardContent>
</Card>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={addField}
className="h-7 w-full border-dashed text-xs sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
</Button>
</div>
{/* 🆕 필드 그룹 관리 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
<p className="text-[10px] text-gray-500 sm:text-xs">
(: 거래처 , )
</p>
{localFieldGroups.map((group, index) => {
const isGroupExpanded = expandedGroups[group.id] ?? true;
return (
<Collapsible
key={group.id}
open={isGroupExpanded}
onOpenChange={(open) => setExpandedGroups(prev => ({ ...prev, [group.id]: open }))}
>
<Card className="border-2">
<CardContent className="p-3 sm:p-4">
<div className="flex items-center justify-between mb-2">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-auto p-0 hover:bg-transparent">
<div className="flex items-center gap-2">
{isGroupExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<span className="text-xs font-semibold sm:text-sm">
{index + 1}: {group.title || group.id}
</span>
</div>
</Button>
</CollapsibleTrigger>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeFieldGroup(group.id)}
className="h-6 w-6 text-red-500 hover:text-red-700 sm:h-7 sm:w-7"
>
<X className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
<CollapsibleContent className="space-y-2 sm:space-y-3 mt-2">
{/* 그룹 ID */}
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ID</Label>
<Input
value={group.id}
onChange={(e) => updateFieldGroup(group.id, { id: e.target.value })}
className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="group_customer"
/>
</div>
{/* 그룹 제목 */}
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> </Label>
<Input
value={group.title}
onChange={(e) => updateFieldGroup(group.id, { title: e.target.value })}
className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="거래처 정보"
/>
</div>
{/* 그룹 설명 */}
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> ()</Label>
<Input
value={group.description || ""}
onChange={(e) => updateFieldGroup(group.id, { description: e.target.value })}
className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="거래처 관련 정보를 입력합니다"
/>
</div>
{/* 표시 순서 */}
<div className="space-y-1">
<Label className="text-[10px] sm:text-xs"> </Label>
<Input
type="number"
value={group.order || 0}
onChange={(e) => updateFieldGroup(group.id, { order: parseInt(e.target.value) || 0 })}
className="h-7 text-xs sm:h-8 sm:text-sm"
min="0"
/>
</div>
{/* 🆕 이 그룹의 항목 표시 설정 */}
<Collapsible
open={expandedDisplayItems[group.id] ?? false}
onOpenChange={(open) => setExpandedDisplayItems(prev => ({ ...prev, [group.id]: open }))}
>
2025-11-18 11:08:05 +09:00
<div className="rounded-lg border-2 border-dashed border-primary/30 bg-primary/5 p-2">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-auto w-full justify-start p-1 hover:bg-transparent">
<div className="flex items-center gap-1">
{expandedDisplayItems[group.id] ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<Label className="text-[10px] font-semibold sm:text-xs cursor-pointer">
({(group.displayItems || []).length})
</Label>
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mt-2">
{/* 추가 버튼들 */}
<div className="flex gap-1 flex-wrap">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addDisplayItemToGroup(group.id, "icon")}
2025-11-18 11:08:05 +09:00
className="h-5 px-1.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
>
2025-11-18 11:08:05 +09:00
<Plus className="mr-0.5 h-2.5 w-2.5 sm:mr-1 sm:h-3 sm:w-3" />
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addDisplayItemToGroup(group.id, "field")}
2025-11-18 11:08:05 +09:00
className="h-5 px-1.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
>
2025-11-18 11:08:05 +09:00
<Plus className="mr-0.5 h-2.5 w-2.5 sm:mr-1 sm:h-3 sm:w-3" />
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addDisplayItemToGroup(group.id, "text")}
2025-11-18 11:08:05 +09:00
className="h-5 px-1.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
>
2025-11-18 11:08:05 +09:00
<Plus className="mr-0.5 h-2.5 w-2.5 sm:mr-1 sm:h-3 sm:w-3" />
</Button>
</div>
2025-11-18 11:08:05 +09:00
<p className="text-[9px] text-muted-foreground sm:text-[10px]">
</p>
{(!group.displayItems || group.displayItems.length === 0) ? (
<div className="rounded border border-dashed p-2 text-center text-[10px] text-muted-foreground">
( " / " )
</div>
) : (
<div className="space-y-1">
{group.displayItems.map((item, itemIndex) => (
<div key={item.id} className="rounded border bg-card p-2 space-y-1">
{/* 헤더 */}
<div className="flex items-center justify-between">
<span className="text-[9px] font-medium sm:text-[10px]">
{item.type === "icon" && "🎨"}
{item.type === "field" && "📝"}
{item.type === "text" && "💬"}
{item.type === "badge" && "🏷️"}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeDisplayItemFromGroup(group.id, itemIndex)}
className="h-4 w-4 p-0"
>
<X className="h-2 w-2" />
</Button>
</div>
{/* 아이콘 설정 */}
{item.type === "icon" && (
<Input
value={item.icon || ""}
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { icon: e.target.value })}
placeholder="Building"
className="h-6 text-[9px] sm:text-[10px]"
/>
)}
{/* 텍스트 설정 */}
{item.type === "text" && (
<Input
value={item.value || ""}
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { value: e.target.value })}
placeholder="| , / , -"
className="h-6 text-[9px] sm:text-[10px]"
/>
)}
{/* 필드 설정 */}
{item.type === "field" && (
<div className="space-y-1">
{/* 필드 선택 */}
<Select
value={item.fieldName || ""}
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { fieldName: value })}
>
2025-11-18 11:08:05 +09:00
<SelectTrigger className="h-6 w-full text-[9px] sm:text-[10px]">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{localFields.filter(f => f.groupId === group.id).map((field) => (
<SelectItem key={field.name} value={field.name} className="text-[9px] sm:text-[10px]">
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 라벨 */}
<Input
value={item.label || ""}
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { label: e.target.value })}
2025-11-18 11:08:05 +09:00
placeholder="라벨 (예: 거래처:)"
className="h-6 w-full text-[9px] sm:text-[10px]"
/>
{/* 표시 형식 */}
<Select
value={item.format || "text"}
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { format: value as DisplayFieldFormat })}
>
2025-11-18 11:08:05 +09:00
<SelectTrigger className="h-6 w-full text-[9px] sm:text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="currency" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="number" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="date" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="badge" className="text-[9px] sm:text-[10px]"></SelectItem>
</SelectContent>
</Select>
{/* 빈 값 처리 */}
2025-11-18 11:08:05 +09:00
<Select
value={item.emptyBehavior || "default"}
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { emptyBehavior: value as EmptyBehavior })}
>
<SelectTrigger className="h-6 w-full text-[9px] sm:text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hide" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="default" className="text-[9px] sm:text-[10px]"></SelectItem>
<SelectItem value="blank" className="text-[9px] sm:text-[10px]"></SelectItem>
</SelectContent>
</Select>
{/* 기본값 */}
{item.emptyBehavior === "default" && (
<Input
value={item.defaultValue || ""}
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: e.target.value })}
placeholder="미입력"
className="h-6 w-full text-[9px] sm:text-[10px]"
/>
)}
</div>
)}
</div>
))}
</div>
)}
</CollapsibleContent>
</div>
</Collapsible>
</CollapsibleContent>
</CardContent>
</Card>
</Collapsible>
);
})}
<Button
type="button"
variant="outline"
size="sm"
onClick={addFieldGroup}
className="h-7 w-full border-dashed text-xs sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
</Button>
{localFieldGroups.length > 0 && (
<p className="text-[10px] text-amber-600 sm:text-xs">
💡 "필드 그룹 ID" ID를
</p>
)}
</div>
{/* 입력 모드 설정 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
<Select
value={config.inputMode || "inline"}
onValueChange={(value) => handleChange("inputMode", value as "inline" | "modal")}
>
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="inline" className="text-xs sm:text-sm">
(Inline)
</SelectItem>
<SelectItem value="modal" className="text-xs sm:text-sm">
(Modal)
</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground sm:text-xs">
{config.inputMode === "modal"
? "추가 버튼 클릭 시 입력창 표시, 완료 후 작은 카드로 표시"
: "모든 항목의 입력창을 항상 표시"}
</p>
</div>
{/* 레이아웃 설정 */}
<div className="space-y-2">
<Label className="text-xs font-semibold sm:text-sm"></Label>
<Select
value={config.layout || "grid"}
onValueChange={(value) => handleChange("layout", value as "grid" | "card")}
>
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="grid" className="text-xs sm:text-sm">
(Grid)
</SelectItem>
<SelectItem value="card" className="text-xs sm:text-sm">
(Card)
</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground sm:text-xs">
{config.layout === "grid" ? "행 단위로 데이터를 표시합니다" : "각 항목을 카드로 표시합니다"}
</p>
</div>
{/* 자동 계산 설정 */}
<div className="space-y-3 rounded-lg border p-3 sm:p-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
2025-11-19 10:03:38 +09:00
<Checkbox
id="enable-auto-calc"
checked={!!config.autoCalculation}
onCheckedChange={(checked) => {
if (checked) {
handleChange("autoCalculation", {
targetField: "",
mode: "template",
inputFields: {
basePrice: "",
discountType: "",
discountValue: "",
roundingType: "",
roundingUnit: "",
},
calculationType: "price",
valueMapping: {},
calculationSteps: [],
});
} else {
handleChange("autoCalculation", undefined);
}
}}
/>
</div>
{config.autoCalculation && (
<div className="space-y-2 border-t pt-2">
2025-11-19 10:03:38 +09:00
{/* 계산 모드 선택 */}
<div className="space-y-1">
2025-11-19 10:03:38 +09:00
<Label className="text-[10px] sm:text-xs"> </Label>
<Select
value={config.autoCalculation.mode || "template"}
onValueChange={(value: "template" | "custom") => {
handleChange("autoCalculation", {
...config.autoCalculation,
mode: value,
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="template">릿 ( )</SelectItem>
<SelectItem value="custom"> ( )</SelectItem>
</SelectContent>
</Select>
</div>
2025-11-19 10:03:38 +09:00
{/* 템플릿 모드 */}
{config.autoCalculation.mode === "template" && (
<>
{/* 계산 필드 선택 */}
<div className="space-y-2 border-t pt-2 mt-2">
<Label className="text-[10px] font-semibold sm:text-xs"> </Label>
<div className="space-y-2">
{/* 계산 결과 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Select
value={config.autoCalculation.targetField || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
targetField: value,
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2025-11-19 10:03:38 +09:00
{/* 기준 단가 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Select
value={config.autoCalculation.inputFields?.basePrice || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
inputFields: {
...config.autoCalculation.inputFields,
basePrice: value,
},
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2025-11-19 10:03:38 +09:00
<div className="grid grid-cols-2 gap-2">
{/* 할인 방식 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Select
value={config.autoCalculation.inputFields?.discountType || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
inputFields: {
...config.autoCalculation.inputFields,
discountType: value,
},
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2025-11-19 10:03:38 +09:00
{/* 할인값 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"></Label>
<Select
value={config.autoCalculation.inputFields?.discountValue || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
inputFields: {
...config.autoCalculation.inputFields,
discountValue: value,
},
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
2025-11-19 10:03:38 +09:00
<div className="grid grid-cols-2 gap-2">
{/* 반올림 방식 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Select
value={config.autoCalculation.inputFields?.roundingType || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
inputFields: {
...config.autoCalculation.inputFields,
roundingType: value,
},
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 반올림 단위 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Select
value={config.autoCalculation.inputFields?.roundingUnit || ""}
onValueChange={(value) => handleChange("autoCalculation", {
...config.autoCalculation,
inputFields: {
...config.autoCalculation.inputFields,
roundingUnit: value,
},
})}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{(config.additionalFields || []).map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
{/* 카테고리 값 매핑 */}
<div className="space-y-3 border-t pt-3 mt-3">
<Label className="text-[10px] font-semibold sm:text-xs"> </Label>
{/* 할인 방식 매핑 */}
<Collapsible>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted"
>
<span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">
{/* 1단계: 메뉴 선택 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">1단계: 메뉴 </Label>
<Select
value={String((config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType || "")}
onValueChange={(value) => handleMenuSelect(Number(value), "discountType")}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="2레벨 메뉴 선택" />
</SelectTrigger>
<SelectContent>
{secondLevelMenus.map((menu) => (
<SelectItem key={menu.menuObjid} value={String(menu.menuObjid)}>
{menu.parentMenuName} &gt; {menu.menuName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 2단계: 카테고리 선택 */}
{(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 </Label>
<Select
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType || ""}
onValueChange={(value) => handleCategorySelect(
value,
(config.autoCalculation.valueMapping as any)._selectedMenus.discountType,
"discountType"
)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
<SelectContent>
{(categoryColumns.discountType || []).map((col: any) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 3단계: 값 매핑 */}
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">3단계: 카테고리 </Label>
{["할인없음", "할인율(%)", "할인금액"].map((label, idx) => {
const operations = ["none", "rate", "amount"];
return (
<div key={label} className="flex items-center gap-2">
<span className="text-xs w-20">{label}</span>
<span className="text-xs text-muted-foreground"></span>
<Select
value={
Object.entries(config.autoCalculation.valueMapping?.discountType || {})
.find(([_, op]) => op === operations[idx])?.[0] || ""
}
onValueChange={(value) => {
const newMapping = { ...config.autoCalculation.valueMapping?.discountType };
Object.keys(newMapping).forEach(key => {
if (newMapping[key] === operations[idx]) delete newMapping[key];
});
if (value) {
newMapping[value] = operations[idx];
}
handleChange("autoCalculation", {
...config.autoCalculation,
valueMapping: {
...config.autoCalculation.valueMapping,
discountType: newMapping,
},
});
}}
>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="값 선택" />
</SelectTrigger>
<SelectContent>
{(categoryValues.discountType || []).map((val: any) => (
<SelectItem key={val.valueCode} value={val.valueCode}>
{val.valueLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-[10px] text-muted-foreground w-12">{operations[idx]}</span>
</div>
);
})}
</div>
)}
</CollapsibleContent>
</Collapsible>
{/* 반올림 방식 매핑 */}
<Collapsible>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted"
>
<span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">
{/* 1단계: 메뉴 선택 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">1단계: 메뉴 </Label>
<Select
value={String((config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingType || "")}
onValueChange={(value) => handleMenuSelect(Number(value), "roundingType")}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="2레벨 메뉴 선택" />
</SelectTrigger>
<SelectContent>
{secondLevelMenus.map((menu) => (
<SelectItem key={menu.menuObjid} value={String(menu.menuObjid)}>
{menu.parentMenuName} &gt; {menu.menuName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 2단계: 카테고리 선택 */}
{(config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingType && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 </Label>
<Select
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingType || ""}
onValueChange={(value) => handleCategorySelect(
value,
(config.autoCalculation.valueMapping as any)._selectedMenus.roundingType,
"roundingType"
)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
<SelectContent>
{(categoryColumns.roundingType || []).map((col: any) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 3단계: 값 매핑 */}
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingType && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">3단계: 카테고리 </Label>
{["반올림없음", "반올림", "절삭", "올림"].map((label, idx) => {
const operations = ["none", "round", "floor", "ceil"];
return (
<div key={label} className="flex items-center gap-2">
<span className="text-xs w-20">{label}</span>
<span className="text-xs text-muted-foreground"></span>
<Select
value={
Object.entries(config.autoCalculation.valueMapping?.roundingType || {})
.find(([_, op]) => op === operations[idx])?.[0] || ""
}
onValueChange={(value) => {
const newMapping = { ...config.autoCalculation.valueMapping?.roundingType };
Object.keys(newMapping).forEach(key => {
if (newMapping[key] === operations[idx]) delete newMapping[key];
});
if (value) {
newMapping[value] = operations[idx];
}
handleChange("autoCalculation", {
...config.autoCalculation,
valueMapping: {
...config.autoCalculation.valueMapping,
roundingType: newMapping,
},
});
}}
>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="값 선택" />
</SelectTrigger>
<SelectContent>
{(categoryValues.roundingType || []).map((val: any) => (
<SelectItem key={val.valueCode} value={val.valueCode}>
{val.valueLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-[10px] text-muted-foreground w-12">{operations[idx]}</span>
</div>
);
})}
</div>
)}
</CollapsibleContent>
</Collapsible>
{/* 반올림 단위 매핑 */}
<Collapsible>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted"
>
<span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">
{/* 1단계: 메뉴 선택 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">1단계: 메뉴 </Label>
<Select
value={String((config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingUnit || "")}
onValueChange={(value) => handleMenuSelect(Number(value), "roundingUnit")}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="2레벨 메뉴 선택" />
</SelectTrigger>
<SelectContent>
{secondLevelMenus.map((menu) => (
<SelectItem key={menu.menuObjid} value={String(menu.menuObjid)}>
{menu.parentMenuName} &gt; {menu.menuName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 2단계: 카테고리 선택 */}
{(config.autoCalculation.valueMapping as any)?._selectedMenus?.roundingUnit && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 </Label>
<Select
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingUnit || ""}
onValueChange={(value) => handleCategorySelect(
value,
(config.autoCalculation.valueMapping as any)._selectedMenus.roundingUnit,
"roundingUnit"
)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
<SelectContent>
{(categoryColumns.roundingUnit || []).map((col: any) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 3단계: 값 매핑 */}
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.roundingUnit && (
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]">3단계: 카테고리 </Label>
{["1원", "10원", "100원", "1,000원"].map((label) => {
const unitValue = label === "1,000원" ? 1000 : parseInt(label);
return (
<div key={label} className="flex items-center gap-2">
<span className="text-xs w-20">{label}</span>
<span className="text-xs text-muted-foreground"></span>
<Select
value={
Object.entries(config.autoCalculation.valueMapping?.roundingUnit || {})
.find(([_, val]) => val === unitValue)?.[0] || ""
}
onValueChange={(value) => {
const newMapping = { ...config.autoCalculation.valueMapping?.roundingUnit };
Object.keys(newMapping).forEach(key => {
if (newMapping[key] === unitValue) delete newMapping[key];
});
if (value) {
newMapping[value] = unitValue;
}
handleChange("autoCalculation", {
...config.autoCalculation,
valueMapping: {
...config.autoCalculation.valueMapping,
roundingUnit: newMapping,
},
});
}}
>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="값 선택" />
</SelectTrigger>
<SelectContent>
{(categoryValues.roundingUnit || []).map((val: any) => (
<SelectItem key={val.valueCode} value={val.valueCode}>
{val.valueLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-[10px] text-muted-foreground w-12">{unitValue}</span>
</div>
);
})}
</div>
)}
</CollapsibleContent>
</Collapsible>
<p className="text-[9px] text-muted-foreground sm:text-[10px]">
💡 1단계: 메뉴 2단계: 카테고리 3단계:
</p>
</div>
2025-11-19 10:03:38 +09:00
</>
)}
{/* 커스텀 모드 (계산식 빌더) */}
{config.autoCalculation.mode === "custom" && (
<div className="space-y-2 border-t pt-2 mt-2">
<CalculationBuilder
steps={config.autoCalculation.calculationSteps || []}
availableFields={config.additionalFields || []}
onChange={(steps) => {
handleChange("autoCalculation", {
...config.autoCalculation,
calculationSteps: steps,
});
}}
/>
</div>
)}
</div>
)}
</div>
{/* 옵션 */}
<div className="space-y-2 rounded-lg border p-3 sm:p-4">
<div className="flex items-center space-x-2">
<Checkbox
id="show-index"
checked={config.showIndex ?? true}
onCheckedChange={(checked) => handleChange("showIndex", checked as boolean)}
/>
<Label htmlFor="show-index" className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="allow-remove"
checked={config.allowRemove ?? false}
onCheckedChange={(checked) => handleChange("allowRemove", checked as boolean)}
/>
<Label htmlFor="allow-remove" className="cursor-pointer text-[10px] font-normal sm:text-xs">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="disabled"
checked={config.disabled ?? false}
onCheckedChange={(checked) => handleChange("disabled", checked as boolean)}
/>
<Label htmlFor="disabled" className="cursor-pointer text-[10px] font-normal sm:text-xs">
( )
</Label>
</div>
</div>
{/* 🆕 부모 데이터 매핑 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold sm:text-sm"> </Label>
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => {
const newMapping = {
sourceTable: "", // 사용자가 선택
sourceField: "",
targetField: "",
defaultValue: undefined,
};
handleChange("parentDataMapping", [
...(config.parentDataMapping || []),
newMapping,
]);
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-[9px] text-muted-foreground sm:text-[10px]">
( ) .
</p>
<div className="space-y-2">
{(config.parentDataMapping || []).map((mapping, index) => (
<Card key={index} className="p-3">
<div className="space-y-2">
{/* 소스 테이블 선택 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs font-normal"
>
{mapping.sourceTable
? allTables.find((t) => t.tableName === mapping.sourceTable)?.displayName ||
mapping.sourceTable
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup>
{allTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
const updated = [...(config.parentDataMapping || [])];
updated[index] = {
...updated[index],
sourceTable: currentValue,
sourceField: "", // 테이블 변경 시 필드 초기화
};
handleChange("parentDataMapping", updated);
// 테이블 선택 시 컬럼 로드
if (currentValue) {
loadMappingSourceColumns(currentValue, index);
}
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.sourceTable === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="text-[8px] text-muted-foreground">
, ,
</p>
</div>
{/* 원본 필드 */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs font-normal"
disabled={!mapping.sourceTable}
>
{mapping.sourceField
? mappingSourceColumns[index]?.find((c) => c.columnName === mapping.sourceField)
?.columnLabel || mapping.sourceField
: "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
{!mapping.sourceTable ? (
<CommandEmpty className="text-xs"> </CommandEmpty>
) : !mappingSourceColumns[index] || mappingSourceColumns[index].length === 0 ? (
<CommandEmpty className="text-xs"> ...</CommandEmpty>
) : (
<>
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup>
{mappingSourceColumns[index].map((col) => {
const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase();
return (
<CommandItem
key={col.columnName}
value={searchValue}
onSelect={() => {
const updated = [...(config.parentDataMapping || [])];
updated[index] = { ...updated[index], sourceField: col.columnName };
handleChange("parentDataMapping", updated);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.sourceField === col.columnName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{col.columnLabel || col.columnName}</span>
{col.dataType && (
<span className="text-[10px] text-muted-foreground">
{col.dataType}
</span>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 저장 필드 (현재 화면 테이블 컬럼) */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> ( )</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs font-normal"
disabled={targetTableColumns.length === 0}
>
{mapping.targetField
? targetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel ||
mapping.targetField
: "저장 테이블 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
{targetTableColumns.length === 0 ? (
<CommandEmpty className="text-xs"> </CommandEmpty>
) : (
<>
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup>
{targetTableColumns.map((col) => {
const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase();
return (
<CommandItem
key={col.columnName}
value={searchValue}
onSelect={() => {
const updated = [...(config.parentDataMapping || [])];
updated[index] = { ...updated[index], targetField: col.columnName };
handleChange("parentDataMapping", updated);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.targetField === col.columnName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{col.columnLabel || col.columnName}</span>
{col.dataType && (
<span className="text-[10px] text-muted-foreground">{col.dataType}</span>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 기본값 (선택사항) */}
<div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> ()</Label>
<Input
value={mapping.defaultValue || ""}
onChange={(e) => {
const updated = [...(config.parentDataMapping || [])];
updated[index] = { ...updated[index], defaultValue: e.target.value };
handleChange("parentDataMapping", updated);
}}
placeholder="값이 없을 때 사용할 기본값"
className="h-7 text-xs"
/>
</div>
{/* 삭제 버튼 */}
<Button
size="sm"
variant="ghost"
className="h-6 w-full text-xs text-destructive hover:text-destructive"
onClick={() => {
const updated = (config.parentDataMapping || []).filter((_, i) => i !== index);
handleChange("parentDataMapping", updated);
}}
>
<X className="mr-1 h-3 w-3" />
</Button>
</div>
</Card>
))}
</div>
{(config.parentDataMapping || []).length === 0 && (
<p className="text-center text-[10px] text-muted-foreground py-4">
. "추가" .
</p>
)}
{/* 예시 */}
<div className="rounded-lg bg-green-50 p-2 text-xs">
<p className="mb-1 text-[10px] font-medium text-green-900">💡 </p>
<div className="space-y-1 text-[9px] text-green-700">
<p><strong> 1: 거래처 ID</strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">customer_mng</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">id</code> : <code className="bg-green-100 px-1">customer_id</code></p>
<p className="mt-1"><strong> 2: 품목 ID</strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">item_info</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">id</code> : <code className="bg-green-100 px-1">item_id</code></p>
<p className="mt-1"><strong> 3: 품목 </strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">item_info</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">standard_price</code> : <code className="bg-green-100 px-1">base_price</code></p>
</div>
</div>
</div>
{/* 사용 예시 */}
<div className="rounded-lg bg-blue-50 p-2 text-xs sm:p-3 sm:text-sm">
<p className="mb-1 font-medium text-blue-900">💡 </p>
<ul className="space-y-1 text-[10px] text-blue-700 sm:text-xs">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
);
};
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";