거래처 에러수정
This commit is contained in:
parent
93d9937343
commit
bc66f3bba1
|
|
@ -29,6 +29,7 @@ export class EntityJoinController {
|
|||
screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열)
|
||||
autoFilter, // 🔒 멀티테넌시 자동 필터
|
||||
dataFilter, // 🆕 데이터 필터 (JSON 문자열)
|
||||
excludeFilter, // 🆕 제외 필터 (JSON 문자열) - 다른 테이블에 이미 존재하는 데이터 제외
|
||||
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
|
||||
...otherParams
|
||||
} = req.query;
|
||||
|
|
@ -125,6 +126,19 @@ export class EntityJoinController {
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
let parsedExcludeFilter: any = undefined;
|
||||
if (excludeFilter) {
|
||||
try {
|
||||
parsedExcludeFilter =
|
||||
typeof excludeFilter === "string" ? JSON.parse(excludeFilter) : excludeFilter;
|
||||
logger.info("제외 필터 파싱 완료:", parsedExcludeFilter);
|
||||
} catch (error) {
|
||||
logger.warn("제외 필터 파싱 오류:", error);
|
||||
parsedExcludeFilter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await tableManagementService.getTableDataWithEntityJoins(
|
||||
tableName,
|
||||
{
|
||||
|
|
@ -141,6 +155,7 @@ export class EntityJoinController {
|
|||
additionalJoinColumns: parsedAdditionalJoinColumns,
|
||||
screenEntityConfigs: parsedScreenEntityConfigs,
|
||||
dataFilter: parsedDataFilter, // 🆕 데이터 필터 전달
|
||||
excludeFilter: parsedExcludeFilter, // 🆕 제외 필터 전달
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2462,6 +2462,14 @@ export class TableManagementService {
|
|||
}>;
|
||||
screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
|
||||
dataFilter?: any; // 🆕 데이터 필터
|
||||
excludeFilter?: {
|
||||
enabled: boolean;
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
sourceColumn: string;
|
||||
filterColumn?: string;
|
||||
filterValue?: any;
|
||||
}; // 🆕 제외 필터 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
}
|
||||
): Promise<EntityJoinResponse> {
|
||||
const startTime = Date.now();
|
||||
|
|
@ -2716,6 +2724,44 @@ export class TableManagementService {
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 제외 필터 적용 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
if (options.excludeFilter && options.excludeFilter.enabled) {
|
||||
const {
|
||||
referenceTable,
|
||||
referenceColumn,
|
||||
sourceColumn,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
} = options.excludeFilter;
|
||||
|
||||
if (referenceTable && referenceColumn && sourceColumn) {
|
||||
// 서브쿼리로 이미 존재하는 데이터 제외
|
||||
let excludeSubquery = `main."${sourceColumn}" NOT IN (
|
||||
SELECT "${referenceColumn}" FROM "${referenceTable}"
|
||||
WHERE "${referenceColumn}" IS NOT NULL`;
|
||||
|
||||
// 추가 필터 조건이 있으면 적용 (예: 특정 거래처의 품목만 제외)
|
||||
if (filterColumn && filterValue !== undefined && filterValue !== null) {
|
||||
excludeSubquery += ` AND "${filterColumn}" = '${String(filterValue).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
excludeSubquery += ")";
|
||||
|
||||
whereClause = whereClause
|
||||
? `${whereClause} AND ${excludeSubquery}`
|
||||
: excludeSubquery;
|
||||
|
||||
logger.info(`🚫 제외 필터 적용 (Entity 조인):`, {
|
||||
referenceTable,
|
||||
referenceColumn,
|
||||
sourceColumn,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
excludeSubquery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ORDER BY 절 구성
|
||||
const orderBy = options.sortBy
|
||||
? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@ export const entityJoinApi = {
|
|||
}>;
|
||||
screenEntityConfigs?: Record<string, any>; // 🎯 화면별 엔티티 설정
|
||||
dataFilter?: any; // 🆕 데이터 필터
|
||||
excludeFilter?: {
|
||||
enabled: boolean;
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
sourceColumn: string;
|
||||
filterColumn?: string;
|
||||
filterValue?: any;
|
||||
}; // 🆕 제외 필터 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
} = {},
|
||||
): Promise<EntityJoinResponse> => {
|
||||
// 🔒 멀티테넌시: company_code 자동 필터링 활성화
|
||||
|
|
@ -90,6 +98,7 @@ export const entityJoinApi = {
|
|||
screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정
|
||||
autoFilter: JSON.stringify(autoFilter), // 🔒 멀티테넌시 필터링
|
||||
dataFilter: params.dataFilter ? JSON.stringify(params.dataFilter) : undefined, // 🆕 데이터 필터
|
||||
excludeFilter: params.excludeFilter ? JSON.stringify(params.excludeFilter) : undefined, // 🆕 제외 필터
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
|
|
|
|||
|
|
@ -663,9 +663,29 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 🆕 modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
|
||||
let effectiveSelectedRowsData = selectedRowsData;
|
||||
if ((!selectedRowsData || selectedRowsData.length === 0) && effectiveTableName) {
|
||||
try {
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||
const modalData = dataRegistry[effectiveTableName];
|
||||
if (modalData && modalData.length > 0) {
|
||||
effectiveSelectedRowsData = modalData;
|
||||
console.log("🔗 [ButtonPrimaryComponent] modalDataStore에서 선택된 데이터 가져옴:", {
|
||||
tableName: effectiveTableName,
|
||||
count: modalData.length,
|
||||
data: modalData,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("modalDataStore 접근 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
|
||||
const hasDataToDelete =
|
||||
(selectedRowsData && selectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0);
|
||||
(effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0);
|
||||
|
||||
if (processedConfig.action.type === "delete" && !hasDataToDelete) {
|
||||
toast.warning("삭제할 항목을 먼저 선택해주세요.");
|
||||
|
|
@ -724,9 +744,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
onClose,
|
||||
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
||||
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
|
||||
// 테이블 선택된 행 정보 추가
|
||||
// 테이블 선택된 행 정보 추가 (modalDataStore에서 가져온 데이터 우선)
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
selectedRowsData: effectiveSelectedRowsData,
|
||||
// 테이블 정렬 정보 추가
|
||||
sortBy, // 🆕 정렬 컬럼
|
||||
sortOrder, // 🆕 정렬 방향
|
||||
|
|
|
|||
|
|
@ -293,8 +293,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
) => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
|
||||
// 카테고리 매핑이 있는지 확인
|
||||
const mapping = categoryMappings[columnName];
|
||||
// 🆕 카테고리 매핑 찾기 (여러 키 형태 시도)
|
||||
// 1. 전체 컬럼명 (예: "item_info.material")
|
||||
// 2. 컬럼명만 (예: "material")
|
||||
let mapping = categoryMappings[columnName];
|
||||
|
||||
if (!mapping && columnName.includes(".")) {
|
||||
// 조인된 컬럼의 경우 컬럼명만으로 다시 시도
|
||||
const simpleColumnName = columnName.split(".").pop() || columnName;
|
||||
mapping = categoryMappings[simpleColumnName];
|
||||
}
|
||||
|
||||
if (mapping && mapping[String(value)]) {
|
||||
const categoryData = mapping[String(value)];
|
||||
const displayLabel = categoryData.label || String(value);
|
||||
|
|
@ -690,43 +699,69 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
loadLeftCategoryMappings();
|
||||
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
|
||||
|
||||
// 우측 테이블 카테고리 매핑 로드
|
||||
// 우측 테이블 카테고리 매핑 로드 (조인된 테이블 포함)
|
||||
useEffect(() => {
|
||||
const loadRightCategoryMappings = async () => {
|
||||
const rightTableName = componentConfig.rightPanel?.tableName;
|
||||
if (!rightTableName || isDesignMode) return;
|
||||
|
||||
try {
|
||||
// 1. 컬럼 메타 정보 조회
|
||||
const columnsResponse = await tableTypeApi.getColumns(rightTableName);
|
||||
const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category");
|
||||
|
||||
if (categoryColumns.length === 0) {
|
||||
setRightCategoryMappings({});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 각 카테고리 컬럼에 대한 값 조회
|
||||
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
|
||||
|
||||
for (const col of categoryColumns) {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
try {
|
||||
const response = await apiClient.get(`/table-categories/${rightTableName}/${columnName}/values`);
|
||||
// 🆕 우측 패널 컬럼 설정에서 조인된 테이블 추출
|
||||
const rightColumns = componentConfig.rightPanel?.columns || [];
|
||||
const tablesToLoad = new Set<string>([rightTableName]);
|
||||
|
||||
// 컬럼명에서 테이블명 추출 (예: "item_info.material" -> "item_info")
|
||||
rightColumns.forEach((col: any) => {
|
||||
const colName = col.name || col.columnName;
|
||||
if (colName && colName.includes(".")) {
|
||||
const joinTableName = colName.split(".")[0];
|
||||
tablesToLoad.add(joinTableName);
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
valueMap[item.value_code || item.valueCode] = {
|
||||
label: item.value_label || item.valueLabel,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
mappings[columnName] = valueMap;
|
||||
console.log(`✅ 우측 카테고리 매핑 로드 [${columnName}]:`, valueMap);
|
||||
console.log("🔍 우측 패널 카테고리 로드 대상 테이블:", Array.from(tablesToLoad));
|
||||
|
||||
// 각 테이블에 대해 카테고리 매핑 로드
|
||||
for (const tableName of tablesToLoad) {
|
||||
try {
|
||||
// 1. 컬럼 메타 정보 조회
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category");
|
||||
|
||||
// 2. 각 카테고리 컬럼에 대한 값 조회
|
||||
for (const col of categoryColumns) {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
try {
|
||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
valueMap[item.value_code || item.valueCode] = {
|
||||
label: item.value_label || item.valueLabel,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
|
||||
// 조인된 테이블의 경우 "테이블명.컬럼명" 형태로 저장
|
||||
const mappingKey = tableName === rightTableName ? columnName : `${tableName}.${columnName}`;
|
||||
mappings[mappingKey] = valueMap;
|
||||
|
||||
// 🆕 컬럼명만으로도 접근할 수 있도록 추가 저장 (모든 테이블)
|
||||
// 기존 매핑이 있으면 병합, 없으면 새로 생성
|
||||
mappings[columnName] = { ...(mappings[columnName] || {}), ...valueMap };
|
||||
|
||||
console.log(`✅ 우측 카테고리 매핑 로드 [${mappingKey}]:`, valueMap);
|
||||
console.log(`✅ 우측 카테고리 매핑 (컬럼명만) [${columnName}]:`, mappings[columnName]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`우측 카테고리 값 조회 실패 [${tableName}.${columnName}]:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`우측 카테고리 값 조회 실패 [${columnName}]:`, error);
|
||||
console.error(`테이블 ${tableName} 컬럼 정보 조회 실패:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -737,7 +772,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
};
|
||||
|
||||
loadRightCategoryMappings();
|
||||
}, [componentConfig.rightPanel?.tableName, isDesignMode]);
|
||||
}, [componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, isDesignMode]);
|
||||
|
||||
// 항목 펼치기/접기 토글
|
||||
const toggleExpand = useCallback((itemId: any) => {
|
||||
|
|
@ -2149,9 +2184,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const format = colConfig?.format;
|
||||
const boldValue = colConfig?.bold ?? false;
|
||||
|
||||
// 숫자 포맷 적용
|
||||
let displayValue = String(value || "-");
|
||||
if (value !== null && value !== undefined && value !== "" && format) {
|
||||
// 🆕 카테고리 매핑 적용
|
||||
const formattedValue = formatCellValue(key, value, rightCategoryMappings);
|
||||
|
||||
// 숫자 포맷 적용 (카테고리가 아닌 경우만)
|
||||
let displayValue: React.ReactNode = formattedValue;
|
||||
if (typeof formattedValue === 'string' && value !== null && value !== undefined && value !== "" && format) {
|
||||
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (!isNaN(numValue)) {
|
||||
displayValue = numValue.toLocaleString('ko-KR', {
|
||||
|
|
@ -2175,7 +2213,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
)}
|
||||
<span
|
||||
className={`text-foreground text-sm ${boldValue ? 'font-semibold' : ''}`}
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
|
|
@ -2240,9 +2277,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const colConfig = rightColumns?.find(c => c.name === key);
|
||||
const format = colConfig?.format;
|
||||
|
||||
// 숫자 포맷 적용
|
||||
let displayValue = String(value);
|
||||
if (value !== null && value !== undefined && value !== "" && format) {
|
||||
// 🆕 카테고리 매핑 적용
|
||||
const formattedValue = formatCellValue(key, value, rightCategoryMappings);
|
||||
|
||||
// 숫자 포맷 적용 (카테고리가 아닌 경우만)
|
||||
let displayValue: React.ReactNode = formattedValue;
|
||||
if (typeof formattedValue === 'string' && value !== null && value !== undefined && value !== "" && format) {
|
||||
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (!isNaN(numValue)) {
|
||||
displayValue = numValue.toLocaleString('ko-KR', {
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
config,
|
||||
className,
|
||||
style,
|
||||
formData: propFormData, // 🆕 부모에서 전달받은 formData
|
||||
onFormDataChange,
|
||||
componentConfig,
|
||||
onSelectedRowsChange,
|
||||
|
|
@ -1198,6 +1199,60 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
console.log("🎯 [TableList] 화면별 엔티티 설정:", screenEntityConfigs);
|
||||
|
||||
// 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
let excludeFilterParam: any = undefined;
|
||||
if (tableConfig.excludeFilter?.enabled) {
|
||||
const excludeConfig = tableConfig.excludeFilter;
|
||||
let filterValue: any = undefined;
|
||||
|
||||
// 필터 값 소스에 따라 값 가져오기 (우선순위: formData > URL > 분할패널)
|
||||
if (excludeConfig.filterColumn && excludeConfig.filterValueField) {
|
||||
const fieldName = excludeConfig.filterValueField;
|
||||
|
||||
// 1순위: props로 전달받은 formData에서 값 가져오기 (모달에서 사용)
|
||||
if (propFormData && propFormData[fieldName]) {
|
||||
filterValue = propFormData[fieldName];
|
||||
console.log("🔗 [TableList] formData에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
// 2순위: URL 파라미터에서 값 가져오기
|
||||
else if (typeof window !== "undefined") {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
filterValue = urlParams.get(fieldName);
|
||||
if (filterValue) {
|
||||
console.log("🔗 [TableList] URL에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 3순위: 분할 패널 부모 데이터에서 값 가져오기
|
||||
if (!filterValue && splitPanelContext?.selectedLeftData) {
|
||||
filterValue = splitPanelContext.selectedLeftData[fieldName];
|
||||
if (filterValue) {
|
||||
console.log("🔗 [TableList] 분할패널에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValue || !excludeConfig.filterColumn) {
|
||||
excludeFilterParam = {
|
||||
enabled: true,
|
||||
referenceTable: excludeConfig.referenceTable,
|
||||
referenceColumn: excludeConfig.referenceColumn,
|
||||
sourceColumn: excludeConfig.sourceColumn,
|
||||
filterColumn: excludeConfig.filterColumn,
|
||||
filterValue: filterValue,
|
||||
};
|
||||
console.log("🚫 [TableList] 제외 필터 적용:", excludeFilterParam);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
||||
response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||
page,
|
||||
|
|
@ -1209,6 +1264,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
||||
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 전달
|
||||
dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
|
||||
excludeFilter: excludeFilterParam, // 🆕 제외 필터 전달
|
||||
});
|
||||
|
||||
// 실제 데이터의 item_number만 추출하여 중복 확인
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { TableListConfig, ColumnConfig } from "./types";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check } from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
|
|
@ -73,6 +74,12 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
|
||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||
|
||||
// 🆕 제외 필터용 참조 테이블 컬럼 목록
|
||||
const [referenceTableColumns, setReferenceTableColumns] = useState<
|
||||
Array<{ columnName: string; dataType: string; label?: string }>
|
||||
>([]);
|
||||
const [loadingReferenceColumns, setLoadingReferenceColumns] = useState(false);
|
||||
|
||||
// 🔄 외부에서 config가 변경될 때 내부 상태 동기화 (표의 페이지네이션 변경 감지)
|
||||
useEffect(() => {
|
||||
// console.log("🔄 TableListConfigPanel - 외부 config 변경 감지:", {
|
||||
|
|
@ -237,6 +244,42 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
fetchEntityJoinColumns();
|
||||
}, [config.selectedTable, screenTableName]);
|
||||
|
||||
// 🆕 제외 필터용 참조 테이블 컬럼 가져오기
|
||||
useEffect(() => {
|
||||
const fetchReferenceColumns = async () => {
|
||||
const refTable = config.excludeFilter?.referenceTable;
|
||||
if (!refTable) {
|
||||
setReferenceTableColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingReferenceColumns(true);
|
||||
try {
|
||||
console.log("🔗 참조 테이블 컬럼 정보 가져오기:", refTable);
|
||||
const result = await tableManagementApi.getColumnList(refTable);
|
||||
if (result.success && result.data) {
|
||||
// result.data는 { columns: [], total, page, size, totalPages } 형태
|
||||
const columns = result.data.columns || [];
|
||||
setReferenceTableColumns(
|
||||
columns.map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || "text",
|
||||
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
}))
|
||||
);
|
||||
console.log("✅ 참조 테이블 컬럼 로드 완료:", columns.length, "개");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 참조 테이블 컬럼 조회 오류:", error);
|
||||
setReferenceTableColumns([]);
|
||||
} finally {
|
||||
setLoadingReferenceColumns(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchReferenceColumns();
|
||||
}, [config.excludeFilter?.referenceTable]);
|
||||
|
||||
// 🎯 엔티티 컬럼 자동 로드
|
||||
useEffect(() => {
|
||||
const entityColumns = config.columns?.filter((col) => col.isEntityJoin && col.entityDisplayConfig);
|
||||
|
|
@ -1333,6 +1376,298 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🆕 제외 필터 설정 (다른 테이블에 이미 존재하는 데이터 제외) */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">제외 필터</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
다른 테이블에 이미 존재하는 데이터를 목록에서 제외합니다
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-border" />
|
||||
|
||||
{/* 제외 필터 활성화 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="excludeFilter-enabled"
|
||||
checked={config.excludeFilter?.enabled || false}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
enabled: checked as boolean,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="excludeFilter-enabled" className="text-xs">
|
||||
제외 필터 활성화
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{config.excludeFilter?.enabled && (
|
||||
<div className="space-y-3 rounded border p-3">
|
||||
{/* 참조 테이블 선택 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium">참조 테이블 (매핑 테이블)</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{config.excludeFilter?.referenceTable || "테이블 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs py-2">테이블을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableTables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
onSelect={() => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
referenceTable: table.tableName,
|
||||
referenceColumn: undefined,
|
||||
sourceColumn: undefined,
|
||||
filterColumn: undefined,
|
||||
filterValueField: undefined,
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.excludeFilter?.referenceTable === table.tableName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{table.displayName || table.tableName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{config.excludeFilter?.referenceTable && (
|
||||
<>
|
||||
{/* 비교 컬럼 설정 - 한 줄에 두 개 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 참조 컬럼 (매핑 테이블) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">비교 컬럼 (매핑)</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={loadingReferenceColumns}
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{loadingReferenceColumns
|
||||
? "..."
|
||||
: config.excludeFilter?.referenceColumn || "선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs py-2">없음</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{referenceTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.columnName}
|
||||
value={col.columnName}
|
||||
onSelect={() => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
referenceColumn: col.columnName,
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.excludeFilter?.referenceColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{col.label || col.columnName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 소스 컬럼 (현재 테이블) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">비교 컬럼 (현재)</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{config.excludeFilter?.sourceColumn || "선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs py-2">없음</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.columnName}
|
||||
value={col.columnName}
|
||||
onSelect={() => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
sourceColumn: col.columnName,
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.excludeFilter?.sourceColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{col.label || col.columnName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조건 필터 - 특정 조건의 데이터만 제외 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">조건 필터 (선택사항)</Label>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">
|
||||
특정 조건의 데이터만 제외하려면 설정하세요 (예: 특정 거래처의 품목만)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 필터 컬럼 (매핑 테이블) */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={loadingReferenceColumns}
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{loadingReferenceColumns
|
||||
? "..."
|
||||
: config.excludeFilter?.filterColumn
|
||||
? `매핑: ${config.excludeFilter.filterColumn}`
|
||||
: "매핑 테이블 컬럼"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs py-2">없음</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value=""
|
||||
onSelect={() => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
filterColumn: undefined,
|
||||
filterValueField: undefined,
|
||||
});
|
||||
}}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", !config.excludeFilter?.filterColumn ? "opacity-100" : "opacity-0")} />
|
||||
사용 안함
|
||||
</CommandItem>
|
||||
{referenceTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.columnName}
|
||||
value={col.columnName}
|
||||
onSelect={() => {
|
||||
// 필터 컬럼 선택 시 같은 이름의 필드를 자동으로 설정
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
filterColumn: col.columnName,
|
||||
filterValueField: col.columnName, // 같은 이름으로 자동 설정
|
||||
filterValueSource: "url",
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.excludeFilter?.filterColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{col.label || col.columnName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* 필터 값 필드명 (부모 화면에서 전달받는 필드) */}
|
||||
<Input
|
||||
placeholder="예: customer_code"
|
||||
value={config.excludeFilter?.filterValueField || ""}
|
||||
onChange={(e) => {
|
||||
handleChange("excludeFilter", {
|
||||
...config.excludeFilter,
|
||||
filterValueField: e.target.value,
|
||||
});
|
||||
}}
|
||||
disabled={!config.excludeFilter?.filterColumn}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 설정 요약 */}
|
||||
{config.excludeFilter?.referenceTable && config.excludeFilter?.referenceColumn && config.excludeFilter?.sourceColumn && (
|
||||
<div className="rounded bg-muted/50 p-2 text-[10px] text-muted-foreground">
|
||||
<strong>설정 요약:</strong> {config.selectedTable || screenTableName}.{config.excludeFilter.sourceColumn} 가
|
||||
{" "}{config.excludeFilter.referenceTable}.{config.excludeFilter.referenceColumn} 에
|
||||
{config.excludeFilter.filterColumn && config.excludeFilter.filterValueField && (
|
||||
<> ({config.excludeFilter.filterColumn}=URL의 {config.excludeFilter.filterValueField}일 때)</>
|
||||
)}
|
||||
{" "}이미 있으면 제외
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -185,6 +185,21 @@ export interface LinkedFilterConfig {
|
|||
enabled?: boolean; // 활성화 여부 (기본: true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 제외 필터 설정
|
||||
* 다른 테이블에 이미 존재하는 데이터를 제외하고 표시
|
||||
* 예: 거래처에 이미 등록된 품목을 품목 선택 모달에서 제외
|
||||
*/
|
||||
export interface ExcludeFilterConfig {
|
||||
enabled: boolean; // 제외 필터 활성화 여부
|
||||
referenceTable: string; // 참조 테이블 (예: customer_item_mapping)
|
||||
referenceColumn: string; // 참조 테이블의 비교 컬럼 (예: item_id)
|
||||
sourceColumn: string; // 현재 테이블의 비교 컬럼 (예: item_number)
|
||||
filterColumn?: string; // 참조 테이블의 필터 컬럼 (예: customer_id)
|
||||
filterValueSource?: "url" | "formData" | "parentData"; // 필터 값 소스 (기본: url)
|
||||
filterValueField?: string; // 필터 값 필드명 (예: customer_code)
|
||||
}
|
||||
|
||||
/**
|
||||
* TableList 컴포넌트 설정 타입
|
||||
*/
|
||||
|
|
@ -249,6 +264,9 @@ export interface TableListConfig extends ComponentConfig {
|
|||
// 🆕 연결된 필터 (다른 컴포넌트 값으로 필터링)
|
||||
linkedFilters?: LinkedFilterConfig[];
|
||||
|
||||
// 🆕 제외 필터 (다른 테이블에 이미 존재하는 데이터 제외)
|
||||
excludeFilter?: ExcludeFilterConfig;
|
||||
|
||||
// 이벤트 핸들러
|
||||
onRowClick?: (row: any) => void;
|
||||
onRowDoubleClick?: (row: any) => void;
|
||||
|
|
|
|||
|
|
@ -1236,8 +1236,13 @@ export class ButtonActionExecutor {
|
|||
} else {
|
||||
console.log("🔄 테이블 데이터 삭제 완료, 테이블 새로고침 호출");
|
||||
context.onRefresh?.(); // 테이블 새로고침
|
||||
|
||||
// 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
console.log("🔄 refreshTable 전역 이벤트 발생");
|
||||
}
|
||||
|
||||
toast.success(config.successMessage || `${dataToDelete.length}개 항목이 삭제되었습니다.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1258,6 +1263,12 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
context.onRefresh?.();
|
||||
|
||||
// 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
console.log("🔄 refreshTable 전역 이벤트 발생 (단일 삭제)");
|
||||
|
||||
toast.success(config.successMessage || "삭제되었습니다.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("삭제 오류:", error);
|
||||
|
|
@ -1536,6 +1547,13 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 부모 화면의 선택된 데이터 가져오기 (excludeFilter에서 사용)
|
||||
const parentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};
|
||||
console.log("📦 [openModalWithData] 부모 데이터 전달:", {
|
||||
dataSourceId,
|
||||
parentData,
|
||||
});
|
||||
|
||||
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
||||
const modalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
|
|
@ -1544,6 +1562,7 @@ export class ButtonActionExecutor {
|
|||
description: description,
|
||||
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
||||
urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
|
||||
splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용)
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1679,3 +1679,4 @@ const 출고등록_설정: ScreenSplitPanel = {
|
|||
|
||||
화면 임베딩 및 데이터 전달 시스템은 복잡한 업무 워크플로우를 효율적으로 처리할 수 있는 강력한 기능입니다. 단계별로 체계적으로 구현하면 약 3.5개월 내에 완성할 수 있으며, 이를 통해 사용자 경험을 크게 향상시킬 수 있습니다.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -526,3 +526,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
|
|||
|
||||
이제 입고 등록과 같은 복잡한 워크플로우를 구현할 수 있습니다. 다음 단계는 각 컴포넌트 타입별 DataReceivable 인터페이스 구현과 설정 UI 개발입니다.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -513,3 +513,4 @@ function ScreenViewPage() {
|
|||
|
||||
새로운 시스템은 기존 시스템과 **독립적으로 동작**하며, 최소한의 수정만으로 통합 가능합니다. 화면 페이지에 조건 분기만 추가하면 바로 사용할 수 있습니다.
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue