169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
import { Response } from "express";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
|
|
/**
|
|
* 엔티티 검색 API
|
|
* GET /api/entity-search/:tableName
|
|
*/
|
|
export async function searchEntity(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { tableName } = req.params;
|
|
const {
|
|
searchText = "",
|
|
searchFields = "",
|
|
filterCondition = "{}",
|
|
page = "1",
|
|
limit = "20",
|
|
} = req.query;
|
|
|
|
// tableName 유효성 검증
|
|
if (!tableName || tableName === "undefined" || tableName === "null") {
|
|
logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName });
|
|
return res.status(400).json({
|
|
success: false,
|
|
message:
|
|
"테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.",
|
|
});
|
|
}
|
|
|
|
// 멀티테넌시
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
// 검색 필드 파싱
|
|
const requestedFields = searchFields
|
|
? (searchFields as string).split(",").map((f) => f.trim())
|
|
: [];
|
|
|
|
// 🆕 테이블의 실제 컬럼 목록 조회
|
|
const pool = getPool();
|
|
const columnsResult = await pool.query(
|
|
`SELECT column_name FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = $1`,
|
|
[tableName]
|
|
);
|
|
const existingColumns = new Set(columnsResult.rows.map((r: any) => r.column_name));
|
|
|
|
// 🆕 존재하는 컬럼만 필터링
|
|
const fields = requestedFields.filter((field) => {
|
|
if (existingColumns.has(field)) {
|
|
return true;
|
|
} else {
|
|
logger.warn(`엔티티 검색: 테이블 "${tableName}"에 컬럼 "${field}"이(가) 존재하지 않아 제외`);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
const existingColumnsArray = Array.from(existingColumns);
|
|
logger.info(`엔티티 검색 필드 확인 - 테이블: ${tableName}, 요청필드: [${requestedFields.join(", ")}], 유효필드: [${fields.join(", ")}], 테이블컬럼(샘플): [${existingColumnsArray.slice(0, 10).join(", ")}]`);
|
|
|
|
// WHERE 조건 생성
|
|
const whereConditions: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// 멀티테넌시 필터링
|
|
if (companyCode !== "*") {
|
|
// 🆕 company_code 컬럼이 있는 경우에만 필터링
|
|
if (existingColumns.has("company_code")) {
|
|
whereConditions.push(`company_code = $${paramIndex}`);
|
|
params.push(companyCode);
|
|
paramIndex++;
|
|
}
|
|
}
|
|
|
|
// 검색 조건
|
|
if (searchText) {
|
|
// 유효한 검색 필드가 없으면 기본 텍스트 컬럼에서 검색
|
|
let searchableFields = fields;
|
|
if (searchableFields.length === 0) {
|
|
// 기본 검색 컬럼: name, code, description 등 일반적인 컬럼명
|
|
const defaultSearchColumns = [
|
|
'name', 'code', 'description', 'title', 'label',
|
|
'item_name', 'item_code', 'item_number',
|
|
'equipment_name', 'equipment_code',
|
|
'inspection_item', 'consumable_name', // 소모품명 추가
|
|
'supplier_name', 'customer_name', 'product_name',
|
|
];
|
|
searchableFields = defaultSearchColumns.filter(col => existingColumns.has(col));
|
|
|
|
logger.info(`엔티티 검색: 기본 검색 필드 사용 - 테이블: ${tableName}, 검색필드: [${searchableFields.join(", ")}]`);
|
|
}
|
|
|
|
if (searchableFields.length > 0) {
|
|
const searchConditions = searchableFields.map((field) => {
|
|
const condition = `${field}::text ILIKE $${paramIndex}`;
|
|
paramIndex++;
|
|
return condition;
|
|
});
|
|
whereConditions.push(`(${searchConditions.join(" OR ")})`);
|
|
|
|
// 검색어 파라미터 추가
|
|
searchableFields.forEach(() => {
|
|
params.push(`%${searchText}%`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 추가 필터 조건 (존재하는 컬럼만)
|
|
const additionalFilter = JSON.parse(filterCondition as string);
|
|
for (const [key, value] of Object.entries(additionalFilter)) {
|
|
if (existingColumns.has(key)) {
|
|
whereConditions.push(`${key} = $${paramIndex}`);
|
|
params.push(value);
|
|
paramIndex++;
|
|
} else {
|
|
logger.warn("엔티티 검색: 필터 조건에 존재하지 않는 컬럼 제외", { tableName, key });
|
|
}
|
|
}
|
|
|
|
// 페이징
|
|
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
|
const whereClause =
|
|
whereConditions.length > 0
|
|
? `WHERE ${whereConditions.join(" AND ")}`
|
|
: "";
|
|
|
|
// 쿼리 실행 (pool은 위에서 이미 선언됨)
|
|
const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`;
|
|
const dataQuery = `
|
|
SELECT * FROM ${tableName} ${whereClause}
|
|
ORDER BY id DESC
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
`;
|
|
|
|
params.push(parseInt(limit as string));
|
|
params.push(offset);
|
|
|
|
const countResult = await pool.query(
|
|
countQuery,
|
|
params.slice(0, params.length - 2)
|
|
);
|
|
const dataResult = await pool.query(dataQuery, params);
|
|
|
|
logger.info("엔티티 검색 성공", {
|
|
tableName,
|
|
searchText,
|
|
companyCode,
|
|
rowCount: dataResult.rowCount,
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: dataResult.rows,
|
|
pagination: {
|
|
total: parseInt(countResult.rows[0].count),
|
|
page: parseInt(page as string),
|
|
limit: parseInt(limit as string),
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("엔티티 검색 오류", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|