Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
7cb8026979
|
|
@ -2409,11 +2409,19 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
||||||
|
// 🔧 테이블에 존재하는 컬럼만 UPDATE (가상 컬럼 제외)
|
||||||
const setConditions: string[] = [];
|
const setConditions: string[] = [];
|
||||||
const setValues: any[] = [];
|
const setValues: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
const skippedColumns: string[] = [];
|
||||||
|
|
||||||
Object.keys(updatedData).forEach((column) => {
|
Object.keys(updatedData).forEach((column) => {
|
||||||
|
// 테이블에 존재하지 않는 컬럼은 스킵
|
||||||
|
if (!columnTypeMap.has(column)) {
|
||||||
|
skippedColumns.push(column);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dataType = columnTypeMap.get(column) || "text";
|
const dataType = columnTypeMap.get(column) || "text";
|
||||||
setConditions.push(
|
setConditions.push(
|
||||||
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
||||||
|
|
@ -2424,6 +2432,10 @@ export class TableManagementService {
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (skippedColumns.length > 0) {
|
||||||
|
logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
||||||
let whereConditions: string[] = [];
|
let whereConditions: string[] = [];
|
||||||
let whereValues: any[] = [];
|
let whereValues: any[] = [];
|
||||||
|
|
@ -3930,9 +3942,10 @@ export class TableManagementService {
|
||||||
`컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
|
`컬럼 입력타입 정보 조회: ${tableName}, company: ${companyCode}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// table_type_columns에서 입력타입 정보 조회 (company_code 필터링)
|
// table_type_columns에서 입력타입 정보 조회
|
||||||
|
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||||
const rawInputTypes = await query<any>(
|
const rawInputTypes = await query<any>(
|
||||||
`SELECT
|
`SELECT DISTINCT ON (ttc.column_name)
|
||||||
ttc.column_name as "columnName",
|
ttc.column_name as "columnName",
|
||||||
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
||||||
ttc.input_type as "inputType",
|
ttc.input_type as "inputType",
|
||||||
|
|
@ -3946,8 +3959,10 @@ export class TableManagementService {
|
||||||
LEFT JOIN information_schema.columns ic
|
LEFT JOIN information_schema.columns ic
|
||||||
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name
|
||||||
WHERE ttc.table_name = $1
|
WHERE ttc.table_name = $1
|
||||||
AND ttc.company_code = $2
|
AND ttc.company_code IN ($2, '*')
|
||||||
ORDER BY ttc.display_order, ttc.column_name`,
|
ORDER BY ttc.column_name,
|
||||||
|
CASE WHEN ttc.company_code = $2 THEN 0 ELSE 1 END,
|
||||||
|
ttc.display_order`,
|
||||||
[tableName, companyCode]
|
[tableName, companyCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -3961,17 +3976,20 @@ export class TableManagementService {
|
||||||
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
const mappingTableExists = tableExistsResult[0]?.table_exists === true;
|
||||||
|
|
||||||
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
// 카테고리 컬럼의 경우, 매핑된 메뉴 목록 조회
|
||||||
|
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||||
let categoryMappings: Map<string, number[]> = new Map();
|
let categoryMappings: Map<string, number[]> = new Map();
|
||||||
if (mappingTableExists) {
|
if (mappingTableExists) {
|
||||||
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
logger.info("카테고리 매핑 조회 시작", { tableName, companyCode });
|
||||||
|
|
||||||
const mappings = await query<any>(
|
const mappings = await query<any>(
|
||||||
`SELECT
|
`SELECT DISTINCT ON (logical_column_name, menu_objid)
|
||||||
logical_column_name as "columnName",
|
logical_column_name as "columnName",
|
||||||
menu_objid as "menuObjid"
|
menu_objid as "menuObjid"
|
||||||
FROM category_column_mapping
|
FROM category_column_mapping
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND company_code = $2`,
|
AND company_code IN ($2, '*')
|
||||||
|
ORDER BY logical_column_name, menu_objid,
|
||||||
|
CASE WHEN company_code = $2 THEN 0 ELSE 1 END`,
|
||||||
[tableName, companyCode]
|
[tableName, companyCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,42 @@ export function AutocompleteSearchInputComponent({
|
||||||
const displayField = config?.displayField || propDisplayField || "";
|
const displayField = config?.displayField || propDisplayField || "";
|
||||||
const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드
|
const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드
|
||||||
const displaySeparator = config?.displaySeparator || " → "; // 구분자
|
const displaySeparator = config?.displaySeparator || " → "; // 구분자
|
||||||
const valueField = config?.valueField || propValueField || "";
|
|
||||||
|
// valueField 결정: fieldMappings 기반으로 추론 (config.valueField가 fieldMappings에 없으면 무시)
|
||||||
|
const getValueField = () => {
|
||||||
|
// fieldMappings가 있으면 그 안에서 추론 (가장 신뢰할 수 있는 소스)
|
||||||
|
if (config?.fieldMappings && config.fieldMappings.length > 0) {
|
||||||
|
// config.valueField가 fieldMappings의 sourceField에 있으면 사용
|
||||||
|
if (config?.valueField) {
|
||||||
|
const hasValueFieldInMappings = config.fieldMappings.some(
|
||||||
|
(m: any) => m.sourceField === config.valueField
|
||||||
|
);
|
||||||
|
if (hasValueFieldInMappings) {
|
||||||
|
return config.valueField;
|
||||||
|
}
|
||||||
|
// fieldMappings에 없으면 무시하고 추론
|
||||||
|
}
|
||||||
|
|
||||||
|
// _code 또는 _id로 끝나는 필드 우선 (보통 PK나 코드 필드)
|
||||||
|
const codeMapping = config.fieldMappings.find(
|
||||||
|
(m: any) => m.sourceField?.endsWith("_code") || m.sourceField?.endsWith("_id")
|
||||||
|
);
|
||||||
|
if (codeMapping) {
|
||||||
|
return codeMapping.sourceField;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 없으면 첫 번째 매핑 사용
|
||||||
|
return config.fieldMappings[0].sourceField || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldMappings가 없으면 기존 방식
|
||||||
|
if (config?.valueField) return config.valueField;
|
||||||
|
if (propValueField) return propValueField;
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
const valueField = getValueField();
|
||||||
|
|
||||||
const searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용
|
const searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용
|
||||||
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
||||||
|
|
||||||
|
|
@ -76,11 +111,39 @@ export function AutocompleteSearchInputComponent({
|
||||||
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
|
// 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지)
|
||||||
const selectedDataRef = useRef<EntitySearchResult | null>(null);
|
const selectedDataRef = useRef<EntitySearchResult | null>(null);
|
||||||
const inputValueRef = useRef<string>("");
|
const inputValueRef = useRef<string>("");
|
||||||
|
const initialValueLoadedRef = useRef<string | null>(null); // 초기값 로드 추적
|
||||||
|
|
||||||
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
||||||
const currentValue = isInteractive && formData && component?.columnName
|
// 우선순위: 1) component.columnName, 2) fieldMappings에서 valueField에 매핑된 targetField
|
||||||
? formData[component.columnName]
|
const getCurrentValue = () => {
|
||||||
: value;
|
if (!isInteractive || !formData) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. component.columnName으로 직접 바인딩된 경우
|
||||||
|
if (component?.columnName && formData[component.columnName] !== undefined) {
|
||||||
|
return formData[component.columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. fieldMappings에서 valueField와 매핑된 targetField에서 값 가져오기
|
||||||
|
if (config?.fieldMappings && Array.isArray(config.fieldMappings)) {
|
||||||
|
const valueFieldMapping = config.fieldMappings.find(
|
||||||
|
(mapping: any) => mapping.sourceField === valueField
|
||||||
|
);
|
||||||
|
|
||||||
|
if (valueFieldMapping) {
|
||||||
|
const targetField = valueFieldMapping.targetField || valueFieldMapping.targetColumn;
|
||||||
|
|
||||||
|
if (targetField && formData[targetField] !== undefined) {
|
||||||
|
return formData[targetField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentValue = getCurrentValue();
|
||||||
|
|
||||||
// selectedData 변경 시 ref도 업데이트
|
// selectedData 변경 시 ref도 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -98,6 +161,79 @@ export function AutocompleteSearchInputComponent({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 초기값이 있을 때 해당 값의 표시 텍스트를 조회하여 설정
|
||||||
|
useEffect(() => {
|
||||||
|
const loadInitialDisplayValue = async () => {
|
||||||
|
// 이미 로드된 값이거나, 값이 없거나, 이미 선택된 데이터가 있으면 스킵
|
||||||
|
if (!currentValue || selectedData || selectedDataRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 같은 값을 로드한 적이 있으면 스킵
|
||||||
|
if (initialValueLoadedRef.current === currentValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블명과 필드 정보가 없으면 스킵
|
||||||
|
if (!tableName || !valueField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔄 AutocompleteSearchInput 초기값 로드:", {
|
||||||
|
currentValue,
|
||||||
|
tableName,
|
||||||
|
valueField,
|
||||||
|
displayFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API를 통해 해당 값의 표시 텍스트 조회
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const filterConditionWithValue = {
|
||||||
|
...filterCondition,
|
||||||
|
[valueField]: currentValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
searchText: "",
|
||||||
|
searchFields: searchFields.join(","),
|
||||||
|
filterCondition: JSON.stringify(filterConditionWithValue),
|
||||||
|
page: "1",
|
||||||
|
limit: "10",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await apiClient.get<{ success: boolean; data: EntitySearchResult[] }>(
|
||||||
|
`/entity-search/${tableName}?${params.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
|
const matchedItem = response.data.data.find((item: EntitySearchResult) =>
|
||||||
|
String(item[valueField]) === String(currentValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedItem) {
|
||||||
|
const displayText = getDisplayValue(matchedItem);
|
||||||
|
console.log("✅ 초기값 표시 텍스트 로드 성공:", {
|
||||||
|
currentValue,
|
||||||
|
displayText,
|
||||||
|
matchedItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedData(matchedItem);
|
||||||
|
setInputValue(displayText);
|
||||||
|
selectedDataRef.current = matchedItem;
|
||||||
|
inputValueRef.current = displayText;
|
||||||
|
initialValueLoadedRef.current = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 초기값 표시 텍스트 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInitialDisplayValue();
|
||||||
|
}, [currentValue, tableName, valueField, displayFields, filterCondition, searchFields, selectedData]);
|
||||||
|
|
||||||
// value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지
|
// value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
|
// selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우)
|
||||||
|
|
@ -107,6 +243,7 @@ export function AutocompleteSearchInputComponent({
|
||||||
|
|
||||||
if (!currentValue) {
|
if (!currentValue) {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
|
initialValueLoadedRef.current = null; // 값이 없어지면 초기화
|
||||||
}
|
}
|
||||||
}, [currentValue, selectedData]);
|
}, [currentValue, selectedData]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,40 @@ export class ButtonActionExecutor {
|
||||||
console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields);
|
console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 루트 레벨 formData에서 RepeaterFieldGroup에 전달할 공통 필드 추출
|
||||||
|
// 주문번호, 발주번호 등 마스터-디테일 관계에서 필요한 필드만 명시적으로 지정
|
||||||
|
const masterDetailFields = [
|
||||||
|
// 번호 필드
|
||||||
|
"order_no", // 발주번호
|
||||||
|
"sales_order_no", // 수주번호
|
||||||
|
"shipment_no", // 출하번호
|
||||||
|
"receipt_no", // 입고번호
|
||||||
|
"work_order_no", // 작업지시번호
|
||||||
|
// 거래처 필드
|
||||||
|
"supplier_code", // 공급처 코드
|
||||||
|
"supplier_name", // 공급처 이름
|
||||||
|
"customer_code", // 고객 코드
|
||||||
|
"customer_name", // 고객 이름
|
||||||
|
// 날짜 필드
|
||||||
|
"order_date", // 발주일
|
||||||
|
"sales_date", // 수주일
|
||||||
|
"shipment_date", // 출하일
|
||||||
|
"receipt_date", // 입고일
|
||||||
|
"due_date", // 납기일
|
||||||
|
// 담당자/메모 필드
|
||||||
|
"manager", // 담당자
|
||||||
|
"memo", // 메모
|
||||||
|
"remark", // 비고
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const fieldName of masterDetailFields) {
|
||||||
|
const value = context.formData[fieldName];
|
||||||
|
if (value !== undefined && value !== "" && value !== null && !(fieldName in commonFields)) {
|
||||||
|
commonFields[fieldName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("📋 [handleSave] 최종 공통 필드 (마스터-디테일 필드 포함):", commonFields);
|
||||||
|
|
||||||
for (const item of parsedData) {
|
for (const item of parsedData) {
|
||||||
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
|
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue