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:
@@ -1165,6 +1165,23 @@ export class TableManagementService {
|
|||||||
paramCount: number;
|
paramCount: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
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} 형태의 필터 객체 처리
|
// 🔧 {value, operator} 형태의 필터 객체 처리
|
||||||
let actualValue = value;
|
let actualValue = value;
|
||||||
let operator = "contains"; // 기본값
|
let operator = "contains"; // 기본값
|
||||||
@@ -1193,6 +1210,12 @@ export class TableManagementService {
|
|||||||
|
|
||||||
// 컬럼 타입 정보 조회
|
// 컬럼 타입 정보 조회
|
||||||
const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName);
|
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) {
|
if (!columnInfo) {
|
||||||
// 컬럼 정보가 없으면 operator에 따른 기본 검색
|
// 컬럼 정보가 없으면 operator에 따른 기본 검색
|
||||||
@@ -1292,20 +1315,41 @@ export class TableManagementService {
|
|||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
let paramCount = 0;
|
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) {
|
if (value.from) {
|
||||||
conditions.push(`${columnName} >= $${paramIndex + paramCount}`);
|
// VARCHAR 컬럼을 DATE로 캐스팅하여 비교
|
||||||
|
conditions.push(`${columnName}::date >= $${paramIndex + paramCount}::date`);
|
||||||
values.push(value.from);
|
values.push(value.from);
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
if (value.to) {
|
if (value.to) {
|
||||||
conditions.push(`${columnName} <= $${paramIndex + paramCount}`);
|
// 종료일은 해당 날짜의 23:59:59까지 포함
|
||||||
|
conditions.push(`${columnName}::date <= $${paramIndex + paramCount}::date`);
|
||||||
values.push(value.to);
|
values.push(value.to);
|
||||||
paramCount++;
|
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);
|
values.push(value);
|
||||||
paramCount = 1;
|
paramCount = 1;
|
||||||
}
|
}
|
||||||
@@ -1544,6 +1588,7 @@ export class TableManagementService {
|
|||||||
columnName: string
|
columnName: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
webType: string;
|
webType: string;
|
||||||
|
inputType?: string;
|
||||||
codeCategory?: string;
|
codeCategory?: string;
|
||||||
referenceTable?: string;
|
referenceTable?: string;
|
||||||
referenceColumn?: string;
|
referenceColumn?: string;
|
||||||
@@ -1552,29 +1597,44 @@ export class TableManagementService {
|
|||||||
try {
|
try {
|
||||||
const result = await queryOne<{
|
const result = await queryOne<{
|
||||||
web_type: string | null;
|
web_type: string | null;
|
||||||
|
input_type: string | null;
|
||||||
code_category: string | null;
|
code_category: string | null;
|
||||||
reference_table: string | null;
|
reference_table: string | null;
|
||||||
reference_column: string | null;
|
reference_column: string | null;
|
||||||
display_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
|
FROM column_labels
|
||||||
WHERE table_name = $1 AND column_name = $2
|
WHERE table_name = $1 AND column_name = $2
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[tableName, columnName]
|
[tableName, columnName]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info(`🔍 [getColumnWebTypeInfo] ${tableName}.${columnName} 조회 결과:`, {
|
||||||
|
found: !!result,
|
||||||
|
web_type: result?.web_type,
|
||||||
|
input_type: result?.input_type,
|
||||||
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
logger.warn(`⚠️ [getColumnWebTypeInfo] 컬럼 정보 없음: ${tableName}.${columnName}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// web_type이 없으면 input_type을 사용 (레거시 호환)
|
||||||
webType: result.web_type || "",
|
const webType = result.web_type || result.input_type || "";
|
||||||
|
|
||||||
|
const columnInfo = {
|
||||||
|
webType: webType,
|
||||||
|
inputType: result.input_type || "",
|
||||||
codeCategory: result.code_category || undefined,
|
codeCategory: result.code_category || undefined,
|
||||||
referenceTable: result.reference_table || undefined,
|
referenceTable: result.reference_table || undefined,
|
||||||
referenceColumn: result.reference_column || undefined,
|
referenceColumn: result.reference_column || undefined,
|
||||||
displayColumn: result.display_column || undefined,
|
displayColumn: result.display_column || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.info(`✅ [getColumnWebTypeInfo] 반환값: webType=${columnInfo.webType}, inputType=${columnInfo.inputType}`);
|
||||||
|
return columnInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,
|
`컬럼 웹타입 정보 조회 실패: ${tableName}.${columnName}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
|
import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
@@ -34,6 +34,17 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
const [selectingType, setSelectingType] = useState<"from" | "to">("from");
|
const [selectingType, setSelectingType] = useState<"from" | "to">("from");
|
||||||
|
|
||||||
|
// 로컬 임시 상태 (확인 버튼 누르기 전까지 임시 저장)
|
||||||
|
const [tempValue, setTempValue] = useState<DateRangeValue>(value || {});
|
||||||
|
|
||||||
|
// 팝오버가 열릴 때 현재 값으로 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setTempValue(value || {});
|
||||||
|
setSelectingType("from");
|
||||||
|
}
|
||||||
|
}, [isOpen, value]);
|
||||||
|
|
||||||
const formatDate = (date: Date | undefined) => {
|
const formatDate = (date: Date | undefined) => {
|
||||||
if (!date) return "";
|
if (!date) return "";
|
||||||
@@ -57,26 +68,91 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDateClick = (date: Date) => {
|
const handleDateClick = (date: Date) => {
|
||||||
|
// 로컬 상태만 업데이트 (onChange 호출 안 함)
|
||||||
if (selectingType === "from") {
|
if (selectingType === "from") {
|
||||||
const newValue = { ...value, from: date };
|
setTempValue({ ...tempValue, from: date });
|
||||||
onChange(newValue);
|
|
||||||
setSelectingType("to");
|
setSelectingType("to");
|
||||||
} else {
|
} else {
|
||||||
const newValue = { ...value, to: date };
|
setTempValue({ ...tempValue, to: date });
|
||||||
onChange(newValue);
|
|
||||||
setSelectingType("from");
|
setSelectingType("from");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onChange({});
|
setTempValue({});
|
||||||
setSelectingType("from");
|
setSelectingType("from");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
// 확인 버튼을 눌렀을 때만 onChange 호출
|
||||||
|
onChange(tempValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
// 취소 시 임시 값 버리고 팝오버 닫기
|
||||||
|
setTempValue(value || {});
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 빠른 기간 선택 함수들 (즉시 적용 + 팝오버 닫기)
|
||||||
|
const setToday = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const newValue = { from: today, to: today };
|
||||||
|
setTempValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThisWeek = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const dayOfWeek = today.getDay();
|
||||||
|
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // 월요일 기준
|
||||||
|
const monday = new Date(today);
|
||||||
|
monday.setDate(today.getDate() + diff);
|
||||||
|
const sunday = new Date(monday);
|
||||||
|
sunday.setDate(monday.getDate() + 6);
|
||||||
|
const newValue = { from: monday, to: sunday };
|
||||||
|
setTempValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThisMonth = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||||
|
const newValue = { from: firstDay, to: lastDay };
|
||||||
|
setTempValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLast7Days = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const sevenDaysAgo = new Date(today);
|
||||||
|
sevenDaysAgo.setDate(today.getDate() - 6);
|
||||||
|
const newValue = { from: sevenDaysAgo, to: today };
|
||||||
|
setTempValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectingType("from");
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLast30Days = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const thirtyDaysAgo = new Date(today);
|
||||||
|
thirtyDaysAgo.setDate(today.getDate() - 29);
|
||||||
|
const newValue = { from: thirtyDaysAgo, to: today };
|
||||||
|
setTempValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setSelectingType("from");
|
setSelectingType("from");
|
||||||
// 날짜는 이미 선택 시점에 onChange가 호출되므로 중복 호출 제거
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const monthStart = startOfMonth(currentMonth);
|
const monthStart = startOfMonth(currentMonth);
|
||||||
@@ -91,16 +167,16 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
const allDays = [...Array(paddingDays).fill(null), ...days];
|
const allDays = [...Array(paddingDays).fill(null), ...days];
|
||||||
|
|
||||||
const isInRange = (date: Date) => {
|
const isInRange = (date: Date) => {
|
||||||
if (!value.from || !value.to) return false;
|
if (!tempValue.from || !tempValue.to) return false;
|
||||||
return date >= value.from && date <= value.to;
|
return date >= tempValue.from && date <= tempValue.to;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRangeStart = (date: Date) => {
|
const isRangeStart = (date: Date) => {
|
||||||
return value.from && isSameDay(date, value.from);
|
return tempValue.from && isSameDay(date, tempValue.from);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRangeEnd = (date: Date) => {
|
const isRangeEnd = (date: Date) => {
|
||||||
return value.to && isSameDay(date, value.to);
|
return tempValue.to && isSameDay(date, tempValue.to);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -127,6 +203,25 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 빠른 선택 버튼 */}
|
||||||
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setToday}>
|
||||||
|
오늘
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setThisWeek}>
|
||||||
|
이번 주
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setThisMonth}>
|
||||||
|
이번 달
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setLast7Days}>
|
||||||
|
최근 7일
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={setLast30Days}>
|
||||||
|
최근 30일
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 월 네비게이션 */}
|
{/* 월 네비게이션 */}
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
|
<Button variant="ghost" size="sm" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
|
||||||
@@ -183,13 +278,13 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 선택된 범위 표시 */}
|
{/* 선택된 범위 표시 */}
|
||||||
{(value.from || value.to) && (
|
{(tempValue.from || tempValue.to) && (
|
||||||
<div className="bg-muted mb-4 rounded-md p-2">
|
<div className="bg-muted mb-4 rounded-md p-2">
|
||||||
<div className="text-muted-foreground mb-1 text-xs">선택된 기간</div>
|
<div className="text-muted-foreground mb-1 text-xs">선택된 기간</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{value.from && <span className="font-medium">시작: {formatDate(value.from)}</span>}
|
{tempValue.from && <span className="font-medium">시작: {formatDate(tempValue.from)}</span>}
|
||||||
{value.from && value.to && <span className="mx-2">~</span>}
|
{tempValue.from && tempValue.to && <span className="mx-2">~</span>}
|
||||||
{value.to && <span className="font-medium">종료: {formatDate(value.to)}</span>}
|
{tempValue.to && <span className="font-medium">종료: {formatDate(tempValue.to)}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -200,7 +295,7 @@ export const ModernDatePicker: React.FC<ModernDatePickerProps> = ({ label, value
|
|||||||
초기화
|
초기화
|
||||||
</Button>
|
</Button>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button variant="outline" size="sm" onClick={() => setIsOpen(false)}>
|
<Button variant="outline" size="sm" onClick={handleCancel}>
|
||||||
취소
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onClick={handleConfirm}>
|
<Button size="sm" onClick={handleConfirm}>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
|||||||
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||||
import { TableFilter } from "@/types/table-options";
|
import { TableFilter } from "@/types/table-options";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
||||||
|
|
||||||
interface PresetFilter {
|
interface PresetFilter {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -62,7 +63,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||||||
|
|
||||||
// 활성화된 필터 목록
|
// 활성화된 필터 목록
|
||||||
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
|
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
|
||||||
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
const [filterValues, setFilterValues] = useState<Record<string, any>>({});
|
||||||
// select 타입 필터의 옵션들
|
// select 타입 필터의 옵션들
|
||||||
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
||||||
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
|
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
|
||||||
@@ -230,7 +231,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||||||
const hasMultipleTables = tableList.length > 1;
|
const hasMultipleTables = tableList.length > 1;
|
||||||
|
|
||||||
// 필터 값 변경 핸들러
|
// 필터 값 변경 핸들러
|
||||||
const handleFilterChange = (columnName: string, value: string) => {
|
const handleFilterChange = (columnName: string, value: any) => {
|
||||||
const newValues = {
|
const newValues = {
|
||||||
...filterValues,
|
...filterValues,
|
||||||
[columnName]: value,
|
[columnName]: value,
|
||||||
@@ -243,14 +244,51 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 필터 적용 함수
|
// 필터 적용 함수
|
||||||
const applyFilters = (values: Record<string, string> = filterValues) => {
|
const applyFilters = (values: Record<string, any> = filterValues) => {
|
||||||
// 빈 값이 아닌 필터만 적용
|
// 빈 값이 아닌 필터만 적용
|
||||||
const filtersWithValues = activeFilters
|
const filtersWithValues = activeFilters
|
||||||
.map((filter) => ({
|
.map((filter) => {
|
||||||
...filter,
|
let filterValue = values[filter.columnName];
|
||||||
value: values[filter.columnName] || "",
|
|
||||||
}))
|
// 날짜 범위 객체를 처리
|
||||||
.filter((f) => f.value !== "");
|
if (filter.filterType === "date" && filterValue && typeof filterValue === "object" && (filterValue.from || filterValue.to)) {
|
||||||
|
// 날짜 범위 객체를 문자열 형식으로 변환 (백엔드 재시작 불필요)
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// "YYYY-MM-DD|YYYY-MM-DD" 형식으로 변환
|
||||||
|
const fromStr = filterValue.from ? formatDate(filterValue.from) : "";
|
||||||
|
const toStr = filterValue.to ? formatDate(filterValue.to) : "";
|
||||||
|
|
||||||
|
if (fromStr && toStr) {
|
||||||
|
// 둘 다 있으면 파이프로 연결
|
||||||
|
filterValue = `${fromStr}|${toStr}`;
|
||||||
|
} else if (fromStr) {
|
||||||
|
// 시작일만 있으면
|
||||||
|
filterValue = `${fromStr}|`;
|
||||||
|
} else if (toStr) {
|
||||||
|
// 종료일만 있으면
|
||||||
|
filterValue = `|${toStr}`;
|
||||||
|
} else {
|
||||||
|
filterValue = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
value: filterValue || "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((f) => {
|
||||||
|
// 빈 값 체크
|
||||||
|
if (!f.value) return false;
|
||||||
|
if (typeof f.value === "string" && f.value === "") return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
currentTable?.onFilterChange(filtersWithValues);
|
currentTable?.onFilterChange(filtersWithValues);
|
||||||
};
|
};
|
||||||
@@ -271,14 +309,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||||||
switch (filter.filterType) {
|
switch (filter.filterType) {
|
||||||
case "date":
|
case "date":
|
||||||
return (
|
return (
|
||||||
<Input
|
<div style={{ width: `${width}px` }}>
|
||||||
type="date"
|
<ModernDatePicker
|
||||||
value={value}
|
label={column?.columnLabel || filter.columnName}
|
||||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
value={value ? (typeof value === 'string' ? { from: new Date(value), to: new Date(value) } : value) : {}}
|
||||||
className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
onChange={(dateRange) => {
|
||||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
if (dateRange.from && dateRange.to) {
|
||||||
placeholder={column?.columnLabel}
|
// 기간이 선택되면 from과 to를 모두 저장
|
||||||
/>
|
handleFilterChange(filter.columnName, dateRange);
|
||||||
|
} else {
|
||||||
|
handleFilterChange(filter.columnName, "");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
includeTime={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
|
|||||||
Reference in New Issue
Block a user