리포트 모달문제 수정
This commit is contained in:
parent
9040faa024
commit
b77fffbad7
|
|
@ -3,11 +3,11 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Printer, FileDown, FileText } from "lucide-react";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
|
|
@ -895,7 +895,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={isExporting}>
|
||||
닫기
|
||||
</Button>
|
||||
|
|
@ -911,7 +911,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
<FileText className="h-4 w-4" />
|
||||
{isExporting ? "생성 중..." : "WORD"}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useState } from "react";
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -131,7 +131,7 @@ export function SaveAsTemplateModal({ isOpen, onClose, onSave }: SaveAsTemplateM
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isSaving}>
|
||||
취소
|
||||
</Button>
|
||||
|
|
@ -145,7 +145,7 @@ export function SaveAsTemplateModal({ isOpen, onClose, onSave }: SaveAsTemplateM
|
|||
"저장"
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,13 +10,7 @@ import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnV
|
|||
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
||||
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||
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";
|
||||
|
||||
interface TableSearchWidgetProps {
|
||||
component: {
|
||||
|
|
@ -39,9 +33,11 @@ interface TableSearchWidgetProps {
|
|||
|
||||
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
||||
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
|
||||
|
||||
|
||||
// 높이 관리 context (실제 화면에서만 사용)
|
||||
let setWidgetHeight: ((screenId: number, componentId: string, height: number, originalHeight: number) => void) | undefined;
|
||||
let setWidgetHeight:
|
||||
| ((screenId: number, componentId: string, height: number, originalHeight: number) => void)
|
||||
| undefined;
|
||||
try {
|
||||
const heightContext = useTableSearchWidgetHeight();
|
||||
setWidgetHeight = heightContext.setWidgetHeight;
|
||||
|
|
@ -49,11 +45,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// Context가 없으면 (디자이너 모드) 무시
|
||||
setWidgetHeight = undefined;
|
||||
}
|
||||
|
||||
|
||||
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [groupingOpen, setGroupingOpen] = useState(false);
|
||||
|
||||
|
||||
// 활성화된 필터 목록
|
||||
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
||||
|
|
@ -61,7 +57,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
const [selectOptions, setSelectOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
||||
// 선택된 값의 라벨 저장 (데이터 없을 때도 라벨 유지)
|
||||
const [selectedLabels, setSelectedLabels] = useState<Record<string, string>>({});
|
||||
|
||||
|
||||
// 높이 감지를 위한 ref
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -75,16 +71,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// 첫 번째 테이블 자동 선택
|
||||
useEffect(() => {
|
||||
const tables = Array.from(registeredTables.values());
|
||||
|
||||
console.log("🔍 [TableSearchWidget] 테이블 감지:", {
|
||||
tablesCount: tables.length,
|
||||
tableIds: tables.map(t => t.tableId),
|
||||
selectedTableId,
|
||||
autoSelectFirstTable,
|
||||
});
|
||||
|
||||
|
||||
if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) {
|
||||
console.log("✅ [TableSearchWidget] 첫 번째 테이블 자동 선택:", tables[0].tableId);
|
||||
setSelectedTableId(tables[0].tableId);
|
||||
}
|
||||
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
||||
|
|
@ -94,7 +82,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
if (currentTable?.tableName) {
|
||||
const storageKey = `table_filters_${currentTable.tableName}`;
|
||||
const savedFilters = localStorage.getItem(storageKey);
|
||||
|
||||
|
||||
if (savedFilters) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedFilters) as Array<{
|
||||
|
|
@ -105,7 +93,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
filterType: "text" | "number" | "date" | "select";
|
||||
width?: number;
|
||||
}>;
|
||||
|
||||
|
||||
// enabled된 필터들만 activeFilters로 설정
|
||||
const activeFiltersList: TableFilter[] = parsed
|
||||
.filter((f) => f.enabled)
|
||||
|
|
@ -116,7 +104,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
filterType: f.filterType,
|
||||
width: f.width || 200, // 저장된 너비 포함
|
||||
}));
|
||||
|
||||
|
||||
setActiveFilters(activeFiltersList);
|
||||
} catch (error) {
|
||||
console.error("저장된 필터 불러오기 실패:", error);
|
||||
|
|
@ -132,20 +120,20 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
}
|
||||
|
||||
const loadSelectOptions = async () => {
|
||||
const selectFilters = activeFilters.filter(f => f.filterType === "select");
|
||||
|
||||
const selectFilters = activeFilters.filter((f) => f.filterType === "select");
|
||||
|
||||
if (selectFilters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...selectOptions };
|
||||
|
||||
|
||||
for (const filter of selectFilters) {
|
||||
// 이미 로드된 옵션이 있으면 스킵 (초기값 유지)
|
||||
if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const options = await currentTable.getColumnUniqueValues(filter.columnName);
|
||||
newOptions[filter.columnName] = options;
|
||||
|
|
@ -155,31 +143,30 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
}
|
||||
setSelectOptions(newOptions);
|
||||
};
|
||||
|
||||
|
||||
loadSelectOptions();
|
||||
}, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경
|
||||
|
||||
|
||||
// 높이 변화 감지 및 알림 (실제 화면에서만)
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !screenId || !setWidgetHeight) return;
|
||||
|
||||
|
||||
// 컴포넌트의 원래 높이 (디자이너에서 설정한 높이)
|
||||
const originalHeight = (component as any).size?.height || 50;
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const newHeight = entry.contentRect.height;
|
||||
|
||||
|
||||
// Context에 높이 저장 (다른 컴포넌트 위치 조정에 사용)
|
||||
setWidgetHeight(screenId, component.id, newHeight, originalHeight);
|
||||
|
||||
|
||||
// localStorage에 높이 저장 (새로고침 시 복원용)
|
||||
localStorage.setItem(
|
||||
`table_search_widget_height_screen_${screenId}_${component.id}`,
|
||||
JSON.stringify({ height: newHeight, originalHeight })
|
||||
JSON.stringify({ height: newHeight, originalHeight }),
|
||||
);
|
||||
|
||||
|
||||
// 콜백이 있으면 호출
|
||||
if (onHeightChange) {
|
||||
onHeightChange(newHeight);
|
||||
|
|
@ -197,10 +184,10 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// 화면 로딩 시 저장된 높이 복원
|
||||
useEffect(() => {
|
||||
if (!screenId || !setWidgetHeight) return;
|
||||
|
||||
|
||||
const storageKey = `table_search_widget_height_screen_${screenId}_${component.id}`;
|
||||
const savedData = localStorage.getItem(storageKey);
|
||||
|
||||
|
||||
if (savedData) {
|
||||
try {
|
||||
const { height, originalHeight } = JSON.parse(savedData);
|
||||
|
|
@ -219,9 +206,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
...filterValues,
|
||||
[columnName]: value,
|
||||
};
|
||||
|
||||
|
||||
setFilterValues(newValues);
|
||||
|
||||
|
||||
// 실시간 검색: 값 변경 시 즉시 필터 적용
|
||||
applyFilters(newValues);
|
||||
};
|
||||
|
|
@ -229,10 +216,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// 필터 적용 함수
|
||||
const applyFilters = (values: Record<string, string> = filterValues) => {
|
||||
// 빈 값이 아닌 필터만 적용
|
||||
const filtersWithValues = activeFilters.map((filter) => ({
|
||||
...filter,
|
||||
value: values[filter.columnName] || "",
|
||||
})).filter((f) => f.value !== "");
|
||||
const filtersWithValues = activeFilters
|
||||
.map((filter) => ({
|
||||
...filter,
|
||||
value: values[filter.columnName] || "",
|
||||
}))
|
||||
.filter((f) => f.value !== "");
|
||||
|
||||
currentTable?.onFilterChange(filtersWithValues);
|
||||
};
|
||||
|
|
@ -257,8 +246,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
type="date"
|
||||
value={value}
|
||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
||||
className="h-9 text-xs sm:text-sm focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }}
|
||||
className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||
placeholder={column?.columnLabel}
|
||||
/>
|
||||
);
|
||||
|
|
@ -269,37 +258,40 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
||||
className="h-9 text-xs sm:text-sm focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }}
|
||||
className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||
placeholder={column?.columnLabel}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select": {
|
||||
let options = selectOptions[filter.columnName] || [];
|
||||
|
||||
|
||||
// 현재 선택된 값이 옵션 목록에 없으면 추가 (데이터 없을 때도 선택값 유지)
|
||||
if (value && !options.find(opt => opt.value === value)) {
|
||||
if (value && !options.find((opt) => opt.value === value)) {
|
||||
const savedLabel = selectedLabels[filter.columnName] || value;
|
||||
options = [{ value, label: savedLabel }, ...options];
|
||||
}
|
||||
|
||||
|
||||
// 중복 제거 (value 기준)
|
||||
const uniqueOptions = options.reduce((acc, option) => {
|
||||
if (!acc.find(opt => opt.value === option.value)) {
|
||||
acc.push(option);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<{ value: string; label: string }>);
|
||||
|
||||
const uniqueOptions = options.reduce(
|
||||
(acc, option) => {
|
||||
if (!acc.find((opt) => opt.value === option.value)) {
|
||||
acc.push(option);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as Array<{ value: string; label: string }>,
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
// 선택한 값의 라벨 저장
|
||||
const selectedOption = uniqueOptions.find(opt => opt.value === val);
|
||||
const selectedOption = uniqueOptions.find((opt) => opt.value === val);
|
||||
if (selectedOption) {
|
||||
setSelectedLabels(prev => ({
|
||||
setSelectedLabels((prev) => ({
|
||||
...prev,
|
||||
[filter.columnName]: selectedOption.label,
|
||||
}));
|
||||
|
|
@ -307,17 +299,15 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
handleFilterChange(filter.columnName, val);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-9 min-h-9 text-xs sm:text-sm focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }}
|
||||
<SelectTrigger
|
||||
className="h-9 min-h-9 text-xs focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||
>
|
||||
<SelectValue placeholder={column?.columnLabel || "선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{uniqueOptions.length === 0 ? (
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
옵션 없음
|
||||
</div>
|
||||
<div className="text-muted-foreground px-2 py-1.5 text-xs">옵션 없음</div>
|
||||
) : (
|
||||
uniqueOptions.map((option, index) => (
|
||||
<SelectItem key={`${filter.columnName}-${option.value}-${index}`} value={option.value}>
|
||||
|
|
@ -336,8 +326,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
|
||||
className="h-9 text-xs sm:text-sm focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }}
|
||||
className="h-9 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
|
||||
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||
placeholder={column?.columnLabel}
|
||||
/>
|
||||
);
|
||||
|
|
@ -347,7 +337,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex w-full flex-wrap items-center gap-2 border-b bg-card"
|
||||
className="bg-card flex w-full flex-wrap items-center gap-2 border-b"
|
||||
style={{
|
||||
padding: component.style?.padding || "0.75rem",
|
||||
backgroundColor: component.style?.backgroundColor,
|
||||
|
|
@ -358,18 +348,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
{activeFilters.length > 0 && (
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||
{activeFilters.map((filter) => (
|
||||
<div key={filter.columnName}>
|
||||
{renderFilterInput(filter)}
|
||||
</div>
|
||||
<div key={filter.columnName}>{renderFilterInput(filter)}</div>
|
||||
))}
|
||||
|
||||
|
||||
{/* 초기화 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
className="h-9 shrink-0 text-xs sm:text-sm"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={handleResetFilters} className="h-9 shrink-0 text-xs sm:text-sm">
|
||||
<X className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
초기화
|
||||
</Button>
|
||||
|
|
@ -380,10 +363,10 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
{activeFilters.length === 0 && <div className="flex-1" />}
|
||||
|
||||
{/* 오른쪽: 데이터 건수 + 설정 버튼들 */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{/* 데이터 건수 표시 */}
|
||||
{currentTable?.dataCount !== undefined && (
|
||||
<div className="rounded-md bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground sm:text-sm">
|
||||
<div className="bg-muted text-muted-foreground rounded-md px-3 py-1.5 text-xs font-medium sm:text-sm">
|
||||
{currentTable.dataCount.toLocaleString()}건
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -423,12 +406,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
</div>
|
||||
|
||||
{/* 패널들 */}
|
||||
<ColumnVisibilityPanel
|
||||
isOpen={columnVisibilityOpen}
|
||||
onClose={() => setColumnVisibilityOpen(false)}
|
||||
/>
|
||||
<FilterPanel
|
||||
isOpen={filterOpen}
|
||||
<ColumnVisibilityPanel isOpen={columnVisibilityOpen} onClose={() => setColumnVisibilityOpen(false)} />
|
||||
<FilterPanel
|
||||
isOpen={filterOpen}
|
||||
onClose={() => setFilterOpen(false)}
|
||||
onFiltersApplied={(filters) => setActiveFilters(filters)}
|
||||
/>
|
||||
|
|
@ -436,4 +416,3 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue