feat(UniversalFormModal): 테이블 섹션 컬럼 조회(Lookup) 기능 구현
- LookupConfig, LookupOption, LookupCondition 타입 정의 - sourceType 4가지 유형 지원 (currentRow, sourceTable, sectionField, externalTable) - TableColumnSettingsModal에 "조회 설정" 탭 추가 - TableSectionSettingsModal에 간단 조회 설정 UI 추가 - fetchExternalValue, fetchExternalLookupValue 함수 구현 - 헤더 드롭다운에서 조회 옵션 선택 기능
This commit is contained in:
parent
fdb9ef9167
commit
c86140fad3
|
|
@ -31,7 +31,7 @@ interface TableSectionRendererProps {
|
|||
|
||||
/**
|
||||
* TableColumnConfig를 RepeaterColumnConfig로 변환
|
||||
* columnModes가 있으면 dynamicDataSource로 변환
|
||||
* columnModes 또는 lookup이 있으면 dynamicDataSource로 변환
|
||||
*/
|
||||
function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
||||
const baseColumn: RepeaterColumnConfig = {
|
||||
|
|
@ -47,8 +47,43 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
|||
// valueMapping은 별도로 처리
|
||||
};
|
||||
|
||||
// columnModes를 dynamicDataSource로 변환
|
||||
if (col.columnModes && col.columnModes.length > 0) {
|
||||
// lookup 설정을 dynamicDataSource로 변환 (새로운 조회 기능)
|
||||
if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) {
|
||||
baseColumn.dynamicDataSource = {
|
||||
enabled: true,
|
||||
options: col.lookup.options.map((option) => ({
|
||||
id: option.id,
|
||||
// displayLabel이 있으면 그것을 사용, 없으면 원래 label 사용
|
||||
label: option.displayLabel || option.label,
|
||||
sourceType: "table" as const,
|
||||
tableConfig: {
|
||||
tableName: option.tableName,
|
||||
valueColumn: option.valueColumn,
|
||||
joinConditions: option.conditions.map((cond) => ({
|
||||
sourceField: cond.sourceField,
|
||||
targetField: cond.targetColumn,
|
||||
// sourceType에 따른 데이터 출처 설정
|
||||
sourceType: cond.sourceType, // "currentRow" | "sectionField" | "externalTable"
|
||||
fromFormData: cond.sourceType === "sectionField",
|
||||
sectionId: cond.sectionId,
|
||||
// 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우)
|
||||
externalLookup: cond.externalLookup,
|
||||
// 값 변환 설정 전달 (레거시 호환)
|
||||
transform: cond.transform?.enabled ? {
|
||||
tableName: cond.transform.tableName,
|
||||
matchColumn: cond.transform.matchColumn,
|
||||
resultColumn: cond.transform.resultColumn,
|
||||
} : undefined,
|
||||
})),
|
||||
},
|
||||
// 조회 유형 정보 추가
|
||||
lookupType: option.type,
|
||||
})),
|
||||
defaultOptionId: col.lookup.options.find((o) => o.isDefault)?.id || col.lookup.options[0]?.id,
|
||||
};
|
||||
}
|
||||
// columnModes를 dynamicDataSource로 변환 (기존 로직 유지)
|
||||
else if (col.columnModes && col.columnModes.length > 0) {
|
||||
baseColumn.dynamicDataSource = {
|
||||
enabled: true,
|
||||
options: col.columnModes.map((mode) => ({
|
||||
|
|
@ -58,12 +93,10 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
|
|||
// 실제 조회 로직은 TableSectionRenderer에서 처리
|
||||
tableConfig: {
|
||||
tableName: mode.valueMapping?.externalRef?.tableName || "",
|
||||
valueField: mode.valueMapping?.externalRef?.valueColumn || "",
|
||||
valueColumn: mode.valueMapping?.externalRef?.valueColumn || "",
|
||||
joinConditions: (mode.valueMapping?.externalRef?.joinConditions || []).map((jc) => ({
|
||||
sourceTable: jc.sourceType === "row" ? "target" : "source",
|
||||
sourceField: jc.sourceField,
|
||||
targetField: jc.targetColumn,
|
||||
operator: jc.operator || "=",
|
||||
})),
|
||||
},
|
||||
})),
|
||||
|
|
@ -85,18 +118,110 @@ function convertToCalculationRule(calc: { resultField: string; formula: string;
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 값 변환 함수: 중간 테이블을 통해 값을 변환
|
||||
* 예: 거래처 이름 "(무)테스트업체" → 거래처 코드 "CUST-0002"
|
||||
*/
|
||||
async function transformValue(
|
||||
value: any,
|
||||
transform: { tableName: string; matchColumn: string; resultColumn: string }
|
||||
): Promise<any> {
|
||||
if (!value || !transform.tableName || !transform.matchColumn || !transform.resultColumn) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${transform.tableName}/data`,
|
||||
{ search: { [transform.matchColumn]: value }, size: 1, page: 1 }
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||
const transformedValue = response.data.data.data[0][transform.resultColumn];
|
||||
return transformedValue;
|
||||
}
|
||||
|
||||
console.warn(`변환 실패: ${transform.tableName}.${transform.matchColumn} = "${value}" 인 행을 찾을 수 없습니다.`);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error("값 변환 오류:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 테이블에서 조건 값을 조회하는 함수
|
||||
* LookupCondition.sourceType이 "externalTable"인 경우 사용
|
||||
*/
|
||||
async function fetchExternalLookupValue(
|
||||
externalLookup: {
|
||||
tableName: string;
|
||||
matchColumn: string;
|
||||
matchSourceType: "currentRow" | "sourceTable" | "sectionField";
|
||||
matchSourceField: string;
|
||||
matchSectionId?: string;
|
||||
resultColumn: string;
|
||||
},
|
||||
rowData: any,
|
||||
sourceData: any,
|
||||
formData: FormDataState
|
||||
): Promise<any> {
|
||||
// 1. 비교 값 가져오기
|
||||
let matchValue: any;
|
||||
if (externalLookup.matchSourceType === "currentRow") {
|
||||
matchValue = rowData[externalLookup.matchSourceField];
|
||||
} else if (externalLookup.matchSourceType === "sourceTable") {
|
||||
matchValue = sourceData?.[externalLookup.matchSourceField];
|
||||
} else {
|
||||
matchValue = formData[externalLookup.matchSourceField];
|
||||
}
|
||||
|
||||
if (matchValue === undefined || matchValue === null || matchValue === "") {
|
||||
console.warn(`외부 테이블 조회: 비교 값이 없습니다. (${externalLookup.matchSourceType}.${externalLookup.matchSourceField})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 2. 외부 테이블에서 값 조회
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${externalLookup.tableName}/data`,
|
||||
{ search: { [externalLookup.matchColumn]: matchValue }, size: 1, page: 1 }
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||
return response.data.data.data[0][externalLookup.resultColumn];
|
||||
}
|
||||
|
||||
console.warn(`외부 테이블 조회: ${externalLookup.tableName}.${externalLookup.matchColumn} = "${matchValue}" 인 행을 찾을 수 없습니다.`);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error("외부 테이블 조회 오류:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 테이블에서 값을 조회하는 함수
|
||||
*
|
||||
* @param tableName - 조회할 테이블명
|
||||
* @param valueColumn - 가져올 컬럼명
|
||||
* @param joinConditions - 조인 조건 목록
|
||||
* @param rowData - 현재 행 데이터 (설정된 컬럼 필드)
|
||||
* @param sourceData - 원본 소스 데이터 (_sourceData)
|
||||
* @param formData - 폼 데이터 (다른 섹션 필드)
|
||||
*/
|
||||
async function fetchExternalValue(
|
||||
tableName: string,
|
||||
valueColumn: string,
|
||||
joinConditions: TableJoinCondition[],
|
||||
rowData: any,
|
||||
sourceData: any,
|
||||
formData: FormDataState
|
||||
): Promise<any> {
|
||||
console.log("📡 [fetchExternalValue] 시작:", { tableName, valueColumn, joinConditions });
|
||||
|
||||
if (joinConditions.length === 0) {
|
||||
console.warn("조인 조건이 없습니다.");
|
||||
console.warn("📡 [fetchExternalValue] 조인 조건이 없습니다.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -106,20 +231,44 @@ async function fetchExternalValue(
|
|||
for (const condition of joinConditions) {
|
||||
let value: any;
|
||||
|
||||
// 값 출처에 따라 가져오기
|
||||
console.log("📡 [fetchExternalValue] 조건 처리:", { condition, rowData, sourceData, formData });
|
||||
|
||||
// 값 출처에 따라 가져오기 (4가지 소스 타입 지원)
|
||||
if (condition.sourceType === "row") {
|
||||
// 현재 행에서 가져오기
|
||||
// 현재 행 데이터 (설정된 컬럼 필드)
|
||||
value = rowData[condition.sourceField];
|
||||
console.log("📡 [fetchExternalValue] row에서 값 가져옴:", { field: condition.sourceField, value });
|
||||
} else if (condition.sourceType === "sourceData") {
|
||||
// 원본 소스 테이블 데이터 (_sourceData)
|
||||
value = sourceData?.[condition.sourceField];
|
||||
console.log("📡 [fetchExternalValue] sourceData에서 값 가져옴:", { field: condition.sourceField, value });
|
||||
} else if (condition.sourceType === "formData") {
|
||||
// formData에서 가져오기 (핵심 기능!)
|
||||
// formData에서 가져오기 (다른 섹션)
|
||||
value = formData[condition.sourceField];
|
||||
console.log("📡 [fetchExternalValue] formData에서 값 가져옴:", { field: condition.sourceField, value });
|
||||
} else if (condition.sourceType === "externalTable" && condition.externalLookup) {
|
||||
// 외부 테이블에서 조회하여 가져오기
|
||||
console.log("📡 [fetchExternalValue] externalTable 조회 시작:", condition.externalLookup);
|
||||
value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData);
|
||||
console.log("📡 [fetchExternalValue] externalTable 조회 결과:", { value });
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
console.warn(`조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`);
|
||||
if (value === undefined || value === null || value === "") {
|
||||
console.warn(`📡 [fetchExternalValue] 조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 값 변환이 필요한 경우 (예: 이름 → 코드) - 레거시 호환
|
||||
if (condition.transform) {
|
||||
console.log("📡 [fetchExternalValue] 값 변환 시작:", { originalValue: value, transform: condition.transform });
|
||||
value = await transformValue(value, condition.transform);
|
||||
console.log("📡 [fetchExternalValue] 값 변환 결과:", { transformedValue: value });
|
||||
if (value === undefined) {
|
||||
console.warn(`📡 [fetchExternalValue] 값 변환 후 결과가 없습니다. 원본 값: "${formData[condition.sourceField]}"`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 숫자형 ID 변환
|
||||
let convertedValue = value;
|
||||
if (condition.targetColumn.endsWith("_id") || condition.targetColumn === "id") {
|
||||
|
|
@ -129,22 +278,33 @@ async function fetchExternalValue(
|
|||
}
|
||||
}
|
||||
|
||||
whereConditions[condition.targetColumn] = convertedValue;
|
||||
// 정확히 일치하는 검색을 위해 operator: "equals" 사용
|
||||
whereConditions[condition.targetColumn] = {
|
||||
value: convertedValue,
|
||||
operator: "equals"
|
||||
};
|
||||
console.log("📡 [fetchExternalValue] WHERE 조건 추가:", { targetColumn: condition.targetColumn, value: convertedValue });
|
||||
}
|
||||
|
||||
// API 호출
|
||||
console.log("📡 [fetchExternalValue] API 호출:", { tableName, whereConditions });
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${tableName}/data`,
|
||||
{ search: whereConditions, size: 1, page: 1 }
|
||||
);
|
||||
|
||||
console.log("📡 [fetchExternalValue] API 응답:", response.data);
|
||||
|
||||
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||
return response.data.data.data[0][valueColumn];
|
||||
const result = response.data.data.data[0][valueColumn];
|
||||
console.log("📡 [fetchExternalValue] 최종 결과:", { valueColumn, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
console.warn("📡 [fetchExternalValue] 조회 결과 없음");
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error("외부 테이블 조회 오류:", error);
|
||||
console.error("📡 [fetchExternalValue] 외부 테이블 조회 오류:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -269,6 +429,77 @@ export function TableSectionRenderer({
|
|||
for (const col of tableConfig.columns) {
|
||||
const mapping = col.valueMapping;
|
||||
|
||||
// 0. lookup 설정이 있는 경우 (동적 조회)
|
||||
if (col.lookup?.enabled && col.lookup.options && col.lookup.options.length > 0) {
|
||||
// 현재 활성화된 옵션 또는 기본 옵션 사용
|
||||
const activeOptionId = activeDataSources[col.field];
|
||||
const defaultOption = col.lookup.options.find((o) => o.isDefault) || col.lookup.options[0];
|
||||
const selectedOption = activeOptionId
|
||||
? col.lookup.options.find((o) => o.id === activeOptionId) || defaultOption
|
||||
: defaultOption;
|
||||
|
||||
if (selectedOption) {
|
||||
// sameTable 타입: 소스 데이터에서 직접 값 복사
|
||||
if (selectedOption.type === "sameTable") {
|
||||
const value = sourceItem[selectedOption.valueColumn];
|
||||
if (value !== undefined) {
|
||||
newItem[col.field] = value;
|
||||
}
|
||||
// _sourceData에 원본 저장 (나중에 다른 옵션으로 전환 시 사용)
|
||||
newItem._sourceData = sourceItem;
|
||||
continue;
|
||||
}
|
||||
|
||||
// relatedTable, combinedLookup: 외부 테이블 조회
|
||||
// 조인 조건 구성 (4가지 소스 타입 지원)
|
||||
const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => {
|
||||
// sourceType 매핑
|
||||
let sourceType: "row" | "sourceData" | "formData" | "externalTable";
|
||||
if (cond.sourceType === "currentRow") {
|
||||
sourceType = "row";
|
||||
} else if (cond.sourceType === "sourceTable") {
|
||||
sourceType = "sourceData";
|
||||
} else if (cond.sourceType === "externalTable") {
|
||||
sourceType = "externalTable";
|
||||
} else {
|
||||
sourceType = "formData";
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType,
|
||||
sourceField: cond.sourceField,
|
||||
targetColumn: cond.targetColumn,
|
||||
// 외부 테이블 조회 설정
|
||||
externalLookup: cond.externalLookup,
|
||||
// 값 변환 설정 전달 (레거시 호환)
|
||||
transform: cond.transform?.enabled ? {
|
||||
tableName: cond.transform.tableName,
|
||||
matchColumn: cond.transform.matchColumn,
|
||||
resultColumn: cond.transform.resultColumn,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
// 외부 테이블에서 값 조회 (sourceItem이 _sourceData 역할)
|
||||
const value = await fetchExternalValue(
|
||||
selectedOption.tableName,
|
||||
selectedOption.valueColumn,
|
||||
joinConditions,
|
||||
{ ...sourceItem, ...newItem }, // rowData (현재 행)
|
||||
sourceItem, // sourceData (소스 테이블 원본)
|
||||
formData
|
||||
);
|
||||
|
||||
if (value !== undefined) {
|
||||
newItem[col.field] = value;
|
||||
}
|
||||
|
||||
// _sourceData에 원본 저장
|
||||
newItem._sourceData = sourceItem;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. 먼저 col.sourceField 확인 (간단 매핑)
|
||||
if (!mapping && col.sourceField) {
|
||||
// sourceField가 명시적으로 설정된 경우
|
||||
|
|
@ -316,7 +547,8 @@ export function TableSectionRenderer({
|
|||
tableName,
|
||||
valueColumn,
|
||||
joinConditions,
|
||||
{ ...sourceItem, ...newItem }, // 현재까지 빌드된 아이템
|
||||
{ ...sourceItem, ...newItem }, // rowData
|
||||
sourceItem, // sourceData
|
||||
formData
|
||||
);
|
||||
if (value !== undefined) {
|
||||
|
|
@ -343,12 +575,14 @@ export function TableSectionRenderer({
|
|||
const newData = [...tableData, ...calculatedItems];
|
||||
handleDataChange(newData);
|
||||
},
|
||||
[tableConfig.columns, formData, tableData, calculateAll, handleDataChange]
|
||||
[tableConfig.columns, formData, tableData, calculateAll, handleDataChange, activeDataSources]
|
||||
);
|
||||
|
||||
// 컬럼 모드 변경 핸들러
|
||||
// 컬럼 모드/조회 옵션 변경 핸들러
|
||||
const handleDataSourceChange = useCallback(
|
||||
async (columnField: string, optionId: string) => {
|
||||
console.log("🔍 [handleDataSourceChange] 시작:", { columnField, optionId });
|
||||
|
||||
setActiveDataSources((prev) => ({
|
||||
...prev,
|
||||
[columnField]: optionId,
|
||||
|
|
@ -356,6 +590,109 @@ export function TableSectionRenderer({
|
|||
|
||||
// 해당 컬럼의 모든 행 데이터 재조회
|
||||
const column = tableConfig.columns.find((col) => col.field === columnField);
|
||||
console.log("🔍 [handleDataSourceChange] 컬럼 찾기:", { column: column?.field, hasLookup: column?.lookup?.enabled });
|
||||
|
||||
// lookup 설정이 있는 경우 (새로운 조회 기능)
|
||||
if (column?.lookup?.enabled && column.lookup.options) {
|
||||
const selectedOption = column.lookup.options.find((opt) => opt.id === optionId);
|
||||
console.log("🔍 [handleDataSourceChange] 선택된 옵션:", { selectedOption, optionId });
|
||||
if (!selectedOption) return;
|
||||
|
||||
// sameTable 타입: 현재 행의 소스 데이터에서 값 복사 (외부 조회 필요 없음)
|
||||
if (selectedOption.type === "sameTable") {
|
||||
console.log("🔍 [handleDataSourceChange] sameTable 타입 - 소스 데이터에서 복사");
|
||||
const updatedData = tableData.map((row) => {
|
||||
// sourceField에서 값을 가져와 해당 컬럼에 복사
|
||||
// row에 _sourceData가 있으면 거기서, 없으면 row 자체에서 가져옴
|
||||
const sourceData = row._sourceData || row;
|
||||
const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField];
|
||||
console.log("🔍 [handleDataSourceChange] sameTable 값 복사:", { valueColumn: selectedOption.valueColumn, sourceData, newValue });
|
||||
return { ...row, [columnField]: newValue };
|
||||
});
|
||||
|
||||
const calculatedData = calculateAll(updatedData);
|
||||
handleDataChange(calculatedData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 행에 대해 새 값 조회
|
||||
console.log("🔍 [handleDataSourceChange] 외부 테이블 조회 시작:", {
|
||||
type: selectedOption.type,
|
||||
tableName: selectedOption.tableName,
|
||||
valueColumn: selectedOption.valueColumn,
|
||||
conditions: selectedOption.conditions,
|
||||
tableDataLength: tableData.length,
|
||||
});
|
||||
|
||||
const updatedData = await Promise.all(
|
||||
tableData.map(async (row, rowIndex) => {
|
||||
let newValue: any = row[columnField];
|
||||
|
||||
// 조인 조건 구성 (4가지 소스 타입 지원)
|
||||
const joinConditions: TableJoinCondition[] = selectedOption.conditions.map((cond) => {
|
||||
// sourceType 매핑
|
||||
let sourceType: "row" | "sourceData" | "formData" | "externalTable";
|
||||
if (cond.sourceType === "currentRow") {
|
||||
sourceType = "row";
|
||||
} else if (cond.sourceType === "sourceTable") {
|
||||
sourceType = "sourceData";
|
||||
} else if (cond.sourceType === "externalTable") {
|
||||
sourceType = "externalTable";
|
||||
} else {
|
||||
sourceType = "formData";
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType,
|
||||
sourceField: cond.sourceField,
|
||||
targetColumn: cond.targetColumn,
|
||||
// 외부 테이블 조회 설정
|
||||
externalLookup: cond.externalLookup,
|
||||
// 값 변환 설정 전달 (레거시 호환)
|
||||
transform: cond.transform?.enabled ? {
|
||||
tableName: cond.transform.tableName,
|
||||
matchColumn: cond.transform.matchColumn,
|
||||
resultColumn: cond.transform.resultColumn,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회:`, {
|
||||
rowData: row,
|
||||
sourceData: row._sourceData,
|
||||
formData,
|
||||
joinConditions,
|
||||
});
|
||||
|
||||
// 외부 테이블에서 값 조회 (_sourceData 전달)
|
||||
const sourceData = row._sourceData || row;
|
||||
const value = await fetchExternalValue(
|
||||
selectedOption.tableName,
|
||||
selectedOption.valueColumn,
|
||||
joinConditions,
|
||||
row,
|
||||
sourceData,
|
||||
formData
|
||||
);
|
||||
|
||||
console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회 결과:`, { value });
|
||||
|
||||
if (value !== undefined) {
|
||||
newValue = value;
|
||||
}
|
||||
|
||||
return { ...row, [columnField]: newValue };
|
||||
})
|
||||
);
|
||||
|
||||
// 계산 필드 업데이트
|
||||
const calculatedData = calculateAll(updatedData);
|
||||
handleDataChange(calculatedData);
|
||||
console.log("🔍 [handleDataSourceChange] 완료:", { calculatedData });
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 columnModes 처리 (레거시 호환)
|
||||
if (!column?.columnModes) return;
|
||||
|
||||
const selectedMode = column.columnModes.find((mode) => mode.id === optionId);
|
||||
|
|
@ -366,10 +703,11 @@ export function TableSectionRenderer({
|
|||
tableData.map(async (row) => {
|
||||
const mapping = selectedMode.valueMapping;
|
||||
let newValue: any = row[columnField];
|
||||
const sourceData = row._sourceData || row;
|
||||
|
||||
if (mapping.type === "external" && mapping.externalRef) {
|
||||
const { tableName, valueColumn, joinConditions } = mapping.externalRef;
|
||||
const value = await fetchExternalValue(tableName, valueColumn, joinConditions, row, formData);
|
||||
const value = await fetchExternalValue(tableName, valueColumn, joinConditions, row, sourceData, formData);
|
||||
if (value !== undefined) {
|
||||
newValue = value;
|
||||
}
|
||||
|
|
@ -417,10 +755,10 @@ export function TableSectionRenderer({
|
|||
return filters.modalFilters.map((filter) => ({
|
||||
column: filter.column,
|
||||
label: filter.label || filter.column,
|
||||
type: filter.type,
|
||||
// category 타입을 select로 변환 (ModalFilterConfig 호환)
|
||||
type: filter.type === "category" ? "select" as const : filter.type as "text" | "select",
|
||||
options: filter.options,
|
||||
categoryRef: filter.categoryRef,
|
||||
booleanRef: filter.booleanRef,
|
||||
defaultValue: filter.defaultValue,
|
||||
}));
|
||||
}, [filters?.modalFilters]);
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
])
|
||||
)}
|
||||
onLoadTableColumns={loadTableColumns}
|
||||
allSections={config.sections as FormSectionConfig[]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ import {
|
|||
ValueMappingConfig,
|
||||
ColumnModeConfig,
|
||||
TableJoinCondition,
|
||||
LookupConfig,
|
||||
LookupOption,
|
||||
LookupCondition,
|
||||
VALUE_MAPPING_TYPE_OPTIONS,
|
||||
JOIN_SOURCE_TYPE_OPTIONS,
|
||||
TABLE_COLUMN_TYPE_OPTIONS,
|
||||
LOOKUP_TYPE_OPTIONS,
|
||||
LOOKUP_CONDITION_SOURCE_OPTIONS,
|
||||
} from "../types";
|
||||
|
||||
import {
|
||||
|
|
@ -42,8 +47,10 @@ interface TableColumnSettingsModalProps {
|
|||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
column: TableColumnConfig;
|
||||
sourceTableName: string; // 소스 테이블명
|
||||
sourceTableColumns: { column_name: string; data_type: string; comment?: string }[];
|
||||
formFields: { columnName: string; label: string }[]; // formData 필드 목록
|
||||
formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함)
|
||||
sections: { id: string; title: string }[]; // 섹션 목록
|
||||
onSave: (updatedColumn: TableColumnConfig) => void;
|
||||
tables: { table_name: string; comment?: string }[];
|
||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>;
|
||||
|
|
@ -54,8 +61,10 @@ export function TableColumnSettingsModal({
|
|||
open,
|
||||
onOpenChange,
|
||||
column,
|
||||
sourceTableName,
|
||||
sourceTableColumns,
|
||||
formFields,
|
||||
sections,
|
||||
onSave,
|
||||
tables,
|
||||
tableColumns,
|
||||
|
|
@ -67,6 +76,9 @@ export function TableColumnSettingsModal({
|
|||
// 외부 테이블 검색 상태
|
||||
const [externalTableOpen, setExternalTableOpen] = useState(false);
|
||||
|
||||
// 조회 테이블 검색 상태 (옵션별)
|
||||
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 활성 탭
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
|
|
@ -174,6 +186,101 @@ export function TableColumnSettingsModal({
|
|||
});
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 조회(Lookup) 관련 함수들
|
||||
// ============================================
|
||||
|
||||
// 조회 설정 업데이트
|
||||
const updateLookup = (updates: Partial<LookupConfig>) => {
|
||||
const current = localColumn.lookup || { enabled: false, options: [] };
|
||||
updateColumn({
|
||||
lookup: { ...current, ...updates },
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 옵션 추가
|
||||
const addLookupOption = () => {
|
||||
const newOption: LookupOption = {
|
||||
id: `lookup_${Date.now()}`,
|
||||
label: `조회 옵션 ${(localColumn.lookup?.options || []).length + 1}`,
|
||||
type: "sameTable",
|
||||
tableName: sourceTableName, // 기본값: 소스 테이블
|
||||
valueColumn: "",
|
||||
conditions: [],
|
||||
isDefault: (localColumn.lookup?.options || []).length === 0, // 첫 번째 옵션은 기본값
|
||||
};
|
||||
updateLookup({
|
||||
options: [...(localColumn.lookup?.options || []), newOption],
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 옵션 삭제
|
||||
const removeLookupOption = (index: number) => {
|
||||
const newOptions = (localColumn.lookup?.options || []).filter((_, i) => i !== index);
|
||||
// 삭제 후 기본 옵션이 없으면 첫 번째를 기본으로
|
||||
if (newOptions.length > 0 && !newOptions.some(opt => opt.isDefault)) {
|
||||
newOptions[0].isDefault = true;
|
||||
}
|
||||
updateLookup({ options: newOptions });
|
||||
};
|
||||
|
||||
// 조회 옵션 업데이트
|
||||
const updateLookupOption = (index: number, updates: Partial<LookupOption>) => {
|
||||
updateLookup({
|
||||
options: (localColumn.lookup?.options || []).map((opt, i) =>
|
||||
i === index ? { ...opt, ...updates } : opt
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 추가
|
||||
const addLookupCondition = (optionIndex: number) => {
|
||||
const option = localColumn.lookup?.options?.[optionIndex];
|
||||
if (!option) return;
|
||||
|
||||
const newCondition: LookupCondition = {
|
||||
sourceType: "currentRow",
|
||||
sourceField: "",
|
||||
targetColumn: "",
|
||||
};
|
||||
updateLookupOption(optionIndex, {
|
||||
conditions: [...(option.conditions || []), newCondition],
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 삭제
|
||||
const removeLookupCondition = (optionIndex: number, conditionIndex: number) => {
|
||||
const option = localColumn.lookup?.options?.[optionIndex];
|
||||
if (!option) return;
|
||||
|
||||
updateLookupOption(optionIndex, {
|
||||
conditions: option.conditions.filter((_, i) => i !== conditionIndex),
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 업데이트
|
||||
const updateLookupCondition = (optionIndex: number, conditionIndex: number, updates: Partial<LookupCondition>) => {
|
||||
const option = localColumn.lookup?.options?.[optionIndex];
|
||||
if (!option) return;
|
||||
|
||||
updateLookupOption(optionIndex, {
|
||||
conditions: option.conditions.map((c, i) =>
|
||||
i === conditionIndex ? { ...c, ...updates } : c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 옵션의 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
if (localColumn.lookup?.enabled) {
|
||||
localColumn.lookup.options?.forEach(option => {
|
||||
if (option.tableName) {
|
||||
onLoadTableColumns(option.tableName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [localColumn.lookup?.enabled, localColumn.lookup?.options, onLoadTableColumns]);
|
||||
|
||||
// 저장 함수
|
||||
const handleSave = () => {
|
||||
onSave(localColumn);
|
||||
|
|
@ -432,8 +539,9 @@ export function TableColumnSettingsModal({
|
|||
<ScrollArea className="h-[calc(90vh-200px)]">
|
||||
<div className="space-y-4 p-1">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="w-full grid grid-cols-3">
|
||||
<TabsList className="w-full grid grid-cols-4">
|
||||
<TabsTrigger value="basic" className="text-xs">기본 설정</TabsTrigger>
|
||||
<TabsTrigger value="lookup" className="text-xs">조회 설정</TabsTrigger>
|
||||
<TabsTrigger value="mapping" className="text-xs">값 매핑</TabsTrigger>
|
||||
<TabsTrigger value="modes" className="text-xs">컬럼 모드</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
@ -595,6 +703,376 @@ export function TableColumnSettingsModal({
|
|||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 조회 설정 탭 */}
|
||||
<TabsContent value="lookup" className="mt-4 space-y-4">
|
||||
{/* 조회 여부 토글 */}
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg bg-muted/20">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">조회 여부</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
다른 테이블에서 값을 조회하여 가져옵니다.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localColumn.lookup?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
updateLookup({ enabled: true, options: [] });
|
||||
} else {
|
||||
updateColumn({ lookup: undefined });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 조회 설정 (활성화 시) */}
|
||||
{localColumn.lookup?.enabled && (
|
||||
<div className="space-y-4">
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">조회 옵션</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
헤더에서 선택 가능한 조회 방식을 정의합니다.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={addLookupOption} className="h-8 text-xs">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
옵션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(localColumn.lookup?.options || []).length === 0 ? (
|
||||
<div className="text-center py-8 border border-dashed rounded-lg bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">조회 옵션이 없습니다</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
"옵션 추가" 버튼을 클릭하여 조회 방식을 추가하세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{(localColumn.lookup?.options || []).map((option, optIndex) => (
|
||||
<div key={option.id} className="border rounded-lg p-4 space-y-4 bg-card">
|
||||
{/* 옵션 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{option.label || `옵션 ${optIndex + 1}`}</span>
|
||||
{option.isDefault && (
|
||||
<Badge variant="secondary" className="text-xs">기본</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeLookupOption(optIndex)}
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 기본 설정 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">옵션명</Label>
|
||||
<Input
|
||||
value={option.label}
|
||||
onChange={(e) => updateLookupOption(optIndex, { label: e.target.value })}
|
||||
placeholder="예: 기준단가"
|
||||
className="h-8 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">조회 유형</Label>
|
||||
<Select
|
||||
value={option.type}
|
||||
onValueChange={(value: "sameTable" | "relatedTable" | "combinedLookup") => {
|
||||
// 유형 변경 시 테이블 초기화
|
||||
const newTableName = value === "sameTable" ? sourceTableName : "";
|
||||
updateLookupOption(optIndex, {
|
||||
type: value,
|
||||
tableName: newTableName,
|
||||
conditions: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOOKUP_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조회 테이블 선택 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">조회 테이블</Label>
|
||||
{option.type === "sameTable" ? (
|
||||
<Input
|
||||
value={sourceTableName}
|
||||
disabled
|
||||
className="h-8 text-xs mt-1 bg-muted"
|
||||
/>
|
||||
) : (
|
||||
<Popover
|
||||
open={lookupTableOpenMap[option.id]}
|
||||
onOpenChange={(open) => setLookupTableOpenMap(prev => ({ ...prev, [option.id]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-8 w-full justify-between text-xs mt-1"
|
||||
>
|
||||
{option.tableName || "테이블 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-full min-w-[250px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="text-xs py-4 text-center">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.table_name}
|
||||
value={table.table_name}
|
||||
onSelect={() => {
|
||||
updateLookupOption(optIndex, { tableName: table.table_name });
|
||||
onLoadTableColumns(table.table_name);
|
||||
setLookupTableOpenMap(prev => ({ ...prev, [option.id]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3.5 w-3.5",
|
||||
option.tableName === table.table_name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{table.table_name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">가져올 컬럼</Label>
|
||||
<Select
|
||||
value={option.valueColumn}
|
||||
onValueChange={(value) => updateLookupOption(optIndex, { valueColumn: value })}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs mt-1">
|
||||
<SelectValue placeholder="컬럼 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[option.tableName] || []).map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
{col.comment && ` (${col.comment})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기본 옵션 체크박스 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={option.isDefault ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
// 기본 옵션은 하나만
|
||||
updateLookup({
|
||||
options: (localColumn.lookup?.options || []).map((opt, i) => ({
|
||||
...opt,
|
||||
isDefault: i === optIndex,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
updateLookupOption(optIndex, { isDefault: false });
|
||||
}
|
||||
}}
|
||||
className="scale-75"
|
||||
/>
|
||||
<span className="text-xs">기본 옵션으로 설정</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 조회 조건 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-xs font-medium">조회 조건</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => addLookupCondition(optIndex)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(option.conditions || []).length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground text-center py-2 border border-dashed rounded">
|
||||
조회 조건을 추가하세요.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{option.conditions.map((condition, condIndex) => (
|
||||
<div key={condIndex} className="flex items-center gap-2 p-2 border rounded-lg bg-muted/30">
|
||||
{/* 소스 타입 */}
|
||||
<Select
|
||||
value={condition.sourceType}
|
||||
onValueChange={(value: "currentRow" | "sectionField") =>
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
sourceType: value,
|
||||
sourceField: "",
|
||||
sectionId: value === "sectionField" ? "" : undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-[110px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOOKUP_CONDITION_SOURCE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 섹션 선택 (sectionField일 때) */}
|
||||
{condition.sourceType === "sectionField" && (
|
||||
<Select
|
||||
value={condition.sectionId || ""}
|
||||
onValueChange={(value) =>
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
sectionId: value,
|
||||
sourceField: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-[100px]">
|
||||
<SelectValue placeholder="섹션" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sections.map((section) => (
|
||||
<SelectItem key={section.id} value={section.id}>
|
||||
{section.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* 소스 필드 */}
|
||||
<Select
|
||||
value={condition.sourceField}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { sourceField: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-[110px]">
|
||||
<SelectValue placeholder="필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{condition.sourceType === "currentRow"
|
||||
? sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
</SelectItem>
|
||||
))
|
||||
: formFields
|
||||
.filter(f => !condition.sectionId || f.sectionId === condition.sectionId)
|
||||
.map((field) => (
|
||||
<SelectItem key={field.columnName} value={field.columnName}>
|
||||
{field.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<span className="text-xs text-muted-foreground">=</span>
|
||||
|
||||
{/* 타겟 컬럼 */}
|
||||
<Select
|
||||
value={condition.targetColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue placeholder="대상 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[option.tableName] || []).map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeLookupCondition(optIndex, condIndex)}
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조회 유형별 설명 */}
|
||||
<div className="text-xs text-muted-foreground p-2 bg-muted/30 rounded">
|
||||
{option.type === "sameTable" && (
|
||||
<>
|
||||
<strong>동일 테이블 조회:</strong> 검색 모달에서 선택한 행의 다른 컬럼 값을 가져옵니다.
|
||||
<br />예: 품목 선택 시 → 품목 테이블의 기준단가
|
||||
</>
|
||||
)}
|
||||
{option.type === "relatedTable" && (
|
||||
<>
|
||||
<strong>연관 테이블 조회:</strong> 현재 행 데이터를 기준으로 다른 테이블에서 값을 조회합니다.
|
||||
<br />예: 품목코드로 → 품목별단가 테이블에서 단가 조회
|
||||
</>
|
||||
)}
|
||||
{option.type === "combinedLookup" && (
|
||||
<>
|
||||
<strong>복합 조건 조회:</strong> 다른 섹션 필드와 현재 행을 조합하여 조회합니다.
|
||||
<br />예: 거래처(섹션1) + 품목(현재행) → 거래처별단가 테이블
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 값 매핑 탭 */}
|
||||
<TabsContent value="mapping" className="mt-4 space-y-4">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon } from "lucide-react";
|
||||
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Check, ChevronsUpDown, Filter, Table as TableIcon, Search } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 타입 import
|
||||
|
|
@ -24,9 +24,13 @@ import {
|
|||
TablePreFilter,
|
||||
TableModalFilter,
|
||||
TableCalculationRule,
|
||||
LookupOption,
|
||||
ExternalTableLookup,
|
||||
TABLE_COLUMN_TYPE_OPTIONS,
|
||||
FILTER_OPERATOR_OPTIONS,
|
||||
MODAL_FILTER_TYPE_OPTIONS,
|
||||
LOOKUP_TYPE_OPTIONS,
|
||||
LOOKUP_CONDITION_SOURCE_OPTIONS,
|
||||
} from "../types";
|
||||
|
||||
import {
|
||||
|
|
@ -52,6 +56,13 @@ interface ColumnSettingItemProps {
|
|||
saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[];
|
||||
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
||||
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼
|
||||
sourceTableName: string; // 소스 테이블명
|
||||
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
|
||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>; // 테이블별 컬럼
|
||||
sections: { id: string; title: string }[]; // 섹션 목록
|
||||
formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록
|
||||
tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용
|
||||
onLoadTableColumns: (tableName: string) => void;
|
||||
onUpdate: (updates: Partial<TableColumnConfig>) => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
|
|
@ -65,6 +76,13 @@ function ColumnSettingItem({
|
|||
saveTableColumns,
|
||||
displayColumns,
|
||||
sourceTableColumns,
|
||||
sourceTableName,
|
||||
tables,
|
||||
tableColumns,
|
||||
sections,
|
||||
formFields,
|
||||
tableConfig,
|
||||
onLoadTableColumns,
|
||||
onUpdate,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
|
|
@ -72,6 +90,86 @@ function ColumnSettingItem({
|
|||
}: ColumnSettingItemProps) {
|
||||
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
||||
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
||||
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 조회 옵션 추가
|
||||
const addLookupOption = () => {
|
||||
const newOption: LookupOption = {
|
||||
id: `lookup_${Date.now()}`,
|
||||
label: `조회 옵션 ${(col.lookup?.options || []).length + 1}`,
|
||||
type: "sameTable",
|
||||
tableName: sourceTableName,
|
||||
valueColumn: "",
|
||||
conditions: [],
|
||||
isDefault: (col.lookup?.options || []).length === 0,
|
||||
};
|
||||
onUpdate({
|
||||
lookup: {
|
||||
enabled: true,
|
||||
options: [...(col.lookup?.options || []), newOption],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 옵션 삭제
|
||||
const removeLookupOption = (optIndex: number) => {
|
||||
const newOptions = (col.lookup?.options || []).filter((_, i) => i !== optIndex);
|
||||
if (newOptions.length > 0 && !newOptions.some((opt) => opt.isDefault)) {
|
||||
newOptions[0].isDefault = true;
|
||||
}
|
||||
onUpdate({
|
||||
lookup: {
|
||||
enabled: col.lookup?.enabled ?? false,
|
||||
options: newOptions,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 옵션 업데이트
|
||||
const updateLookupOption = (optIndex: number, updates: Partial<LookupOption>) => {
|
||||
onUpdate({
|
||||
lookup: {
|
||||
enabled: col.lookup?.enabled ?? false,
|
||||
options: (col.lookup?.options || []).map((opt, i) =>
|
||||
i === optIndex ? { ...opt, ...updates } : opt
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 추가
|
||||
const addLookupCondition = (optIndex: number) => {
|
||||
const option = col.lookup?.options?.[optIndex];
|
||||
if (!option) return;
|
||||
const newCondition: LookupCondition = {
|
||||
sourceType: "currentRow",
|
||||
sourceField: "",
|
||||
targetColumn: "",
|
||||
};
|
||||
updateLookupOption(optIndex, {
|
||||
conditions: [...(option.conditions || []), newCondition],
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 삭제
|
||||
const removeLookupCondition = (optIndex: number, condIndex: number) => {
|
||||
const option = col.lookup?.options?.[optIndex];
|
||||
if (!option) return;
|
||||
updateLookupOption(optIndex, {
|
||||
conditions: option.conditions.filter((_, i) => i !== condIndex),
|
||||
});
|
||||
};
|
||||
|
||||
// 조회 조건 업데이트
|
||||
const updateLookupCondition = (optIndex: number, condIndex: number, updates: Partial<LookupCondition>) => {
|
||||
const option = col.lookup?.options?.[optIndex];
|
||||
if (!option) return;
|
||||
updateLookupOption(optIndex, {
|
||||
conditions: option.conditions.map((c, i) =>
|
||||
i === condIndex ? { ...c, ...updates } : c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-3 space-y-3 bg-card">
|
||||
|
|
@ -304,7 +402,695 @@ function ColumnSettingItem({
|
|||
/>
|
||||
<span>필수</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<Switch
|
||||
checked={col.lookup?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onUpdate({ lookup: { enabled: true, options: [] } });
|
||||
} else {
|
||||
onUpdate({ lookup: undefined });
|
||||
}
|
||||
}}
|
||||
className="scale-75"
|
||||
/>
|
||||
<span className="flex items-center gap-1">
|
||||
<Search className="h-3 w-3" />
|
||||
조회
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 조회 설정 (조회 ON일 때만 표시) */}
|
||||
{col.lookup?.enabled && (
|
||||
<div className="border-t pt-3 mt-3 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-xs font-medium text-blue-600">조회 옵션</Label>
|
||||
<Button size="sm" variant="outline" onClick={addLookupOption} className="h-6 text-xs px-2">
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
옵션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(col.lookup?.options || []).length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground text-center py-2 border border-dashed rounded bg-muted/20">
|
||||
"옵션 추가" 버튼을 클릭하여 조회 방식을 추가하세요.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(col.lookup?.options || []).map((option, optIndex) => (
|
||||
<div key={option.id} className="border rounded-lg p-3 space-y-3 bg-blue-50/30">
|
||||
{/* 옵션 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">{option.label || `옵션 ${optIndex + 1}`}</span>
|
||||
{option.isDefault && (
|
||||
<Badge variant="secondary" className="text-[10px] h-4">기본</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeLookupOption(optIndex)}
|
||||
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 기본 설정 - 첫 번째 줄: 옵션명, 표시 라벨 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">옵션명</Label>
|
||||
<Input
|
||||
value={option.label}
|
||||
onChange={(e) => updateLookupOption(optIndex, { label: e.target.value })}
|
||||
placeholder="예: 기준단가"
|
||||
className="h-7 text-xs mt-0.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">표시 라벨 (헤더 드롭다운)</Label>
|
||||
<Input
|
||||
value={option.displayLabel || ""}
|
||||
onChange={(e) => updateLookupOption(optIndex, { displayLabel: e.target.value })}
|
||||
placeholder={`예: 단가 (${option.label || "옵션명"})`}
|
||||
className="h-7 text-xs mt-0.5"
|
||||
/>
|
||||
<p className="text-[9px] text-muted-foreground mt-0.5">
|
||||
비워두면 옵션명만 표시
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기본 설정 - 두 번째 줄: 조회 유형, 테이블, 가져올 컬럼 */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">조회 유형</Label>
|
||||
<Select
|
||||
value={option.type}
|
||||
onValueChange={(value: "sameTable" | "relatedTable" | "combinedLookup") => {
|
||||
const newTableName = value === "sameTable" ? sourceTableName : "";
|
||||
updateLookupOption(optIndex, { type: value, tableName: newTableName, conditions: [] });
|
||||
if (newTableName) onLoadTableColumns(newTableName);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-0.5">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOOKUP_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">조회 테이블</Label>
|
||||
{option.type === "sameTable" ? (
|
||||
<Input value={sourceTableName} disabled className="h-7 text-xs mt-0.5 bg-muted" />
|
||||
) : (
|
||||
<Popover
|
||||
open={lookupTableOpenMap[option.id]}
|
||||
onOpenChange={(open) => setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="h-7 w-full justify-between text-xs mt-0.5">
|
||||
{option.tableName || "선택..."}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[200px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[150px]">
|
||||
<CommandEmpty className="text-xs py-2 text-center">없음</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.table_name}
|
||||
value={table.table_name}
|
||||
onSelect={() => {
|
||||
updateLookupOption(optIndex, { tableName: table.table_name });
|
||||
onLoadTableColumns(table.table_name);
|
||||
setLookupTableOpenMap((prev) => ({ ...prev, [option.id]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", option.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
|
||||
{table.table_name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">가져올 컬럼</Label>
|
||||
<Select
|
||||
value={option.valueColumn}
|
||||
onValueChange={(value) => updateLookupOption(optIndex, { valueColumn: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-0.5">
|
||||
<SelectValue placeholder="선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[option.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
|
||||
{c.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기본 옵션 & 조회 조건 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Switch
|
||||
checked={option.isDefault ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onUpdate({
|
||||
lookup: {
|
||||
enabled: true,
|
||||
options: (col.lookup?.options || []).map((o, i) => ({ ...o, isDefault: i === optIndex })),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateLookupOption(optIndex, { isDefault: false });
|
||||
}
|
||||
}}
|
||||
className="scale-[0.6]"
|
||||
/>
|
||||
<span>기본 옵션</span>
|
||||
</label>
|
||||
<Button size="sm" variant="ghost" onClick={() => addLookupCondition(optIndex)} className="h-6 text-xs px-2">
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
조건 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 조회 조건 목록 */}
|
||||
{(option.conditions || []).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{option.conditions.map((cond, condIndex) => (
|
||||
<div key={condIndex} className="border rounded bg-white p-2 space-y-2">
|
||||
{/* 기본 조건 행 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Select
|
||||
value={cond.sourceType}
|
||||
onValueChange={(value: "currentRow" | "sourceTable" | "sectionField" | "externalTable") =>
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
sourceType: value,
|
||||
sourceField: "",
|
||||
sectionId: undefined,
|
||||
transform: undefined,
|
||||
externalLookup: value === "externalTable" ? {
|
||||
tableName: "",
|
||||
matchColumn: "",
|
||||
matchSourceType: "sourceTable",
|
||||
matchSourceField: "",
|
||||
resultColumn: "",
|
||||
} : undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] w-[90px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOOKUP_CONDITION_SOURCE_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value} className="text-xs">{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 다른 섹션 선택 시 - 섹션 드롭다운 */}
|
||||
{cond.sourceType === "sectionField" && (
|
||||
<Select
|
||||
value={cond.sectionId || ""}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { sectionId: value, sourceField: "" })}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] w-[70px]">
|
||||
<SelectValue placeholder="섹션" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sections.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id} className="text-xs">{s.title}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* 현재 행 / 소스 테이블 / 다른 섹션 - 필드 선택 */}
|
||||
{cond.sourceType !== "externalTable" && (
|
||||
<div className="space-y-0.5">
|
||||
<Select
|
||||
value={cond.sourceField}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { sourceField: value })}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] w-[110px]">
|
||||
<SelectValue placeholder="필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cond.sourceType === "currentRow" ? (
|
||||
// 현재 행: 테이블에 설정된 컬럼 필드 표시
|
||||
<>
|
||||
<div className="px-2 py-1 text-[10px] font-medium text-green-600 bg-green-50 border-b">
|
||||
설정된 컬럼
|
||||
</div>
|
||||
{tableConfig?.columns?.map((c) => (
|
||||
<SelectItem key={c.field} value={c.field} className="text-xs">
|
||||
{c.label} ({c.field})
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
) : cond.sourceType === "sourceTable" ? (
|
||||
// 소스 테이블: 원본 테이블의 컬럼 표시
|
||||
<>
|
||||
<div className="px-2 py-1 text-[10px] font-medium text-orange-600 bg-orange-50 border-b">
|
||||
{sourceTableName} 컬럼
|
||||
</div>
|
||||
{sourceTableColumns.map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
|
||||
{c.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
// 다른 섹션: 폼 필드 표시
|
||||
<>
|
||||
{cond.sectionId && (
|
||||
<div className="px-2 py-1 text-[10px] font-medium text-purple-600 bg-purple-50 border-b">
|
||||
{sections.find(s => s.id === cond.sectionId)?.title || cond.sectionId} 필드
|
||||
</div>
|
||||
)}
|
||||
{formFields
|
||||
.filter((f) => !cond.sectionId || f.sectionId === cond.sectionId)
|
||||
.map((f) => (
|
||||
<SelectItem key={f.columnName} value={f.columnName} className="text-xs">{f.label}</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{cond.sourceField && (
|
||||
<p className="text-[9px] text-muted-foreground truncate">
|
||||
{cond.sourceType === "currentRow"
|
||||
? `rowData.${cond.sourceField}`
|
||||
: cond.sourceType === "sourceTable"
|
||||
? `${sourceTableName}.${cond.sourceField}`
|
||||
: `formData.${cond.sourceField}`
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 현재 행 / 소스 테이블 / 다른 섹션일 때 = 기호와 조회 컬럼 */}
|
||||
{cond.sourceType !== "externalTable" && (
|
||||
<>
|
||||
<span className="text-[10px] text-muted-foreground">=</span>
|
||||
|
||||
<div className="flex-1 space-y-0.5">
|
||||
<Select
|
||||
value={cond.targetColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue placeholder="조회 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{option.tableName && (
|
||||
<div className="px-2 py-1 text-[10px] font-medium text-blue-600 bg-blue-50 border-b">
|
||||
{option.tableName} 컬럼
|
||||
</div>
|
||||
)}
|
||||
{(tableColumns[option.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{cond.targetColumn && option.tableName && (
|
||||
<p className="text-[9px] text-muted-foreground truncate">
|
||||
{option.tableName}.{cond.targetColumn}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeLookupCondition(optIndex, condIndex)}
|
||||
className="h-6 w-6 p-0 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 외부 테이블 조회 설정 */}
|
||||
{cond.sourceType === "externalTable" && cond.externalLookup && (
|
||||
<div className="pl-2 border-l-2 border-orange-200 space-y-2">
|
||||
<p className="text-[10px] text-orange-600 font-medium">외부 테이블에서 조건 값 조회</p>
|
||||
|
||||
{/* 1행: 조회 테이블 선택 */}
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div>
|
||||
<p className="text-[9px] text-muted-foreground mb-0.5">조회 테이블</p>
|
||||
<Popover
|
||||
open={lookupTableOpenMap[`ext_${optIndex}_${condIndex}`] || false}
|
||||
onOpenChange={(open) => setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" className="h-6 w-full justify-between text-[10px]">
|
||||
{cond.externalLookup.tableName || "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[180px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs py-2 text-center">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.table_name}
|
||||
value={table.table_name}
|
||||
onSelect={() => {
|
||||
onLoadTableColumns(table.table_name);
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, tableName: table.table_name, matchColumn: "", resultColumn: "" }
|
||||
});
|
||||
setLookupTableOpenMap(prev => ({ ...prev, [`ext_${optIndex}_${condIndex}`]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", cond.externalLookup?.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
|
||||
{table.table_name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[9px] text-muted-foreground mb-0.5">비교 컬럼 (WHERE)</p>
|
||||
<Select
|
||||
value={cond.externalLookup.matchColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, matchColumn: value }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue placeholder="컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[cond.externalLookup.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[9px] text-muted-foreground mb-0.5">가져올 값 (SELECT)</p>
|
||||
<Select
|
||||
value={cond.externalLookup.resultColumn}
|
||||
onValueChange={(value) => {
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, resultColumn: value },
|
||||
sourceField: value // sourceField에도 저장
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue placeholder="컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[cond.externalLookup.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2행: 비교 값 출처 */}
|
||||
<div className="p-1.5 bg-orange-50/50 rounded">
|
||||
<p className="text-[9px] text-muted-foreground mb-1">비교 값 출처</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Select
|
||||
value={cond.externalLookup.matchSourceType}
|
||||
onValueChange={(value: "currentRow" | "sourceTable" | "sectionField") => updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, matchSourceType: value, matchSourceField: "", matchSectionId: undefined }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] w-[80px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="currentRow" className="text-xs">현재 행</SelectItem>
|
||||
<SelectItem value="sourceTable" className="text-xs">소스 테이블</SelectItem>
|
||||
<SelectItem value="sectionField" className="text-xs">다른 섹션</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{cond.externalLookup.matchSourceType === "sectionField" && (
|
||||
<Select
|
||||
value={cond.externalLookup.matchSectionId || ""}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, matchSectionId: value, matchSourceField: "" }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] w-[60px]">
|
||||
<SelectValue placeholder="섹션" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sections.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id} className="text-xs">{s.title}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value={cond.externalLookup.matchSourceField}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
|
||||
externalLookup: { ...cond.externalLookup!, matchSourceField: value }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] flex-1">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cond.externalLookup.matchSourceType === "currentRow" ? (
|
||||
<>
|
||||
<div className="px-2 py-1 text-[9px] font-medium text-green-600 bg-green-50 border-b">
|
||||
설정된 컬럼
|
||||
</div>
|
||||
{tableConfig?.columns?.map((c) => (
|
||||
<SelectItem key={c.field} value={c.field} className="text-xs">{c.label} ({c.field})</SelectItem>
|
||||
))}
|
||||
</>
|
||||
) : cond.externalLookup.matchSourceType === "sourceTable" ? (
|
||||
<>
|
||||
<div className="px-2 py-1 text-[9px] font-medium text-orange-600 bg-orange-50 border-b">
|
||||
{sourceTableName} 컬럼
|
||||
</div>
|
||||
{sourceTableColumns.map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{cond.externalLookup.matchSectionId && (
|
||||
<div className="px-2 py-1 text-[9px] font-medium text-purple-600 bg-purple-50 border-b">
|
||||
{sections.find(s => s.id === cond.externalLookup?.matchSectionId)?.title} 필드
|
||||
</div>
|
||||
)}
|
||||
{formFields
|
||||
.filter((f) => !cond.externalLookup?.matchSectionId || f.sectionId === cond.externalLookup?.matchSectionId)
|
||||
.map((f) => (
|
||||
<SelectItem key={f.columnName} value={f.columnName} className="text-xs">{f.label}</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3행: 최종 조회 컬럼 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[9px] text-muted-foreground">조회된 값</span>
|
||||
<span className="text-[10px] text-muted-foreground">=</span>
|
||||
<Select
|
||||
value={cond.targetColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[9px] flex-1">
|
||||
<SelectValue placeholder="조회 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{option.tableName && (
|
||||
<div className="px-2 py-1 text-[9px] font-medium text-blue-600 bg-blue-50 border-b">
|
||||
{option.tableName} 컬럼
|
||||
</div>
|
||||
)}
|
||||
{(tableColumns[option.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 설명 텍스트 */}
|
||||
{cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && (
|
||||
<p className="text-[9px] text-orange-600 bg-orange-100/50 rounded px-1.5 py-0.5">
|
||||
{cond.externalLookup.tableName}에서 {cond.externalLookup.matchColumn} = 입력값 인 행의{" "}
|
||||
{cond.externalLookup.resultColumn} 값을 가져와 {option.tableName}.{cond.targetColumn}와 비교
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 값 변환 설정 (다른 섹션일 때만 표시) */}
|
||||
{cond.sourceType === "sectionField" && (
|
||||
<div className="pl-2 border-l-2 border-blue-200">
|
||||
<label className="flex items-center gap-1.5 text-[10px] cursor-pointer">
|
||||
<Switch
|
||||
checked={cond.transform?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
transform: { enabled: true, tableName: "", matchColumn: "", resultColumn: "" }
|
||||
});
|
||||
} else {
|
||||
updateLookupCondition(optIndex, condIndex, { transform: undefined });
|
||||
}
|
||||
}}
|
||||
className="scale-[0.5]"
|
||||
/>
|
||||
<span className="text-blue-600 font-medium">값 변환 필요</span>
|
||||
<span className="text-muted-foreground">(이름 → 코드 등)</span>
|
||||
</label>
|
||||
|
||||
{cond.transform?.enabled && (
|
||||
<div className="mt-1.5 p-2 bg-blue-50/50 rounded space-y-1.5">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div>
|
||||
<Label className="text-[9px] text-muted-foreground">변환 테이블</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="h-6 w-full justify-between text-[10px]">
|
||||
{cond.transform.tableName || "선택..."}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[180px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[150px]">
|
||||
<CommandEmpty className="text-xs py-2 text-center">없음</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.table_name}
|
||||
value={table.table_name}
|
||||
onSelect={() => {
|
||||
updateLookupCondition(optIndex, condIndex, {
|
||||
transform: { ...cond.transform!, tableName: table.table_name, matchColumn: "", resultColumn: "" }
|
||||
});
|
||||
onLoadTableColumns(table.table_name);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", cond.transform?.tableName === table.table_name ? "opacity-100" : "opacity-0")} />
|
||||
{table.table_name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px] text-muted-foreground">찾을 컬럼</Label>
|
||||
<Select
|
||||
value={cond.transform.matchColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
|
||||
transform: { ...cond.transform!, matchColumn: value }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue placeholder="선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[cond.transform.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px] text-muted-foreground">가져올 컬럼</Label>
|
||||
<Select
|
||||
value={cond.transform.resultColumn}
|
||||
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
|
||||
transform: { ...cond.transform!, resultColumn: value }
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue placeholder="선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(tableColumns[cond.transform.tableName] || []).map((c) => (
|
||||
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">{c.column_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{cond.transform.tableName && cond.transform.matchColumn && cond.transform.resultColumn && (
|
||||
<p className="text-[9px] text-blue-600 bg-blue-100/50 rounded px-1.5 py-0.5">
|
||||
{cond.transform.tableName}에서 {cond.transform.matchColumn} = 입력값 인 행의 {cond.transform.resultColumn} 값으로 변환
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조회 유형 설명 */}
|
||||
<p className="text-[10px] text-muted-foreground bg-muted/50 rounded p-1.5">
|
||||
{option.type === "sameTable" && "동일 테이블: 검색 모달에서 선택한 행의 다른 컬럼 값"}
|
||||
{option.type === "relatedTable" && "연관 테이블: 현재 행 데이터로 다른 테이블 조회"}
|
||||
{option.type === "combinedLookup" && "복합 조건: 다른 섹션 필드 + 현재 행 조합 조회"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -320,6 +1106,8 @@ interface TableSectionSettingsModalProps {
|
|||
// 카테고리 목록 (table_column_category_values에서 가져옴)
|
||||
categoryList?: { tableName: string; columnName: string; displayName?: string }[];
|
||||
onLoadCategoryList?: () => void;
|
||||
// 전체 섹션 목록 (다른 섹션 필드 참조용)
|
||||
allSections?: FormSectionConfig[];
|
||||
}
|
||||
|
||||
export function TableSectionSettingsModal({
|
||||
|
|
@ -332,6 +1120,7 @@ export function TableSectionSettingsModal({
|
|||
onLoadTableColumns,
|
||||
categoryList = [],
|
||||
onLoadCategoryList,
|
||||
allSections = [],
|
||||
}: TableSectionSettingsModalProps) {
|
||||
// 로컬 상태
|
||||
const [title, setTitle] = useState(section.title);
|
||||
|
|
@ -370,6 +1159,38 @@ export function TableSectionSettingsModal({
|
|||
}
|
||||
}, [tableConfig.saveConfig?.targetTable, onLoadTableColumns]);
|
||||
|
||||
// 조회 설정에 있는 테이블들의 컬럼 로드 (모달 열릴 때)
|
||||
useEffect(() => {
|
||||
if (open && tableConfig.columns) {
|
||||
const tablesToLoad = new Set<string>();
|
||||
|
||||
// 각 컬럼의 lookup 설정에서 테이블 수집
|
||||
tableConfig.columns.forEach((col) => {
|
||||
if (col.lookup?.enabled && col.lookup.options) {
|
||||
col.lookup.options.forEach((option) => {
|
||||
// 조회 테이블
|
||||
if (option.tableName) {
|
||||
tablesToLoad.add(option.tableName);
|
||||
}
|
||||
// 변환 테이블
|
||||
option.conditions?.forEach((cond) => {
|
||||
if (cond.transform?.enabled && cond.transform.tableName) {
|
||||
tablesToLoad.add(cond.transform.tableName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 수집된 테이블들의 컬럼 로드
|
||||
tablesToLoad.forEach((tableName) => {
|
||||
if (!tableColumns[tableName]) {
|
||||
onLoadTableColumns(tableName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [open, tableConfig.columns, tableColumns, onLoadTableColumns]);
|
||||
|
||||
// 소스 테이블의 컬럼 목록
|
||||
const sourceTableColumns = useMemo(() => {
|
||||
return tableColumns[tableConfig.source.tableName] || [];
|
||||
|
|
@ -385,6 +1206,30 @@ export function TableSectionSettingsModal({
|
|||
return sourceTableColumns;
|
||||
}, [tableColumns, tableConfig.saveConfig?.targetTable, sourceTableColumns]);
|
||||
|
||||
// 다른 섹션 목록 (현재 섹션 제외, 테이블 타입이 아닌 섹션만)
|
||||
const otherSections = useMemo(() => {
|
||||
return allSections
|
||||
.filter((s) => s.id !== section.id && s.type !== "table")
|
||||
.map((s) => ({ id: s.id, title: s.title }));
|
||||
}, [allSections, section.id]);
|
||||
|
||||
// 다른 섹션의 필드 목록
|
||||
const otherSectionFields = useMemo(() => {
|
||||
const fields: { columnName: string; label: string; sectionId: string }[] = [];
|
||||
allSections
|
||||
.filter((s) => s.id !== section.id && s.type !== "table")
|
||||
.forEach((s) => {
|
||||
(s.fields || []).forEach((f) => {
|
||||
fields.push({
|
||||
columnName: f.columnName,
|
||||
label: f.label,
|
||||
sectionId: s.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
return fields;
|
||||
}, [allSections, section.id]);
|
||||
|
||||
// 설정 업데이트 함수
|
||||
const updateTableConfig = (updates: Partial<TableSectionConfig>) => {
|
||||
setTableConfig((prev) => ({ ...prev, ...updates }));
|
||||
|
|
@ -831,6 +1676,13 @@ export function TableSectionSettingsModal({
|
|||
saveTableColumns={saveTableColumns}
|
||||
displayColumns={tableConfig.source.displayColumns || []}
|
||||
sourceTableColumns={sourceTableColumns}
|
||||
sourceTableName={tableConfig.source.tableName}
|
||||
tables={tables}
|
||||
tableColumns={tableColumns}
|
||||
sections={otherSections}
|
||||
formFields={otherSectionFields}
|
||||
tableConfig={tableConfig}
|
||||
onLoadTableColumns={onLoadTableColumns}
|
||||
onUpdate={(updates) => updateColumn(index, updates)}
|
||||
onMoveUp={() => moveColumn(index, "up")}
|
||||
onMoveDown={() => moveColumn(index, "down")}
|
||||
|
|
|
|||
|
|
@ -328,6 +328,97 @@ export interface TableColumnConfig {
|
|||
|
||||
// 컬럼 모드 전환 (동적 데이터 소스)
|
||||
columnModes?: ColumnModeConfig[];
|
||||
|
||||
// 조회 설정 (동적 값 조회)
|
||||
lookup?: LookupConfig;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 조회(Lookup) 설정 관련 타입 정의
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 조회 유형
|
||||
* - sameTable: 동일 테이블 조회 (소스 테이블에서 다른 컬럼 값)
|
||||
* - relatedTable: 연관 테이블 조회 (현재 행 기준으로 다른 테이블에서)
|
||||
* - combinedLookup: 복합 조건 조회 (다른 섹션 필드 + 현재 행 조합)
|
||||
*/
|
||||
export type LookupType = "sameTable" | "relatedTable" | "combinedLookup";
|
||||
|
||||
/**
|
||||
* 값 변환 설정
|
||||
* 예: 거래처 이름 → 거래처 코드로 변환
|
||||
*/
|
||||
export interface LookupTransform {
|
||||
enabled: boolean; // 변환 사용 여부
|
||||
tableName: string; // 변환 테이블 (예: customer_mng)
|
||||
matchColumn: string; // 찾을 컬럼 (예: customer_name)
|
||||
resultColumn: string; // 가져올 컬럼 (예: customer_code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 테이블 조회 설정
|
||||
* 다른 테이블에서 조건 값을 조회하여 사용 (이름→코드 변환 등)
|
||||
*/
|
||||
export interface ExternalTableLookup {
|
||||
tableName: string; // 조회할 테이블
|
||||
matchColumn: string; // 조회 조건 컬럼 (WHERE 절에서 비교할 컬럼)
|
||||
matchSourceType: "currentRow" | "sourceTable" | "sectionField"; // 비교값 출처
|
||||
matchSourceField: string; // 비교값 필드명
|
||||
matchSectionId?: string; // sectionField인 경우 섹션 ID
|
||||
resultColumn: string; // 가져올 컬럼 (SELECT 절)
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회 조건 설정
|
||||
*
|
||||
* sourceType 설명:
|
||||
* - "currentRow": 테이블에 설정된 컬럼 필드값 (rowData에서 가져옴, 예: part_code, quantity)
|
||||
* - "sourceTable": 원본 소스 테이블의 컬럼값 (_sourceData에서 가져옴, 예: item_number, company_code)
|
||||
* - "sectionField": 폼의 다른 섹션 필드값 (formData에서 가져옴, 예: partner_id)
|
||||
* - "externalTable": 외부 테이블에서 조회한 값 (다른 테이블에서 값을 조회해서 조건으로 사용)
|
||||
*/
|
||||
export interface LookupCondition {
|
||||
sourceType: "currentRow" | "sourceTable" | "sectionField" | "externalTable"; // 값 출처
|
||||
sourceField: string; // 출처의 필드명 (참조할 필드)
|
||||
sectionId?: string; // sectionField인 경우 섹션 ID
|
||||
targetColumn: string; // 조회 테이블의 컬럼
|
||||
|
||||
// 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우)
|
||||
externalLookup?: ExternalTableLookup;
|
||||
|
||||
// 값 변환 설정 (선택) - 이름→코드 등 변환이 필요할 때 (레거시 호환)
|
||||
transform?: LookupTransform;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회 옵션 설정
|
||||
* 하나의 컬럼에 여러 조회 방식을 정의하고 헤더에서 선택 가능
|
||||
*/
|
||||
export interface LookupOption {
|
||||
id: string; // 옵션 고유 ID
|
||||
label: string; // 옵션 라벨 (예: "기준단가", "거래처별 단가")
|
||||
displayLabel?: string; // 헤더 드롭다운에 표시될 텍스트 (예: "기준단가" → "단가 (기준단가)")
|
||||
type: LookupType; // 조회 유형
|
||||
|
||||
// 조회 테이블 설정
|
||||
tableName: string; // 조회할 테이블
|
||||
valueColumn: string; // 가져올 컬럼
|
||||
|
||||
// 조회 조건 (여러 조건 AND로 결합)
|
||||
conditions: LookupCondition[];
|
||||
|
||||
// 기본 옵션 여부
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 조회 설정
|
||||
*/
|
||||
export interface LookupConfig {
|
||||
enabled: boolean; // 조회 사용 여부
|
||||
options: LookupOption[]; // 조회 옵션 목록
|
||||
defaultOptionId?: string; // 기본 선택 옵션 ID
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -354,12 +445,28 @@ export interface ValueMappingConfig {
|
|||
/**
|
||||
* 테이블 조인 조건
|
||||
* 외부 테이블 조회 시 사용하는 조인 조건
|
||||
*
|
||||
* sourceType 설명:
|
||||
* - "row": 현재 행의 설정된 컬럼 (rowData)
|
||||
* - "sourceData": 원본 소스 테이블 데이터 (_sourceData)
|
||||
* - "formData": 폼의 다른 섹션 필드 (formData)
|
||||
* - "externalTable": 외부 테이블에서 조회한 값
|
||||
*/
|
||||
export interface TableJoinCondition {
|
||||
sourceType: "row" | "formData"; // 값 출처 (현재 행 또는 폼 데이터)
|
||||
sourceType: "row" | "sourceData" | "formData" | "externalTable"; // 값 출처
|
||||
sourceField: string; // 출처의 필드명
|
||||
targetColumn: string; // 조회 테이블의 컬럼
|
||||
operator?: "=" | "!=" | ">" | "<" | ">=" | "<="; // 연산자 (기본: "=")
|
||||
|
||||
// 외부 테이블 조회 설정 (sourceType이 "externalTable"인 경우)
|
||||
externalLookup?: ExternalTableLookup;
|
||||
|
||||
// 값 변환 설정 (선택) - 이름→코드 등 중간 변환이 필요할 때 (레거시 호환)
|
||||
transform?: {
|
||||
tableName: string; // 변환 테이블 (예: customer_mng)
|
||||
matchColumn: string; // 찾을 컬럼 (예: customer_name)
|
||||
resultColumn: string; // 가져올 컬럼 (예: customer_code)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -665,3 +772,18 @@ export const MODAL_FILTER_TYPE_OPTIONS = [
|
|||
{ value: "category", label: "테이블 조회" },
|
||||
{ value: "text", label: "텍스트 입력" },
|
||||
] as const;
|
||||
|
||||
// 조회 유형 옵션
|
||||
export const LOOKUP_TYPE_OPTIONS = [
|
||||
{ value: "sameTable", label: "동일 테이블 조회" },
|
||||
{ value: "relatedTable", label: "연관 테이블 조회" },
|
||||
{ value: "combinedLookup", label: "복합 조건 조회" },
|
||||
] as const;
|
||||
|
||||
// 조회 조건 소스 타입 옵션
|
||||
export const LOOKUP_CONDITION_SOURCE_OPTIONS = [
|
||||
{ value: "currentRow", label: "현재 행" },
|
||||
{ value: "sourceTable", label: "소스 테이블" },
|
||||
{ value: "sectionField", label: "다른 섹션" },
|
||||
{ value: "externalTable", label: "외부 테이블" },
|
||||
] as const;
|
||||
|
|
|
|||
Loading…
Reference in New Issue