142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { ChevronDown, ChevronRight, Minus } from "lucide-react";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { cn } from "@/lib/utils";
|
|
import { GroupState, TableGroupedConfig } from "../types";
|
|
|
|
interface GroupHeaderProps {
|
|
/** 그룹 상태 */
|
|
group: GroupState;
|
|
/** 설정 */
|
|
config: TableGroupedConfig;
|
|
/** 그룹 토글 핸들러 */
|
|
onToggle: () => void;
|
|
/** 그룹 선택 토글 핸들러 */
|
|
onSelectToggle?: () => void;
|
|
/** 그룹 헤더 스타일 */
|
|
style?: "default" | "compact" | "card";
|
|
/** 컬럼 개수 (colspan용) */
|
|
columnCount?: number;
|
|
}
|
|
|
|
/**
|
|
* 그룹 헤더 컴포넌트
|
|
* 그룹 펼치기/접기, 체크박스, 요약 정보 표시
|
|
*/
|
|
export function GroupHeader({
|
|
group,
|
|
config,
|
|
onToggle,
|
|
onSelectToggle,
|
|
style = "default",
|
|
columnCount = 1,
|
|
}: GroupHeaderProps) {
|
|
const { showCheckbox } = config;
|
|
const { summary } = group;
|
|
|
|
// 일부 선택 여부
|
|
const isIndeterminate =
|
|
group.selectedItemIds &&
|
|
group.selectedItemIds.length > 0 &&
|
|
group.selectedItemIds.length < group.items.length;
|
|
|
|
// 요약 텍스트 생성
|
|
const summaryText = React.useMemo(() => {
|
|
const parts: string[] = [];
|
|
|
|
// 개수
|
|
if (config.groupConfig?.summary?.showCount !== false) {
|
|
parts.push(`${summary.count}건`);
|
|
}
|
|
|
|
// 합계
|
|
if (summary.sum) {
|
|
for (const [col, value] of Object.entries(summary.sum)) {
|
|
const displayName =
|
|
config.columns?.find((c) => c.columnName === col)?.displayName || col;
|
|
parts.push(`${displayName}: ${value.toLocaleString()}`);
|
|
}
|
|
}
|
|
|
|
return parts.join(" | ");
|
|
}, [summary, config]);
|
|
|
|
// 스타일별 클래스
|
|
const headerClasses = cn(
|
|
"flex items-center gap-2 cursor-pointer select-none transition-colors",
|
|
{
|
|
// default 스타일
|
|
"px-3 py-2 bg-muted/50 hover:bg-muted border-b": style === "default",
|
|
// compact 스타일
|
|
"px-2 py-1 bg-muted/30 hover:bg-muted/50 border-b text-sm":
|
|
style === "compact",
|
|
// card 스타일
|
|
"px-4 py-3 bg-card border rounded-t-lg shadow-sm hover:shadow":
|
|
style === "card",
|
|
}
|
|
);
|
|
|
|
return (
|
|
<tr className="group-header-row">
|
|
<td
|
|
colSpan={columnCount}
|
|
className="p-0"
|
|
onClick={(e) => {
|
|
// 체크박스 클릭 시 토글 방지
|
|
if ((e.target as HTMLElement).closest('[data-checkbox="true"]')) {
|
|
return;
|
|
}
|
|
onToggle();
|
|
}}
|
|
>
|
|
<div className={headerClasses}>
|
|
{/* 펼치기/접기 아이콘 */}
|
|
<span className="flex-shrink-0 text-muted-foreground">
|
|
{group.expanded ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</span>
|
|
|
|
{/* 체크박스 */}
|
|
{showCheckbox && onSelectToggle && (
|
|
<span
|
|
data-checkbox="true"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onSelectToggle();
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={group.selected}
|
|
className={cn(
|
|
"data-[state=checked]:bg-primary",
|
|
isIndeterminate && "data-[state=checked]:bg-muted"
|
|
)}
|
|
/>
|
|
{isIndeterminate && (
|
|
<Minus className="absolute h-3 w-3 text-muted-foreground" />
|
|
)}
|
|
</span>
|
|
)}
|
|
|
|
{/* 그룹 라벨 */}
|
|
<span className="font-medium text-foreground">{group.groupLabel}</span>
|
|
|
|
{/* 요약 정보 */}
|
|
{summaryText && (
|
|
<span className="ml-auto text-sm text-muted-foreground">
|
|
{summaryText}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
export default GroupHeader;
|