feat: RepeaterInput 하위 데이터 조회 컬럼 설정 기능 개선
- 표시 컬럼 순서 변경 기능 추가 (columnOrder) - 조회 컬럼 -> 저장 컬럼 매핑 기능 추가 (fieldMappings) - 컬럼별 라벨, 순서, 저장 여부 통합 설정 UI 구현 - 하위 호환성 유지 (fieldMappings 없으면 기존 로직 사용)
This commit is contained in:
parent
0f9e91050e
commit
d4b5bdd835
|
|
@ -309,6 +309,19 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
|||
_subDataMaxValue: maxValue,
|
||||
};
|
||||
|
||||
// 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) {
|
||||
|
|
@ -322,6 +335,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -78,8 +78,20 @@ export const SubDataLookupPanel: React.FC<SubDataLookupPanelProps> = ({
|
|||
return config.lookup.columnLabels?.[columnName] || columnName;
|
||||
};
|
||||
|
||||
// 표시할 컬럼 목록
|
||||
const displayColumns = config.lookup.displayColumns || [];
|
||||
// 표시할 컬럼 목록 (columnOrder가 있으면 순서 적용)
|
||||
const displayColumns = useMemo(() => {
|
||||
const columns = config.lookup.displayColumns || [];
|
||||
const columnOrder = config.lookup.columnOrder;
|
||||
|
||||
if (columnOrder && columnOrder.length > 0) {
|
||||
// columnOrder 순서대로 정렬 (displayColumns에 있는 것만)
|
||||
const orderedCols = columnOrder.filter(col => columns.includes(col));
|
||||
// columnOrder에 없지만 displayColumns에 있는 컬럼 추가
|
||||
const remainingCols = columns.filter(col => !columnOrder.includes(col));
|
||||
return [...orderedCols, ...remainingCols];
|
||||
}
|
||||
return columns;
|
||||
}, [config.lookup.displayColumns, config.lookup.columnOrder]);
|
||||
|
||||
// 요약 정보 표시용 선택 상태
|
||||
const summaryText = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -197,10 +197,18 @@ export function useSubDataLookup(props: UseSubDataLookupProps): UseSubDataLookup
|
|||
return "선택 안됨";
|
||||
}
|
||||
|
||||
const { displayColumns, columnLabels } = config.lookup;
|
||||
const { displayColumns, columnLabels, columnOrder } = config.lookup;
|
||||
const parts: string[] = [];
|
||||
|
||||
displayColumns.forEach((col) => {
|
||||
// columnOrder가 있으면 순서 적용, 없으면 displayColumns 순서
|
||||
let orderedColumns = displayColumns;
|
||||
if (columnOrder && columnOrder.length > 0) {
|
||||
const orderedCols = columnOrder.filter(col => displayColumns.includes(col));
|
||||
const remainingCols = displayColumns.filter(col => !columnOrder.includes(col));
|
||||
orderedColumns = [...orderedCols, ...remainingCols];
|
||||
}
|
||||
|
||||
orderedColumns.forEach((col) => {
|
||||
const value = selectedItem[col];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
const label = columnLabels?.[col] || col;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ export type RepeaterData = RepeaterItemData[];
|
|||
// 품목 선택 시 재고/단가 등 관련 데이터를 조회하고 선택하는 기능
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 선택 데이터 필드 매핑 설정
|
||||
*/
|
||||
export interface SubDataFieldMapping {
|
||||
sourceColumn: string; // 조회 테이블 컬럼 (예: lot_number)
|
||||
targetField: string; // 저장 테이블 컬럼 (예: lot_number) 또는 "" (선택안함)
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 데이터 조회 테이블 설정
|
||||
*/
|
||||
|
|
@ -121,6 +129,8 @@ export interface SubDataLookupSettings {
|
|||
linkColumn: string; // 상위 데이터와 연결할 컬럼 (예: item_code)
|
||||
displayColumns: string[]; // 표시할 컬럼들 (예: ["warehouse_code", "location_code", "quantity"])
|
||||
columnLabels?: Record<string, string>; // 컬럼 라벨 (예: { warehouse_code: "창고" })
|
||||
columnOrder?: string[]; // 컬럼 표시 순서 (없으면 displayColumns 순서 사용)
|
||||
fieldMappings?: SubDataFieldMapping[]; // 선택 데이터 저장 매핑 (조회 컬럼 → 저장 컬럼)
|
||||
additionalFilters?: Record<string, any>; // 추가 필터 조건
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue