diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 38fc77b1..173de022 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -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}`, diff --git a/frontend/components/screen/filters/ModernDatePicker.tsx b/frontend/components/screen/filters/ModernDatePicker.tsx index 55a9c64f..0a134927 100644 --- a/frontend/components/screen/filters/ModernDatePicker.tsx +++ b/frontend/components/screen/filters/ModernDatePicker.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react"; @@ -34,6 +34,17 @@ export const ModernDatePicker: React.FC = ({ label, value const [isOpen, setIsOpen] = useState(false); const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectingType, setSelectingType] = useState<"from" | "to">("from"); + + // ๋กœ์ปฌ ์ž„์‹œ ์ƒํƒœ (ํ™•์ธ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๊ธฐ ์ „๊นŒ์ง€ ์ž„์‹œ ์ €์žฅ) + const [tempValue, setTempValue] = useState(value || {}); + + // ํŒ์˜ค๋ฒ„๊ฐ€ ์—ด๋ฆด ๋•Œ ํ˜„์žฌ ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™” + useEffect(() => { + if (isOpen) { + setTempValue(value || {}); + setSelectingType("from"); + } + }, [isOpen, value]); const formatDate = (date: Date | undefined) => { if (!date) return ""; @@ -57,26 +68,91 @@ export const ModernDatePicker: React.FC = ({ label, value }; const handleDateClick = (date: Date) => { + // ๋กœ์ปฌ ์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ (onChange ํ˜ธ์ถœ ์•ˆ ํ•จ) if (selectingType === "from") { - const newValue = { ...value, from: date }; - onChange(newValue); + setTempValue({ ...tempValue, from: date }); setSelectingType("to"); } else { - const newValue = { ...value, to: date }; - onChange(newValue); + setTempValue({ ...tempValue, to: date }); setSelectingType("from"); } }; const handleClear = () => { - onChange({}); + setTempValue({}); setSelectingType("from"); }; 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); setSelectingType("from"); - // ๋‚ ์งœ๋Š” ์ด๋ฏธ ์„ ํƒ ์‹œ์ ์— onChange๊ฐ€ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ ์ค‘๋ณต ํ˜ธ์ถœ ์ œ๊ฑฐ }; const monthStart = startOfMonth(currentMonth); @@ -91,16 +167,16 @@ export const ModernDatePicker: React.FC = ({ label, value const allDays = [...Array(paddingDays).fill(null), ...days]; const isInRange = (date: Date) => { - if (!value.from || !value.to) return false; - return date >= value.from && date <= value.to; + if (!tempValue.from || !tempValue.to) return false; + return date >= tempValue.from && date <= tempValue.to; }; const isRangeStart = (date: Date) => { - return value.from && isSameDay(date, value.from); + return tempValue.from && isSameDay(date, tempValue.from); }; const isRangeEnd = (date: Date) => { - return value.to && isSameDay(date, value.to); + return tempValue.to && isSameDay(date, tempValue.to); }; return ( @@ -127,6 +203,25 @@ export const ModernDatePicker: React.FC = ({ label, value + {/* ๋น ๋ฅธ ์„ ํƒ ๋ฒ„ํŠผ */} +
+ + + + + +
+ {/* ์›” ๋„ค๋น„๊ฒŒ์ด์…˜ */}
{/* ์„ ํƒ๋œ ๋ฒ”์œ„ ํ‘œ์‹œ */} - {(value.from || value.to) && ( + {(tempValue.from || tempValue.to) && (
์„ ํƒ๋œ ๊ธฐ๊ฐ„
- {value.from && ์‹œ์ž‘: {formatDate(value.from)}} - {value.from && value.to && ~} - {value.to && ์ข…๋ฃŒ: {formatDate(value.to)}} + {tempValue.from && ์‹œ์ž‘: {formatDate(tempValue.from)}} + {tempValue.from && tempValue.to && ~} + {tempValue.to && ์ข…๋ฃŒ: {formatDate(tempValue.to)}}
)} @@ -200,7 +295,7 @@ export const ModernDatePicker: React.FC = ({ label, value ์ดˆ๊ธฐํ™”
-