diff --git a/apps/cs/app/(with-base-layout)/recent/page.tsx b/apps/cs/app/(with-base-layout)/recent/page.tsx new file mode 100644 index 000000000..02ed5aba7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/recent/page.tsx @@ -0,0 +1,2 @@ +import { RecentPage } from "@/pages/recent"; +export default RecentPage; diff --git a/apps/cs/src/entities/board/api/board-service.ts b/apps/cs/src/entities/board/api/board-service.ts index 150ff677a..dc474b3ad 100644 --- a/apps/cs/src/entities/board/api/board-service.ts +++ b/apps/cs/src/entities/board/api/board-service.ts @@ -11,7 +11,7 @@ import type { } from "../model/board-interface"; export class BoardApiService implements BoardService { - public async getBoardList(params: BoardParams): Promise { + public async getBoardList(params?: BoardParams): Promise { const response = await client.get( `${ENDPOINT.BOARD_SERVICE.BBS}`, { params }, diff --git a/apps/cs/src/entities/post/api/post-service.ts b/apps/cs/src/entities/post/api/post-service.ts index 7820af709..471da5d81 100644 --- a/apps/cs/src/entities/post/api/post-service.ts +++ b/apps/cs/src/entities/post/api/post-service.ts @@ -4,12 +4,16 @@ import { postFormSchema, postUpdateFormSchema, type Post, + type MovePost, + type DeletePost, type PostInfo, type PostList, + type RecentPostList, type PostParams, - type PostService, + type RecentPostParams, type PostFormData, type PostUpdateFormData, + type PostService, } from "../model/post-interface"; export class PostApiService implements PostService { @@ -24,6 +28,16 @@ export class PostApiService implements PostService { return response.data; } + public async getRecentPostList( + params?: RecentPostParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`, + { params }, + ); + return response.data; + } + public async getPostInfo(bbsId: number, pstNo: number): Promise { const response = await client.get( `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, @@ -62,6 +76,22 @@ export class PostApiService implements PostService { params: { delRsn }, }); } + + public async softDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data); + } + + public async hardDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data); + } + + public async movePost(data: MovePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data); + } + + public async restorePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data); + } } export const PostApi = new PostApiService(); diff --git a/apps/cs/src/entities/post/index.ts b/apps/cs/src/entities/post/index.ts index d2b94ba84..0e19b54e2 100644 --- a/apps/cs/src/entities/post/index.ts +++ b/apps/cs/src/entities/post/index.ts @@ -3,9 +3,13 @@ export { postFormSchema, postUpdateFormSchema, type Post, + type RecentPost, + type MovePost, + type DeletePost, type PostList, type PostParams, - type PostService, + type RecentPostParams, type PostFormData, type PostUpdateFormData, + type PostService, } from "./model/post-interface"; diff --git a/apps/cs/src/entities/post/model/post-interface.ts b/apps/cs/src/entities/post/model/post-interface.ts index b848ccc6b..86e27afd5 100644 --- a/apps/cs/src/entities/post/model/post-interface.ts +++ b/apps/cs/src/entities/post/model/post-interface.ts @@ -11,6 +11,15 @@ export interface PostParams extends PageableParams { bbsTypeCd?: string; } +export interface RecentPostParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number | null; + delYn?: string; + frDateYmd?: string; + toDateYmd?: string; +} + export interface Post { rowNo: number; bbsId: number; @@ -33,6 +42,11 @@ export interface Post { cmntCnt: number; } +export interface RecentPost extends Post { + bbsNm: string; + bbsTypeCd: string; +} + export interface PostInfo extends Post { pstCn: string; rgtr: string; @@ -40,6 +54,18 @@ export interface PostInfo extends Post { mdfr: string; } +export interface MovePost { + fromBbsId: number; + toBbsId?: number; + pstNo: number; +} + +export interface DeletePost { + bbsId: number; + pstNo: number; + delRsn?: string; +} + const postEtcSchema = z.object({ bbsPstgTypeCd: z.string(), pstgPrdSeCd: z.string(), @@ -71,11 +97,13 @@ export const postUpdateFormSchema = postFormSchema.omit({ }); export type PostList = PageableList; +export type RecentPostList = PageableList; export type PostFormData = z.infer; export type PostUpdateFormData = z.infer; export interface PostService { getPostList(bbsId: number, params: PostParams): Promise; + getRecentPostList(params: RecentPostParams): Promise; getPostInfo(bbsId: number, pstNo: number): Promise; createPost(data: PostFormData): Promise; updatePost( @@ -84,4 +112,7 @@ export interface PostService { data: PostUpdateFormData, ): Promise; deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise; + softDeletePost(data: DeletePost[]): Promise; + hardDeletePost(data: DeletePost[]): Promise; + movePost(data: MovePost[]): Promise; } diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx index b742eee28..ac31ad0dc 100644 --- a/apps/cs/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -287,7 +287,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { {parseInt(rememberPeriodCode[site.siteId] ?? "") === 0 && ( + onValueChange={(date) => handlePeriodDateChange(site.siteId)( date as DateRange | undefined, ) diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx index 969c905de..24ffada45 100644 --- a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import dayjs from "dayjs"; +import React, { useEffect, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; import { @@ -23,8 +24,6 @@ import { AttachmentApi } from "@/entities/attachment"; import { PostApi, type PostParams } from "@/entities/post"; import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; -import dayjs from "dayjs"; -import React from "react"; function FaqPage({ bbsId }: { bbsId: string }) { const router = useRouter(); diff --git a/apps/cs/src/pages/recent/index.ts b/apps/cs/src/pages/recent/index.ts new file mode 100644 index 000000000..72b8579e1 --- /dev/null +++ b/apps/cs/src/pages/recent/index.ts @@ -0,0 +1 @@ +export { RecentPage } from "./ui/recent-page"; diff --git a/apps/cs/src/pages/recent/model/table-columns.tsx b/apps/cs/src/pages/recent/model/table-columns.tsx new file mode 100644 index 000000000..272a7d149 --- /dev/null +++ b/apps/cs/src/pages/recent/model/table-columns.tsx @@ -0,0 +1,117 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button, Checkbox } from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; +import type { RecentPost } from "@/entities/post"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + meta: { + width: 40, + }, + }, + { + accessorKey: "rowNo", + header: "NO", + meta: { + width: 80, + }, + }, + { + accessorKey: "bbsNm", + header: "게시판", + meta: { + width: 200, + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + cell: ({ row }) =>
{row.original.pstTtl}
, + }, + { + accessorKey: "atflCd", + header: "첨부파일", + cell: ({ row }) => { + const atflCd = row.original.atflCd; + if (!atflCd) return null; + return ; + }, + meta: { + width: 100, + }, + }, + { + accessorKey: "rgtrNm", + header: "작성자", + meta: { + width: 120, + }, + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + meta: { + width: 120, + }, + }, +]; + +const AttachmentButton = ({ atflCd }: { atflCd: string }) => { + const handleDownload = async (e: React.MouseEvent) => { + e.stopPropagation(); + try { + let filename = ""; + const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd); + const contentDisposition = response.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = contentDisposition.match( + /filename\*=UTF-8''([^;]+)/, + ); + if (filenameMatch) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); + } catch (error) { + console.error(error); + } + }; + + return ( + + ); +}; diff --git a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx new file mode 100644 index 000000000..64afef317 --- /dev/null +++ b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx @@ -0,0 +1,68 @@ +import React, { useRef } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import type { Board } from "@/entities/board"; +import type { MovePost } from "@/entities/post"; + +type MovePostDialogProps = { + selectedPosts: MovePost[]; + boardList: Board[]; + onMovePost: (data: MovePost[]) => void; +}; + +function MovePostDialog({ + selectedPosts, + boardList, + onMovePost, +}: MovePostDialogProps) { + const selectedPostsRef = useRef(selectedPosts); + + const onSelectValueChange = (toBbsId: string) => { + selectedPostsRef.current = selectedPosts.map((post) => ({ + ...post, + toBbsId: parseInt(toBbsId), + })); + }; + + return ( + + + + + +

+ 이동할 게시판을 선택해 주세요. +

+ + + +
+
+ ); +} + +export { MovePostDialog }; diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx new file mode 100644 index 000000000..407b90d49 --- /dev/null +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -0,0 +1,342 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DateRangePicker, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + DataTable, +} from "@common/components"; +import { + PostApi, + type DeletePost, + type MovePost, + type RecentPost, + type RecentPostParams, +} from "@/entities/post"; +import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; +import { BoardApi } from "@/entities/board"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/table-columns"; +import { MovePostDialog } from "./move-post-dialog"; + +type DateRange = { + from: Date | undefined; + to?: Date | undefined; +}; + +function RecentPage() { + const router = useRouter(); + const { prompt } = usePrompt(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const { pageMenuAuthority } = useMenuStore((state) => state); + const [dateRange, setDateRange] = useState(); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + const [recentPostParams, setRecentPostParams] = useState({ + keywordType: "", + keyword: "", + bbsId: null, + delYn: "N", + frDateYmd: "", + toDateYmd: "", + }); + const [isDeletePostData, setIsDeletePostData] = useState(false); + + const { data: recentPostList, refetch } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination], + queryFn: () => + PostApi.getRecentPostList({ + ...recentPostParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const { data: boardList } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS], + queryFn: () => BoardApi.getBoardList(), + }); + + const { mutate: movePost } = useMutation({ + mutationFn: (data: MovePost[]) => PostApi.movePost(data), + onSuccess: () => { + alert("이동이 완료되었습니다.", refetch); + }, + }); + + const table = useReactTable({ + data: recentPostList?.content ?? [], + columns, + state: { + pagination, + rowSelection, + }, + rowCount: recentPostList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setIsDeletePostData(recentPostParams.delYn === "Y"); + refetch(); + }; + + const handleRowClick = (row: RecentPost) => { + router.push( + `/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`, + ); + }; + + const handleRecentPostParamsChange = + (key: keyof RecentPostParams) => (value: unknown) => { + setRecentPostParams({ ...recentPostParams, [key]: value }); + }; + + const handlePresetDateRange = (value: string) => { + let presetDateRange: DateRange | undefined; + switch (value) { + case "1W": + presetDateRange = { + from: dayjs().subtract(7, "day").toDate(), + to: dayjs().toDate(), + }; + break; + case "1M": + presetDateRange = { + from: dayjs().subtract(30, "day").toDate(), + to: dayjs().toDate(), + }; + break; + case "3M": + presetDateRange = { + from: dayjs().subtract(90, "day").toDate(), + to: dayjs().toDate(), + }; + break; + default: + presetDateRange = undefined; + } + setDateRange(presetDateRange); + }; + + const onSoftDeletePost = async () => { + const { resolved, value: delRsn } = + await prompt("삭제 사유를 입력해주세요."); + if (!resolved) return; + + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + delRsn, + })); + PostApi.softDeletePost(selectedPosts); + alert("삭제가 완료되었습니다.", refetch); + setRowSelection({}); + }; + + const onHardDeletePost = async () => { + if ( + await confirm( + "게시물을 완전 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.", + ) + ) { + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + })); + PostApi.hardDeletePost(selectedPosts); + alert("삭제가 완료되었습니다.", refetch); + setRowSelection({}); + } + }; + + const onRestorePost = async () => { + if (await confirm("게시물을 복원하시겠습니까?")) { + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + })); + PostApi.restorePost(selectedPosts); + alert("복원이 완료되었습니다.", refetch); + setRowSelection({}); + } + }; + + return ( +
+
+
+ + +
+ +
+
+ + + + + + + + + + + handleRecentPostParamsChange("keyword")(e.target.value) + } + className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" + /> + +
+
+
+
+
+

콘텐츠 목록

+
+ {pageMenuAuthority?.mngAuthrtYn === "Y" && + (isDeletePostData ? ( + <> + + + + ) : ( + <> + ({ + fromBbsId: row.original.bbsId, + pstNo: row.original.pstNo, + }))} + boardList={boardList?.content ?? []} + onMovePost={movePost} + /> + + + ))} + +
+
+
+
+ +
+
+
+ ); +} + +export { RecentPage }; diff --git a/apps/cs/src/widgets/post/ui/posting-period-selector.tsx b/apps/cs/src/widgets/post/ui/posting-period-selector.tsx index 1673df8c9..9c23995a6 100644 --- a/apps/cs/src/widgets/post/ui/posting-period-selector.tsx +++ b/apps/cs/src/widgets/post/ui/posting-period-selector.tsx @@ -76,9 +76,7 @@ function PostingPeriodSelector({ ) )} - {periodCode === "0" && ( - - )} + {periodCode === "0" && } ); } diff --git a/packages/common/components/src/ui/button-group/button-group.stories.tsx b/packages/common/components/src/ui/button-group/button-group.stories.tsx index 17e2f3664..fa52df4be 100644 --- a/packages/common/components/src/ui/button-group/button-group.stories.tsx +++ b/packages/common/components/src/ui/button-group/button-group.stories.tsx @@ -2,11 +2,11 @@ import "./button-group.css"; import type { Meta, StoryObj } from "@storybook/react"; -import { RadioGroup, RadioGroupItem } from "../radio-group"; +import { ButtonGroup, ButtonGroupItem } from "../button-group"; -const meta: Meta = { +const meta: Meta = { title: "Elements/ButtonGroup", - component: RadioGroup, + component: ButtonGroup, parameters: { docs: { description: { @@ -19,49 +19,49 @@ ButtonGroup 컴포넌트는 RadioGroup 컴포넌트를 기반으로 버튼 모 }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const Secondary: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const Rounded: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const SecondaryRounded: Story = { render: () => ( - - Default - Comfortable - Compact - + Default + Comfortable + Compact + ), }; diff --git a/packages/common/components/src/ui/button-group/index.tsx b/packages/common/components/src/ui/button-group/index.tsx new file mode 100644 index 000000000..86d44b9c9 --- /dev/null +++ b/packages/common/components/src/ui/button-group/index.tsx @@ -0,0 +1,39 @@ +import "./button-group.css"; + +import * as React from "react"; +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; + +const ButtonGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + variant?: "radio" | "button"; + color?: "primary" | "secondary"; + shape?: "square" | "rounded"; + } +>( + ( + { variant = "radio", color = "primary", shape = "square", ...props }, + ref, + ) => { + return ( + + ); + }, +); +ButtonGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const ButtonGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + return ; +}); +ButtonGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { ButtonGroup, ButtonGroupItem }; diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index bcb072bb1..2b9879879 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -16,6 +16,7 @@ import type { HTMLAttributes } from "react"; declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { + width?: number; align?: "left" | "center" | "right"; rowSpan?: number; } diff --git a/packages/common/components/src/ui/date-range-picker/index.tsx b/packages/common/components/src/ui/date-range-picker/index.tsx index 2c99347a2..3572ffb7b 100644 --- a/packages/common/components/src/ui/date-range-picker/index.tsx +++ b/packages/common/components/src/ui/date-range-picker/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import type { DateRange } from "react-day-picker"; @@ -11,20 +11,38 @@ import { Calendar } from "../calendar"; import { Popover, PopoverContent, PopoverTrigger } from "../popover"; type DateRangePickerProps = React.HTMLAttributes & { + value?: DateRange; + defaultValue?: DateRange; disabled?: boolean; - handleDateChange?: (date: DateRange | undefined) => void; + onValueChange?: (date: DateRange | undefined) => void; }; export function DateRangePicker({ + value, + defaultValue, disabled, className, - handleDateChange, + children, + onValueChange, }: DateRangePickerProps) { - const [date, setDate] = useState(); + const [dateState, setDateState] = useState( + defaultValue, + ); + + const isControlled = value !== undefined; + const dateValue = isControlled ? value : dateState; + + useEffect(() => { + if (isControlled) { + setDateState(value); + } + }, [isControlled, value]); const handleSelect = (date: DateRange | undefined) => { - setDate(date); - handleDateChange?.(date); + if (!isControlled) { + setDateState(date); + } + onValueChange?.(date); }; return ( @@ -36,19 +54,19 @@ export function DateRangePicker({ variant="outline" className={cn( "h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent", - !date && "text-muted-foreground", + !dateValue && "text-muted-foreground", )} disabled={disabled} > - {date?.from ? ( - date.to ? ( + {dateValue?.from ? ( + dateValue.to ? ( <> - {format(date.from, "yyyy-MM-dd")} ~{" "} - {format(date.to, "yyyy-MM-dd")} + {format(dateValue.from, "yyyy-MM-dd")} ~{" "} + {format(dateValue.to, "yyyy-MM-dd")} ) : ( - format(date.from, "yyyy-MM-dd") + format(dateValue.from, "yyyy-MM-dd") ) ) : ( 기간 선택 @@ -56,14 +74,17 @@ export function DateRangePicker({ - +
+ {children} + +
diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 134b76d21..2e2cea547 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -1,6 +1,7 @@ export * from "./accordion"; export * from "./badge"; export * from "./button"; +export * from "./button-group"; export * from "./calendar"; export * from "./carousel"; export * from "./checkbox";