feat: Introduce new date picker components for enhanced date selection

- Added `FormDatePicker` and `InlineCellDatePicker` components to provide flexible date selection options.
- Implemented a modernized date picker interface with calendar navigation, year selection, and time input capabilities.
- Enhanced `DateWidget` to support both date and datetime formats, improving user experience in date handling.
- Updated `CategoryColumnList` to group columns dynamically and manage expanded states for better organization.
- Improved `AlertDialog` z-index for better visibility in modal interactions.
- Refactored `ScreenModal` to ensure consistent modal behavior across the application.
This commit is contained in:
kjs
2026-02-26 17:32:20 +09:00
parent 27be48464a
commit 17d4cc297c
22 changed files with 2001 additions and 461 deletions

View File

@@ -3,7 +3,7 @@
import React, { useState, useEffect, useMemo } from "react";
import { apiClient } from "@/lib/api/client";
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { FolderTree, Loader2, Search, X } from "lucide-react";
import { ChevronRight, FolderTree, Loader2, Search, X } from "lucide-react";
import { Input } from "@/components/ui/input";
interface CategoryColumn {
@@ -30,6 +30,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
const [columns, setColumns] = useState<CategoryColumn[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
// 검색어로 필터링된 컬럼 목록
const filteredColumns = useMemo(() => {
@@ -49,6 +50,44 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
});
}, [columns, searchQuery]);
// 테이블별로 그룹화된 컬럼 목록
const groupedColumns = useMemo(() => {
const groups: { tableName: string; tableLabel: string; columns: CategoryColumn[] }[] = [];
const groupMap = new Map<string, CategoryColumn[]>();
for (const col of filteredColumns) {
const key = col.tableName;
if (!groupMap.has(key)) {
groupMap.set(key, []);
}
groupMap.get(key)!.push(col);
}
for (const [tblName, cols] of groupMap) {
groups.push({
tableName: tblName,
tableLabel: cols[0]?.tableLabel || tblName,
columns: cols,
});
}
return groups;
}, [filteredColumns]);
// 선택된 컬럼이 있는 그룹을 자동 펼침
useEffect(() => {
if (!selectedColumn) return;
const tableName = selectedColumn.split(".")[0];
if (tableName) {
setExpandedGroups((prev) => {
if (prev.has(tableName)) return prev;
const next = new Set(prev);
next.add(tableName);
return next;
});
}
}, [selectedColumn]);
useEffect(() => {
// 메뉴 종속 없이 항상 회사 기준으로 카테고리 컬럼 조회
loadCategoryColumnsByMenu();
@@ -279,35 +318,114 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,
)}
</div>
<div className="space-y-2">
<div className="space-y-1">
{filteredColumns.length === 0 && searchQuery ? (
<div className="text-muted-foreground py-4 text-center text-xs">
&apos;{searchQuery}&apos;
</div>
) : null}
{filteredColumns.map((column) => {
const uniqueKey = `${column.tableName}.${column.columnName}`;
const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교
return (
<div
key={uniqueKey}
onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)}
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
}`}
>
<div className="flex items-center gap-2">
<FolderTree
className={`h-4 w-4 ${isSelected ? "text-primary" : "text-muted-foreground"}`}
/>
<div className="flex-1">
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
<p className="text-muted-foreground text-xs">{column.tableLabel || column.tableName}</p>
</div>
<span className="text-muted-foreground text-xs font-medium">
{column.valueCount !== undefined ? `${column.valueCount}` : "..."}
</span>
{groupedColumns.map((group) => {
const isExpanded = expandedGroups.has(group.tableName);
const totalValues = group.columns.reduce((sum, c) => sum + (c.valueCount ?? 0), 0);
const hasSelectedInGroup = group.columns.some(
(c) => selectedColumn === `${c.tableName}.${c.columnName}`,
);
// 그룹이 1개뿐이면 드롭다운 없이 바로 표시
if (groupedColumns.length <= 1) {
return (
<div key={group.tableName} className="space-y-1.5">
{group.columns.map((column) => {
const uniqueKey = `${column.tableName}.${column.columnName}`;
const isSelected = selectedColumn === uniqueKey;
return (
<div
key={uniqueKey}
onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)}
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
}`}
>
<div className="flex items-center gap-2">
<FolderTree
className={`h-4 w-4 ${isSelected ? "text-primary" : "text-muted-foreground"}`}
/>
<div className="flex-1">
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
<p className="text-muted-foreground text-xs">{column.tableLabel || column.tableName}</p>
</div>
<span className="text-muted-foreground text-xs font-medium">
{column.valueCount !== undefined ? `${column.valueCount}` : "..."}
</span>
</div>
</div>
);
})}
</div>
);
}
return (
<div key={group.tableName} className="overflow-hidden rounded-lg border">
{/* 드롭다운 헤더 */}
<button
type="button"
onClick={() => {
setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(group.tableName)) {
next.delete(group.tableName);
} else {
next.add(group.tableName);
}
return next;
});
}}
className={`flex w-full items-center gap-2 px-3 py-2 text-left transition-colors ${
hasSelectedInGroup ? "bg-primary/5" : "hover:bg-muted/50"
}`}
>
<ChevronRight
className={`h-3.5 w-3.5 shrink-0 transition-transform duration-200 ${
isExpanded ? "rotate-90" : ""
} ${hasSelectedInGroup ? "text-primary" : "text-muted-foreground"}`}
/>
<span className={`flex-1 text-xs font-semibold ${hasSelectedInGroup ? "text-primary" : ""}`}>
{group.tableLabel}
</span>
<span className="text-muted-foreground text-[10px]">
{group.columns.length} / {totalValues}
</span>
</button>
{/* 펼쳐진 컬럼 목록 */}
{isExpanded && (
<div className="space-y-1 border-t px-2 py-2">
{group.columns.map((column) => {
const uniqueKey = `${column.tableName}.${column.columnName}`;
const isSelected = selectedColumn === uniqueKey;
return (
<div
key={uniqueKey}
onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)}
className={`cursor-pointer rounded-md px-3 py-1.5 transition-all ${
isSelected ? "bg-primary/10 font-semibold text-primary" : "hover:bg-muted/50"
}`}
>
<div className="flex items-center gap-2">
<FolderTree
className={`h-3.5 w-3.5 ${isSelected ? "text-primary" : "text-muted-foreground"}`}
/>
<span className="flex-1 text-xs">{column.columnLabel || column.columnName}</span>
<span className="text-muted-foreground text-[10px]">
{column.valueCount !== undefined ? `${column.valueCount}` : "..."}
</span>
</div>
</div>
);
})}
</div>
)}
</div>
);
})}