feat(modal-repeater-table): 동적 데이터 소스 전환 기능 및 UniversalFormModal 저장 버튼 옵션 추가
- ModalRepeaterTable: 컬럼 헤더 클릭으로 데이터 소스 동적 전환 - 단순 조인, 복합 조인(다중 테이블), 전용 API 호출 지원 - DynamicDataSourceConfig, MultiTableJoinStep 타입 추가 - 설정 패널에 동적 데이터 소스 설정 모달 추가 - UniversalFormModal: showSaveButton 옵션 추가
This commit is contained in:
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { ItemSelectionModal } from "./ItemSelectionModal";
|
||||
import { RepeaterTable } from "./RepeaterTable";
|
||||
import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types";
|
||||
import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition, DynamicDataSourceOption } from "./types";
|
||||
import { useCalculation } from "./useCalculation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -293,6 +293,9 @@ export function ModalRepeaterTableComponent({
|
||||
|
||||
// 🆕 수주일 일괄 적용 플래그 (딱 한 번만 실행)
|
||||
const [isOrderDateApplied, setIsOrderDateApplied] = useState(false);
|
||||
|
||||
// 🆕 동적 데이터 소스 활성화 상태 (컬럼별로 현재 선택된 옵션 ID)
|
||||
const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});
|
||||
|
||||
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
||||
@@ -409,6 +412,193 @@ export function ModalRepeaterTableComponent({
|
||||
}, [localValue, columnName, component?.id, onFormDataChange, targetTable]);
|
||||
|
||||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||||
|
||||
/**
|
||||
* 동적 데이터 소스 변경 시 호출
|
||||
* 해당 컬럼의 모든 행 데이터를 새로운 소스에서 다시 조회
|
||||
*/
|
||||
const handleDataSourceChange = async (columnField: string, optionId: string) => {
|
||||
console.log(`🔄 데이터 소스 변경: ${columnField} → ${optionId}`);
|
||||
|
||||
// 활성화 상태 업데이트
|
||||
setActiveDataSources((prev) => ({
|
||||
...prev,
|
||||
[columnField]: optionId,
|
||||
}));
|
||||
|
||||
// 해당 컬럼 찾기
|
||||
const column = columns.find((col) => col.field === columnField);
|
||||
if (!column?.dynamicDataSource?.enabled) {
|
||||
console.warn(`⚠️ 컬럼 "${columnField}"에 동적 데이터 소스가 설정되지 않음`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 옵션 찾기
|
||||
const option = column.dynamicDataSource.options.find((opt) => opt.id === optionId);
|
||||
if (!option) {
|
||||
console.warn(`⚠️ 옵션 "${optionId}"을 찾을 수 없음`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 행에 대해 새 값 조회
|
||||
const updatedData = await Promise.all(
|
||||
localValue.map(async (row, index) => {
|
||||
try {
|
||||
const newValue = await fetchDynamicValue(option, row);
|
||||
console.log(` ✅ 행 ${index}: ${columnField} = ${newValue}`);
|
||||
return {
|
||||
...row,
|
||||
[columnField]: newValue,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(` ❌ 행 ${index} 조회 실패:`, error);
|
||||
return row;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 계산 필드 업데이트 후 데이터 반영
|
||||
const calculatedData = calculateAll(updatedData);
|
||||
handleChange(calculatedData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 동적 데이터 소스 옵션에 따라 값 조회
|
||||
*/
|
||||
async function fetchDynamicValue(
|
||||
option: DynamicDataSourceOption,
|
||||
rowData: any
|
||||
): Promise<any> {
|
||||
if (option.sourceType === "table" && option.tableConfig) {
|
||||
// 테이블 직접 조회 (단순 조인)
|
||||
const { tableName, valueColumn, joinConditions } = option.tableConfig;
|
||||
|
||||
const whereConditions: Record<string, any> = {};
|
||||
for (const cond of joinConditions) {
|
||||
const value = rowData[cond.sourceField];
|
||||
if (value === undefined || value === null) {
|
||||
console.warn(`⚠️ 조인 조건의 소스 필드 "${cond.sourceField}" 값이 없음`);
|
||||
return undefined;
|
||||
}
|
||||
whereConditions[cond.targetField] = value;
|
||||
}
|
||||
|
||||
console.log(`🔍 테이블 조회: ${tableName}`, whereConditions);
|
||||
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${tableName}/data`,
|
||||
{ search: whereConditions, size: 1, page: 1 }
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||
return response.data.data.data[0][valueColumn];
|
||||
}
|
||||
return undefined;
|
||||
|
||||
} else if (option.sourceType === "multiTable" && option.multiTableConfig) {
|
||||
// 테이블 복합 조인 (2개 이상 테이블 순차 조인)
|
||||
const { joinChain, valueColumn } = option.multiTableConfig;
|
||||
|
||||
if (!joinChain || joinChain.length === 0) {
|
||||
console.warn("⚠️ 조인 체인이 비어있습니다.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log(`🔗 복합 조인 시작: ${joinChain.length}단계`);
|
||||
|
||||
// 현재 값을 추적 (첫 단계는 현재 행에서 시작)
|
||||
let currentValue: any = null;
|
||||
let currentRow: any = null;
|
||||
|
||||
for (let i = 0; i < joinChain.length; i++) {
|
||||
const step = joinChain[i];
|
||||
const { tableName, joinCondition, outputField } = step;
|
||||
|
||||
// 조인 조건 값 가져오기
|
||||
let fromValue: any;
|
||||
if (i === 0) {
|
||||
// 첫 번째 단계: 현재 행에서 값 가져오기
|
||||
fromValue = rowData[joinCondition.fromField];
|
||||
console.log(` 📍 단계 ${i + 1}: 현재행.${joinCondition.fromField} = ${fromValue}`);
|
||||
} else {
|
||||
// 이후 단계: 이전 조회 결과에서 값 가져오기
|
||||
fromValue = currentRow?.[joinCondition.fromField] || currentValue;
|
||||
console.log(` 📍 단계 ${i + 1}: 이전결과.${joinCondition.fromField} = ${fromValue}`);
|
||||
}
|
||||
|
||||
if (fromValue === undefined || fromValue === null) {
|
||||
console.warn(`⚠️ 단계 ${i + 1}: 조인 조건 값이 없습니다. (${joinCondition.fromField})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 테이블 조회
|
||||
const whereConditions: Record<string, any> = {
|
||||
[joinCondition.toField]: fromValue
|
||||
};
|
||||
|
||||
console.log(` 🔍 단계 ${i + 1}: ${tableName} 조회`, whereConditions);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${tableName}/data`,
|
||||
{ search: whereConditions, size: 1, page: 1 }
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||
currentRow = response.data.data.data[0];
|
||||
currentValue = outputField ? currentRow[outputField] : currentRow;
|
||||
console.log(` ✅ 단계 ${i + 1} 성공:`, { outputField, value: currentValue });
|
||||
} else {
|
||||
console.warn(` ⚠️ 단계 ${i + 1}: 조회 결과 없음`);
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(` ❌ 단계 ${i + 1} 조회 실패:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 값 반환 (마지막 테이블에서 valueColumn 가져오기)
|
||||
const finalValue = currentRow?.[valueColumn];
|
||||
console.log(`🎯 복합 조인 완료: ${valueColumn} = ${finalValue}`);
|
||||
return finalValue;
|
||||
|
||||
} else if (option.sourceType === "api" && option.apiConfig) {
|
||||
// 전용 API 호출 (복잡한 다중 조인)
|
||||
const { endpoint, method = "GET", parameterMappings, responseValueField } = option.apiConfig;
|
||||
|
||||
// 파라미터 빌드
|
||||
const params: Record<string, any> = {};
|
||||
for (const mapping of parameterMappings) {
|
||||
const value = rowData[mapping.sourceField];
|
||||
if (value !== undefined && value !== null) {
|
||||
params[mapping.paramName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 API 호출: ${method} ${endpoint}`, params);
|
||||
|
||||
let response;
|
||||
if (method === "POST") {
|
||||
response = await apiClient.post(endpoint, params);
|
||||
} else {
|
||||
response = await apiClient.get(endpoint, { params });
|
||||
}
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
// responseValueField로 값 추출 (중첩 경로 지원: "data.price")
|
||||
const keys = responseValueField.split(".");
|
||||
let value = response.data.data;
|
||||
for (const key of keys) {
|
||||
value = value?.[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 초기 데이터에 계산 필드 적용
|
||||
useEffect(() => {
|
||||
@@ -579,6 +769,8 @@ export function ModalRepeaterTableComponent({
|
||||
onDataChange={handleChange}
|
||||
onRowChange={handleRowChange}
|
||||
onRowDelete={handleRowDelete}
|
||||
activeDataSources={activeDataSources}
|
||||
onDataSourceChange={handleDataSourceChange}
|
||||
/>
|
||||
|
||||
{/* 항목 선택 모달 */}
|
||||
|
||||
Reference in New Issue
Block a user