lhj #376
|
|
@ -16,6 +16,7 @@ import {
|
||||||
PivotAreaType,
|
PivotAreaType,
|
||||||
AggregationType,
|
AggregationType,
|
||||||
FieldDataType,
|
FieldDataType,
|
||||||
|
DateGroupInterval,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -202,6 +203,28 @@ const AreaDropZone: React.FC<AreaDropZoneProps> = ({
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 행/열 영역에서 날짜 타입일 때 그룹화 옵션 */}
|
||||||
|
{(area === "row" || area === "column") && field.dataType === "date" && (
|
||||||
|
<Select
|
||||||
|
value={field.groupInterval || "__none__"}
|
||||||
|
onValueChange={(v) => onUpdateField(idx, {
|
||||||
|
groupInterval: v === "__none__" ? undefined : v as DateGroupInterval
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-16 text-xs">
|
||||||
|
<SelectValue placeholder="그룹" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">없음</SelectItem>
|
||||||
|
<SelectItem value="year">년</SelectItem>
|
||||||
|
<SelectItem value="quarter">분기</SelectItem>
|
||||||
|
<SelectItem value="month">월</SelectItem>
|
||||||
|
<SelectItem value="week">주</SelectItem>
|
||||||
|
<SelectItem value="day">일</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -295,7 +318,8 @@ export const PivotGridConfigPanel: React.FC<PivotGridConfigPanelProps> = ({
|
||||||
const mappedColumns: ColumnInfo[] = columnList.map((c: any) => ({
|
const mappedColumns: ColumnInfo[] = columnList.map((c: any) => ({
|
||||||
column_name: c.columnName || c.column_name,
|
column_name: c.columnName || c.column_name,
|
||||||
data_type: c.dataType || c.data_type || "text",
|
data_type: c.dataType || c.data_type || "text",
|
||||||
column_comment: c.columnLabel || c.column_label || c.columnName || c.column_name,
|
// 라벨 우선순위: displayName > comment > columnLabel > columnName
|
||||||
|
column_comment: c.displayName || c.comment || c.columnLabel || c.column_label || c.columnName || c.column_name,
|
||||||
}));
|
}));
|
||||||
setColumns(mappedColumns);
|
setColumns(mappedColumns);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ import {
|
||||||
FilterX,
|
FilterX,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Calendar,
|
||||||
|
Check,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -132,6 +134,16 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
// 필터 적용 여부 확인
|
// 필터 적용 여부 확인
|
||||||
const hasFilter = field.filterValues && field.filterValues.length > 0;
|
const hasFilter = field.filterValues && field.filterValues.length > 0;
|
||||||
const filterCount = field.filterValues?.length || 0;
|
const filterCount = field.filterValues?.length || 0;
|
||||||
|
|
||||||
|
// 그룹화 상태 확인
|
||||||
|
const hasGrouping = field.groupInterval && field.dataType === "date";
|
||||||
|
const groupLabels: Record<string, string> = {
|
||||||
|
year: "연도",
|
||||||
|
quarter: "분기",
|
||||||
|
month: "월",
|
||||||
|
week: "주",
|
||||||
|
day: "일",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -169,6 +181,12 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
<span className={cn("font-medium", hasFilter && "text-primary")}>
|
<span className={cn("font-medium", hasFilter && "text-primary")}>
|
||||||
{field.caption}
|
{field.caption}
|
||||||
</span>
|
</span>
|
||||||
|
{/* 그룹화 적용 표시 */}
|
||||||
|
{hasGrouping && (
|
||||||
|
<span className="bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 text-[10px] px-1 rounded">
|
||||||
|
{groupLabels[field.groupInterval!]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{/* 필터 적용 개수 배지 */}
|
{/* 필터 적용 개수 배지 */}
|
||||||
{hasFilter && (
|
{hasFilter && (
|
||||||
<span className="bg-primary text-primary-foreground text-[10px] px-1 rounded">
|
<span className="bg-primary text-primary-foreground text-[10px] px-1 rounded">
|
||||||
|
|
@ -224,6 +242,59 @@ const SortableFieldChip: React.FC<FieldChipProps> = ({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* 날짜 그룹화 옵션 (행/열 영역의 날짜 타입 필드만) */}
|
||||||
|
{(field.area === "row" || field.area === "column") &&
|
||||||
|
field.dataType === "date" && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
날짜 그룹화
|
||||||
|
</div>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: undefined })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{!field.groupInterval && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={!field.groupInterval ? "font-medium" : ""}>그룹화 없음</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: "year" })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{field.groupInterval === "year" && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={field.groupInterval === "year" ? "font-medium" : ""}>연도별</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: "quarter" })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{field.groupInterval === "quarter" && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={field.groupInterval === "quarter" ? "font-medium" : ""}>분기별</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: "month" })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{field.groupInterval === "month" && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={field.groupInterval === "month" ? "font-medium" : ""}>월별</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: "week" })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{field.groupInterval === "week" && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={field.groupInterval === "week" ? "font-medium" : ""}>주별</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSettingsChange?.({ ...field, groupInterval: "day" })}
|
||||||
|
className="pl-6"
|
||||||
|
>
|
||||||
|
{field.groupInterval === "day" && <Check className="h-3 w-3 mr-2" />}
|
||||||
|
<span className={field.groupInterval === "day" ? "font-medium" : ""}>일별</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onSettingsChange?.({
|
onSettingsChange?.({
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,7 @@ export interface PivotHeaderNode {
|
||||||
level: number; // 깊이
|
level: number; // 깊이
|
||||||
children?: PivotHeaderNode[]; // 자식 노드
|
children?: PivotHeaderNode[]; // 자식 노드
|
||||||
isExpanded: boolean; // 확장 상태
|
isExpanded: boolean; // 확장 상태
|
||||||
|
hasChildren: boolean; // 자식 존재 가능 여부 (다음 레벨 필드 있음)
|
||||||
path: string[]; // 경로 (드릴다운용)
|
path: string[]; // 경로 (드릴다운용)
|
||||||
subtotal?: PivotCellValue[]; // 소계
|
subtotal?: PivotCellValue[]; // 소계
|
||||||
span?: number; // colspan/rowspan
|
span?: number; // colspan/rowspan
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ function buildHeaderTree(
|
||||||
caption: key,
|
caption: key,
|
||||||
level: 0,
|
level: 0,
|
||||||
isExpanded: expandedPaths.has(pathKey),
|
isExpanded: expandedPaths.has(pathKey),
|
||||||
|
hasChildren: remainingFields.length > 0, // 다음 레벨 필드가 있으면 자식 있음
|
||||||
path: path,
|
path: path,
|
||||||
span: 1,
|
span: 1,
|
||||||
};
|
};
|
||||||
|
|
@ -195,6 +196,7 @@ function buildChildNodes(
|
||||||
caption: key,
|
caption: key,
|
||||||
level: level,
|
level: level,
|
||||||
isExpanded: expandedPaths.has(pathKey),
|
isExpanded: expandedPaths.has(pathKey),
|
||||||
|
hasChildren: remainingFields.length > 0, // 다음 레벨 필드가 있으면 자식 있음
|
||||||
path: path,
|
path: path,
|
||||||
span: 1,
|
span: 1,
|
||||||
};
|
};
|
||||||
|
|
@ -238,7 +240,7 @@ function flattenRows(nodes: PivotHeaderNode[]): PivotFlatRow[] {
|
||||||
level: node.level,
|
level: node.level,
|
||||||
caption: node.caption,
|
caption: node.caption,
|
||||||
isExpanded: node.isExpanded,
|
isExpanded: node.isExpanded,
|
||||||
hasChildren: !!(node.children && node.children.length > 0),
|
hasChildren: node.hasChildren, // 노드에서 직접 가져옴 (다음 레벨 필드 존재 여부 기준)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (node.isExpanded && node.children) {
|
if (node.isExpanded && node.children) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue