feat: 날짜 기간 검색 기능 구현

- ModernDatePicker: 로컬 상태 관리로 즉시 검색 방지
  - tempValue 상태 추가하여 확인 버튼 클릭 시에만 검색 실행
  - 빠른 선택 버튼 추가 (오늘, 이번주, 이번달, 최근 7일, 최근 30일)

- TableSearchWidget: ModernDatePicker 통합
  - 기본 HTML input[type=date]를 ModernDatePicker로 교체
  - 날짜 범위 객체 {from, to}를 파이프 구분 문자열로 변환
  - 백엔드 재시작 없이 작동하도록 임시 포맷팅 적용

- tableManagementService: 날짜 범위 검색 로직 개선
  - getColumnWebTypeInfo: web_type이 null이면 input_type 폴백
  - buildDateRangeCondition: VARCHAR 타입 날짜 컬럼 지원
  - 날짜 컬럼을 ::date로 캐스팅하여 타입 호환성 확보
  - 파이프 구분 문자열 파싱 지원 (YYYY-MM-DD|YYYY-MM-DD)

- 디버깅 로깅 추가
  - 컬럼 타입 정보 조회 결과 로깅
  - 날짜 범위 검색 조건 생성 과정 추적
This commit is contained in:
kjs
2025-11-25 17:48:23 +09:00
parent 629be13816
commit ea88cfd043
3 changed files with 241 additions and 41 deletions

View File

@@ -1165,6 +1165,23 @@ export class TableManagementService {
paramCount: number;
} | null> {
try {
// 🔧 날짜 범위 문자열 "YYYY-MM-DD|YYYY-MM-DD" 체크 (최우선!)
if (typeof value === "string" && value.includes("|")) {
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
return this.buildDateRangeCondition(columnName, value, paramIndex);
}
}
// 🔧 날짜 범위 객체 {from, to} 체크
if (typeof value === "object" && value !== null && ("from" in value || "to" in value)) {
// 날짜 범위 객체는 그대로 전달
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
if (columnInfo && (columnInfo.webType === "date" || columnInfo.webType === "datetime")) {
return this.buildDateRangeCondition(columnName, value, paramIndex);
}
}
// 🔧 {value, operator} 형태의 필터 객체 처리
let actualValue = value;
let operator = "contains"; // 기본값
@@ -1193,6 +1210,12 @@ export class TableManagementService {
// 컬럼 타입 정보 조회
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
logger.info(`🔍 [buildAdvancedSearchCondition] ${tableName}.${columnName}`,
`webType=${columnInfo?.webType || 'NULL'}`,
`inputType=${columnInfo?.inputType || 'NULL'}`,
`actualValue=${JSON.stringify(actualValue)}`,
`operator=${operator}`
);
if (!columnInfo) {
// 컬럼 정보가 없으면 operator에 따른 기본 검색
@@ -1292,20 +1315,41 @@ export class TableManagementService {
const values: any[] = [];
let paramCount = 0;
if (typeof value === "object" && value !== null) {
// 문자열 형식의 날짜 범위 파싱 ("YYYY-MM-DD|YYYY-MM-DD")
if (typeof value === "string" && value.includes("|")) {
const [fromStr, toStr] = value.split("|");
if (fromStr && fromStr.trim() !== "") {
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
values.push(fromStr.trim());
paramCount++;
}
if (toStr && toStr.trim() !== "") {
// 종료일은 해당 날짜의 23:59:59까지 포함
conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
values.push(toStr.trim());
paramCount++;
}
}
// 객체 형식의 날짜 범위 ({from, to})
else if (typeof value === "object" && value !== null) {
if (value.from) {
conditions.push(`${columnName} >= $${paramIndex + paramCount}`);
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
values.push(value.from);
paramCount++;
}
if (value.to) {
conditions.push(`${columnName} <= $${paramIndex + paramCount}`);
// 종료일은 해당 날짜의 23:59:59까지 포함
conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
values.push(value.to);
paramCount++;
}
} else if (typeof value === "string" && value.trim() !== "") {
// 단일 날짜 검색 (해당 날짜의 데이터)
conditions.push(`DATE(${columnName}) = DATE($${paramIndex})`);
}
// 단일 날짜 검색
else if (typeof value === "string" && value.trim() !== "") {
conditions.push(`${columnName}::date = $${paramIndex}::date`);
values.push(value);
paramCount = 1;
}
@@ -1544,6 +1588,7 @@ export class TableManagementService {
columnName: string
): Promise<{
webType: string;
inputType?: string;
codeCategory?: string;
referenceTable?: string;
referenceColumn?: string;
@@ -1552,29 +1597,44 @@ export class TableManagementService {
try {
const result = await queryOne<{
web_type: string | null;
input_type: string | null;
code_category: string | null;
reference_table: string | null;
reference_column: string | null;
display_column: string | null;
}>(
`SELECT web_type, code_category, reference_table, reference_column, display_column
`SELECT web_type, input_type, code_category, reference_table, reference_column, display_column
FROM column_labels
WHERE table_name = $1 AND column_name = $2
LIMIT 1`,
[tableName, columnName]
);
logger.info(`🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`, {
found: !!result,
web_type: result?.web_type,
input_type: result?.input_type,
});
if (!result) {
logger.warn(`⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`);
return null;
}
return {
webType: result.web_type || "",
// web_type이 없으면 input_type을 사용 (레거시 호환)
const webType = result.web_type || result.input_type || "";
const columnInfo = {
webType: webType,
inputType: result.input_type || "",
codeCategory: result.code_category || undefined,
referenceTable: result.reference_table || undefined,
referenceColumn: result.reference_column || undefined,
displayColumn: result.display_column || undefined,
};
logger.info(`✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`);
return columnInfo;
} catch (error) {
logger.error(
`컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,