feat: RepeaterInput 하위 데이터 조회 컬럼 설정 기능 개선
- 표시 컬럼 순서 변경 기능 추가 (columnOrder) - 조회 컬럼 -> 저장 컬럼 매핑 기능 추가 (fieldMappings) - 컬럼별 라벨, 순서, 저장 여부 통합 설정 UI 구현 - 하위 호환성 유지 (fieldMappings 없으면 기존 로직 사용)
This commit is contained in:
@@ -309,18 +309,32 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||
_subDataMaxValue: maxValue,
|
||||
};
|
||||
|
||||
// 선택된 하위 데이터의 필드 값을 상위 item에 복사 (설정된 경우)
|
||||
// 예: warehouse_code, location_code 등
|
||||
if (subDataLookup.lookup.displayColumns) {
|
||||
subDataLookup.lookup.displayColumns.forEach((col) => {
|
||||
if (selectedItem[col] !== undefined) {
|
||||
// 필드가 정의되어 있으면 복사
|
||||
const fieldDef = fields.find((f) => f.name === col);
|
||||
if (fieldDef || col.includes("_code") || col.includes("_id")) {
|
||||
newItems[itemIndex][col] = selectedItem[col];
|
||||
// fieldMappings가 설정되어 있으면 매핑에 따라 값 복사
|
||||
if (subDataLookup.lookup.fieldMappings && subDataLookup.lookup.fieldMappings.length > 0) {
|
||||
subDataLookup.lookup.fieldMappings.forEach((mapping) => {
|
||||
if (mapping.targetField && mapping.targetField !== "") {
|
||||
// 매핑된 타겟 필드에 소스 컬럼 값 복사
|
||||
const sourceValue = selectedItem[mapping.sourceColumn];
|
||||
if (sourceValue !== undefined) {
|
||||
newItems[itemIndex][mapping.targetField] = sourceValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// fieldMappings가 없으면 기존 로직 (하위 호환성)
|
||||
// 선택된 하위 데이터의 필드 값을 상위 item에 복사 (설정된 경우)
|
||||
// 예: warehouse_code, location_code 등
|
||||
if (subDataLookup.lookup.displayColumns) {
|
||||
subDataLookup.lookup.displayColumns.forEach((col) => {
|
||||
if (selectedItem[col] !== undefined) {
|
||||
// 필드가 정의되어 있으면 복사
|
||||
const fieldDef = fields.find((f) => f.name === col);
|
||||
if (fieldDef || col.includes("_code") || col.includes("_id")) {
|
||||
newItems[itemIndex][col] = selectedItem[col];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setItems(newItems);
|
||||
|
||||
@@ -319,6 +319,103 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 표시 컬럼 순서 가져오기 (columnOrder가 있으면 사용, 없으면 displayColumns 순서)
|
||||
const getOrderedDisplayColumns = (): string[] => {
|
||||
const displayColumns = config.subDataLookup?.lookup?.displayColumns || [];
|
||||
const columnOrder = config.subDataLookup?.lookup?.columnOrder;
|
||||
|
||||
if (columnOrder && columnOrder.length > 0) {
|
||||
// columnOrder에 있는 컬럼만, 순서대로 반환 (displayColumns에 있는 것만)
|
||||
const orderedCols = columnOrder.filter(col => displayColumns.includes(col));
|
||||
// columnOrder에 없지만 displayColumns에 있는 컬럼 추가
|
||||
const remainingCols = displayColumns.filter(col => !columnOrder.includes(col));
|
||||
return [...orderedCols, ...remainingCols];
|
||||
}
|
||||
return displayColumns;
|
||||
};
|
||||
|
||||
// 표시 컬럼 순서 변경 핸들러 (위로)
|
||||
const handleDisplayColumnMoveUp = (columnName: string) => {
|
||||
const orderedColumns = getOrderedDisplayColumns();
|
||||
const index = orderedColumns.indexOf(columnName);
|
||||
if (index <= 0) return;
|
||||
|
||||
const newOrder = [...orderedColumns];
|
||||
[newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]];
|
||||
handleSubDataLookupChange("lookup.columnOrder", newOrder);
|
||||
};
|
||||
|
||||
// 표시 컬럼 순서 변경 핸들러 (아래로)
|
||||
const handleDisplayColumnMoveDown = (columnName: string) => {
|
||||
const orderedColumns = getOrderedDisplayColumns();
|
||||
const index = orderedColumns.indexOf(columnName);
|
||||
if (index < 0 || index >= orderedColumns.length - 1) return;
|
||||
|
||||
const newOrder = [...orderedColumns];
|
||||
[newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]];
|
||||
handleSubDataLookupChange("lookup.columnOrder", newOrder);
|
||||
};
|
||||
|
||||
// 표시 컬럼 토글 시 columnOrder도 업데이트
|
||||
const handleDisplayColumnToggleWithOrder = (columnName: string, checked: boolean) => {
|
||||
const currentColumns = config.subDataLookup?.lookup?.displayColumns || [];
|
||||
const currentOrder = config.subDataLookup?.lookup?.columnOrder || [];
|
||||
const currentMappings = config.subDataLookup?.lookup?.fieldMappings || [];
|
||||
|
||||
let newColumns: string[];
|
||||
let newOrder: string[];
|
||||
let newMappings: { sourceColumn: string; targetField: string }[];
|
||||
|
||||
if (checked) {
|
||||
newColumns = [...currentColumns, columnName];
|
||||
newOrder = [...currentOrder, columnName];
|
||||
// 기본 매핑 추가: 동일한 컬럼명이 targetTable에 있으면 자동 매핑, 없으면 빈 문자열
|
||||
const targetColumn = tableColumns.find((c) => c.columnName === columnName);
|
||||
newMappings = [...currentMappings, { sourceColumn: columnName, targetField: targetColumn ? columnName : "" }];
|
||||
} else {
|
||||
newColumns = currentColumns.filter((c) => c !== columnName);
|
||||
newOrder = currentOrder.filter((c) => c !== columnName);
|
||||
newMappings = currentMappings.filter((m) => m.sourceColumn !== columnName);
|
||||
}
|
||||
|
||||
// displayColumns, columnOrder, fieldMappings 함께 업데이트
|
||||
const newConfig = { ...config.subDataLookup } as SubDataLookupConfig;
|
||||
if (!newConfig.lookup) {
|
||||
newConfig.lookup = { tableName: "", linkColumn: "", displayColumns: [] };
|
||||
}
|
||||
newConfig.lookup.displayColumns = newColumns;
|
||||
newConfig.lookup.columnOrder = newOrder;
|
||||
newConfig.lookup.fieldMappings = newMappings;
|
||||
|
||||
onChange({
|
||||
...config,
|
||||
subDataLookup: newConfig,
|
||||
});
|
||||
};
|
||||
|
||||
// 필드 매핑 변경 핸들러
|
||||
const handleFieldMappingChange = (sourceColumn: string, targetField: string) => {
|
||||
const currentMappings = config.subDataLookup?.lookup?.fieldMappings || [];
|
||||
const existingIndex = currentMappings.findIndex((m) => m.sourceColumn === sourceColumn);
|
||||
|
||||
let newMappings: { sourceColumn: string; targetField: string }[];
|
||||
if (existingIndex >= 0) {
|
||||
newMappings = [...currentMappings];
|
||||
newMappings[existingIndex] = { sourceColumn, targetField };
|
||||
} else {
|
||||
newMappings = [...currentMappings, { sourceColumn, targetField }];
|
||||
}
|
||||
|
||||
handleSubDataLookupChange("lookup.fieldMappings", newMappings);
|
||||
};
|
||||
|
||||
// 특정 컬럼의 현재 매핑된 타겟 필드 가져오기
|
||||
const getFieldMapping = (sourceColumn: string): string => {
|
||||
const mappings = config.subDataLookup?.lookup?.fieldMappings || [];
|
||||
const mapping = mappings.find((m) => m.sourceColumn === sourceColumn);
|
||||
return mapping?.targetField || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 대상 테이블 선택 */}
|
||||
@@ -588,7 +685,7 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
<Checkbox
|
||||
id={`display-col-${col.columnName}`}
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => handleDisplayColumnToggle(col.columnName, checked as boolean)}
|
||||
onCheckedChange={(checked) => handleDisplayColumnToggleWithOrder(col.columnName, checked as boolean)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`display-col-${col.columnName}`}
|
||||
@@ -605,6 +702,103 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 설정 (순서 + 라벨 + 저장 컬럼) */}
|
||||
{(config.subDataLookup?.lookup?.displayColumns?.length || 0) > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-purple-700">컬럼 설정</Label>
|
||||
<p className="text-[10px] text-purple-500">순서, 라벨, 저장 여부를 설정하세요</p>
|
||||
<div className="space-y-1.5 rounded border bg-white p-2">
|
||||
{getOrderedDisplayColumns().map((colName, index) => {
|
||||
const col = subDataTableColumns.find((c) => c.columnName === colName);
|
||||
const currentLabel = config.subDataLookup?.lookup?.columnLabels?.[colName] || "";
|
||||
const currentMapping = getFieldMapping(colName);
|
||||
const orderedColumns = getOrderedDisplayColumns();
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === orderedColumns.length - 1;
|
||||
|
||||
return (
|
||||
<div key={colName} className="rounded bg-purple-50 p-2">
|
||||
{/* 상단: 순서 버튼 + 번호 + 컬럼명 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 순서 변경 버튼 */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => handleDisplayColumnMoveUp(colName)}
|
||||
disabled={isFirst}
|
||||
>
|
||||
<ArrowUp className={cn("h-3 w-3", isFirst ? "text-gray-300" : "text-purple-600")} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => handleDisplayColumnMoveDown(colName)}
|
||||
disabled={isLast}
|
||||
>
|
||||
<ArrowDown className={cn("h-3 w-3", isLast ? "text-gray-300" : "text-purple-600")} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 순서 번호 */}
|
||||
<span className="w-4 text-center text-xs font-medium text-purple-600">{index + 1}</span>
|
||||
|
||||
{/* 컬럼명 */}
|
||||
<div className="flex-1 text-xs">
|
||||
<span className="font-medium">{col?.columnLabel || colName}</span>
|
||||
<span className="ml-1 text-gray-400">({colName})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 중단: 라벨 입력 */}
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<span className="text-[10px] text-gray-500 whitespace-nowrap">표시 라벨:</span>
|
||||
<Input
|
||||
value={currentLabel}
|
||||
onChange={(e) => handleColumnLabelChange(colName, e.target.value)}
|
||||
placeholder={col?.columnLabel || colName}
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 하단: 저장 컬럼 선택 */}
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<span className="text-[10px] text-gray-500 whitespace-nowrap">저장 컬럼:</span>
|
||||
<Select
|
||||
value={currentMapping || "__none__"}
|
||||
onValueChange={(v) => handleFieldMappingChange(colName, v === "__none__" ? "" : v)}
|
||||
>
|
||||
<SelectTrigger className="h-6 flex-1 text-xs">
|
||||
<SelectValue placeholder="선택안함" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__" className="text-xs">
|
||||
선택안함
|
||||
</SelectItem>
|
||||
{tableColumns.map((targetCol) => (
|
||||
<SelectItem key={targetCol.columnName} value={targetCol.columnName} className="text-xs">
|
||||
{targetCol.columnLabel || targetCol.columnName} ({targetCol.columnName})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{config.targetTable && (
|
||||
<p className="text-[10px] text-purple-500">
|
||||
* 저장 대상: {config.targetTable}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 선택 설정 */}
|
||||
{(config.subDataLookup?.lookup?.displayColumns?.length || 0) > 0 && (
|
||||
<div className="space-y-3 border-t border-purple-200 pt-3">
|
||||
|
||||
Reference in New Issue
Block a user