"use client";
/**
* V2Group
*
* 통합 그룹 컴포넌트
* - tabs: 탭 그룹
* - accordion: 아코디언 그룹
* - section: 섹션 그룹
* - card-section: 카드 섹션
* - modal: 모달 그룹
* - form-modal: 폼 모달 그룹
*/
import React, { forwardRef, useState, useCallback } from "react";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { V2GroupProps, TabItem } from "@/types/v2-components";
import { ChevronDown, ChevronRight, X } from "lucide-react";
/**
* 탭 그룹 컴포넌트
*/
const TabsGroup = forwardRef<
HTMLDivElement,
{
tabs?: TabItem[];
activeTab?: string;
onTabChange?: (tabId: string) => void;
children?: React.ReactNode;
className?: string;
}
>(({ tabs = [], activeTab, onTabChange, children, className }, ref) => {
const [internalActiveTab, setInternalActiveTab] = useState(activeTab || tabs[0]?.id || "");
const currentTab = activeTab || internalActiveTab;
const handleTabChange = useCallback(
(tabId: string) => {
setInternalActiveTab(tabId);
onTabChange?.(tabId);
},
[onTabChange],
);
// 탭 정보가 있으면 탭 사용, 없으면 children 그대로 렌더링
if (tabs.length === 0) {
return (
{children}
);
}
return (
{tabs.map((tab) => (
{tab.title}
))}
{tabs.map((tab) => (
{tab.content || children}
))}
);
});
TabsGroup.displayName = "TabsGroup";
/**
* 아코디언 그룹 컴포넌트
*/
const AccordionGroup = forwardRef<
HTMLDivElement,
{
title?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
children?: React.ReactNode;
className?: string;
}
>(({ title, collapsible = true, defaultExpanded = true, children, className }, ref) => {
const [isOpen, setIsOpen] = useState(defaultExpanded);
if (!collapsible) {
return (
{title && (
{title}
)}
{children}
);
}
return (
{title || "그룹"}
{isOpen ? : }
{children}
);
});
AccordionGroup.displayName = "AccordionGroup";
/**
* 섹션 그룹 컴포넌트
*/
const SectionGroup = forwardRef<
HTMLDivElement,
{
title?: string;
description?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
children?: React.ReactNode;
className?: string;
}
>(({ title, description, collapsible = false, defaultExpanded = true, children, className }, ref) => {
const [isOpen, setIsOpen] = useState(defaultExpanded);
if (collapsible) {
return (
{title &&
{title}
}
{description &&
{description}
}
{isOpen ?
:
}
{children}
);
}
return (
{(title || description) && (
{title &&
{title}
}
{description &&
{description}
}
)}
{children}
);
});
SectionGroup.displayName = "SectionGroup";
/**
* 카드 섹션 그룹 컴포넌트
*/
const CardSectionGroup = forwardRef<
HTMLDivElement,
{
title?: string;
description?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
children?: React.ReactNode;
className?: string;
}
>(({ title, description, collapsible = false, defaultExpanded = true, children, className }, ref) => {
const [isOpen, setIsOpen] = useState(defaultExpanded);
if (collapsible) {
return (
{title && {title}}
{description && {description}}
{isOpen ?
:
}
{children}
);
}
return (
{(title || description) && (
{title && {title}}
{description && {description}}
)}
{children}
);
});
CardSectionGroup.displayName = "CardSectionGroup";
/**
* 모달 그룹 컴포넌트
*/
const ModalGroup = forwardRef<
HTMLDivElement,
{
title?: string;
description?: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
modalSize?: "sm" | "md" | "lg" | "xl";
children?: React.ReactNode;
className?: string;
}
>(({ title, description, open = false, onOpenChange, modalSize = "md", children, className }, ref) => {
const sizeClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
};
return (
);
});
ModalGroup.displayName = "ModalGroup";
/**
* 폼 모달 그룹 컴포넌트
*/
const FormModalGroup = forwardRef<
HTMLDivElement,
{
title?: string;
description?: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
modalSize?: "sm" | "md" | "lg" | "xl";
onSubmit?: () => void;
onCancel?: () => void;
submitLabel?: string;
cancelLabel?: string;
children?: React.ReactNode;
className?: string;
}
>(
(
{
title,
description,
open = false,
onOpenChange,
modalSize = "md",
onSubmit,
onCancel,
submitLabel = "저장",
cancelLabel = "취소",
children,
className,
},
ref,
) => {
const sizeClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
};
const handleCancel = useCallback(() => {
onCancel?.();
onOpenChange?.(false);
}, [onCancel, onOpenChange]);
const handleSubmit = useCallback(() => {
onSubmit?.();
}, [onSubmit]);
return (
);
},
);
FormModalGroup.displayName = "FormModalGroup";
/**
* 메인 V2Group 컴포넌트
*/
export const V2Group = forwardRef((props, ref) => {
const { id, style, size, config: configProp, children, open, onOpenChange } = props;
// config가 없으면 기본값 사용
const config = configProp || { type: "section" as const, tabs: [] };
// 타입별 그룹 렌더링
const renderGroup = () => {
const groupType = config.type || "section";
switch (groupType) {
case "tabs":
return (
{children}
);
case "accordion":
return (
{children}
);
case "section":
return (
{children}
);
case "card-section":
return (
{children}
);
case "modal":
return (
{children}
);
case "form-modal":
return (
{children}
);
default:
return {children};
}
};
const componentWidth = size?.width || style?.width;
const componentHeight = size?.height || style?.height;
return (
{renderGroup()}
);
});
V2Group.displayName = "V2Group";
export default V2Group;