jongsultest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/index.d.mts +339 -0
  2. package/dist/index.d.ts +339 -0
  3. package/dist/index.js +1280 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +1234 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +59 -0
  8. package/src/atoms/Badge.stories.tsx +136 -0
  9. package/src/atoms/Badge.tsx +46 -0
  10. package/src/atoms/Button.stories.tsx +142 -0
  11. package/src/atoms/Button.tsx +50 -0
  12. package/src/atoms/ComboboxAutocomplete.stories.tsx +43 -0
  13. package/src/atoms/ComboboxAutocomplete.tsx +6 -0
  14. package/src/atoms/ComboboxSelect.stories.tsx +56 -0
  15. package/src/atoms/ComboboxSelect.tsx +6 -0
  16. package/src/atoms/DateTimePicker.stories.tsx +42 -0
  17. package/src/atoms/DateTimePicker.tsx +6 -0
  18. package/src/atoms/Icon.stories.tsx +86 -0
  19. package/src/atoms/Icon.tsx +81 -0
  20. package/src/atoms/IconButton.stories.tsx +59 -0
  21. package/src/atoms/IconButton.tsx +19 -0
  22. package/src/atoms/InputColor.stories.tsx +42 -0
  23. package/src/atoms/InputColor.tsx +6 -0
  24. package/src/atoms/InputDatePicker.stories.tsx +55 -0
  25. package/src/atoms/InputDatePicker.tsx +6 -0
  26. package/src/atoms/InputFile.stories.tsx +57 -0
  27. package/src/atoms/InputFile.tsx +15 -0
  28. package/src/atoms/InputNumber.stories.tsx +58 -0
  29. package/src/atoms/InputNumber.tsx +15 -0
  30. package/src/atoms/InputPassword.stories.tsx +42 -0
  31. package/src/atoms/InputPassword.tsx +24 -0
  32. package/src/atoms/InputRadio.stories.tsx +65 -0
  33. package/src/atoms/InputRadio.tsx +19 -0
  34. package/src/atoms/InputSegmentedControl.stories.tsx +44 -0
  35. package/src/atoms/InputSegmentedControl.tsx +15 -0
  36. package/src/atoms/InputSwitch.stories.tsx +45 -0
  37. package/src/atoms/InputSwitch.tsx +23 -0
  38. package/src/atoms/InputSwitchInTable.stories.tsx +43 -0
  39. package/src/atoms/InputSwitchInTable.tsx +35 -0
  40. package/src/atoms/InputText.stories.tsx +42 -0
  41. package/src/atoms/InputText.tsx +7 -0
  42. package/src/atoms/InputTextarea.stories.tsx +50 -0
  43. package/src/atoms/InputTextarea.tsx +6 -0
  44. package/src/atoms/index.ts +20 -0
  45. package/src/atoms/inputClassNames.ts +20 -0
  46. package/src/blocks/Accordion.stories.tsx +102 -0
  47. package/src/blocks/Accordion.tsx +124 -0
  48. package/src/blocks/AccordionDraggable.stories.tsx +169 -0
  49. package/src/blocks/AccordionDraggable.tsx +200 -0
  50. package/src/blocks/BoxContainer.stories.tsx +34 -0
  51. package/src/blocks/BoxContainer.tsx +16 -0
  52. package/src/blocks/DataTable.tsx +127 -0
  53. package/src/blocks/DescriptionRow.stories.tsx +34 -0
  54. package/src/blocks/DescriptionRow.tsx +22 -0
  55. package/src/blocks/EditorLayout.stories.tsx +79 -0
  56. package/src/blocks/EditorLayout.tsx +43 -0
  57. package/src/blocks/ImageUpload.tsx +292 -0
  58. package/src/blocks/LabeledField.stories.tsx +34 -0
  59. package/src/blocks/LabeledField.tsx +31 -0
  60. package/src/blocks/MediDrawer.stories.tsx +58 -0
  61. package/src/blocks/MediDrawer.tsx +34 -0
  62. package/src/blocks/Modal.stories.tsx +42 -0
  63. package/src/blocks/Modal.tsx +31 -0
  64. package/src/blocks/ModalDeleteConfirm.stories.tsx +31 -0
  65. package/src/blocks/ModalDeleteConfirm.tsx +56 -0
  66. package/src/blocks/ModalTokenExpired.tsx +52 -0
  67. package/src/blocks/NavigationBanner.tsx +100 -0
  68. package/src/blocks/PageHeader.tsx +24 -0
  69. package/src/blocks/PageLoading.stories.tsx +22 -0
  70. package/src/blocks/PageLoading.tsx +19 -0
  71. package/src/blocks/PageTitle.stories.tsx +39 -0
  72. package/src/blocks/PageTitle.tsx +25 -0
  73. package/src/blocks/Pagination.tsx +49 -0
  74. package/src/blocks/Panel.stories.tsx +84 -0
  75. package/src/blocks/Panel.tsx +97 -0
  76. package/src/blocks/SectionGroupWithTitle.stories.tsx +24 -0
  77. package/src/blocks/SectionGroupWithTitle.tsx +21 -0
  78. package/src/blocks/Table.tsx +114 -0
  79. package/src/blocks/TagInput.stories.tsx +74 -0
  80. package/src/blocks/TagInput.tsx +93 -0
  81. package/src/blocks/index.ts +25 -0
  82. package/src/index.ts +51 -0
  83. package/src/utils/file.ts +7 -0
@@ -0,0 +1,20 @@
1
+ export { Button } from "./Button";
2
+ export { InputText } from "./InputText";
3
+ export { InputPassword } from "./InputPassword";
4
+ export { InputNumber } from "./InputNumber";
5
+ export { InputColor } from "./InputColor";
6
+ export { InputFile } from "./InputFile";
7
+ export { InputDatePicker } from "./InputDatePicker";
8
+ export { InputTextarea } from "./InputTextarea";
9
+ export { InputRadio } from "./InputRadio";
10
+ export { InputSwitch } from "./InputSwitch";
11
+ export { InputSwitchInTable } from "./InputSwitchInTable";
12
+ export { InputSegmentedControl } from "./InputSegmentedControl";
13
+ export { ComboboxSelect } from "./ComboboxSelect";
14
+ export { ComboboxAutocomplete } from "./ComboboxAutocomplete";
15
+ export { DateTimePicker } from "./DateTimePicker";
16
+ export { Badge } from "./Badge";
17
+ export { IconButton } from "./IconButton";
18
+ export { PlusIcon, TrashIcon, GripVerticalIcon, ChevronDownIcon } from "./Icon";
19
+ export type { IconSvgProps } from "./Icon";
20
+ export { inputClassNames } from "./inputClassNames";
@@ -0,0 +1,20 @@
1
+ export const inputClassNames = {
2
+ input: `!rounded-[8px] !border-input
3
+ !text-sm !h-12
4
+
5
+ data-[error=true]:!border-error-line
6
+ data-[error=true]:!border-2
7
+ data-[error=true]:!bg-error-fill
8
+ data-[error=true]:!text-body
9
+
10
+ data-[disabled=true]:!text-body
11
+ data-[disabled=true]:!bg-disabled-input
12
+ data-[disabled=true]:!opacity-100
13
+ data-[disabled=true]:!border-default
14
+ `,
15
+ label: "!text-sm !text-[#999] !font-bold !tracking-tight !mb-2",
16
+ placeholder: "!text-sm !tracking-normal",
17
+ error: "!mt-2 !ml-2",
18
+ description: "!-mt-[.5px] !mb-2",
19
+ root: "!leading-0",
20
+ };
@@ -0,0 +1,102 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Accordion } from "./Accordion";
3
+ import { Badge, Text } from "@mantine/core";
4
+
5
+ const meta: Meta<typeof Accordion> = {
6
+ title: "Blocks/Accordion",
7
+ component: Accordion,
8
+ tags: ["autodocs"],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Accordion>;
13
+
14
+ const sampleItems = [
15
+ {
16
+ id: "1",
17
+ title: "시술 안내",
18
+ content: <Text size="sm">시술에 대한 상세 안내 내용입니다.</Text>,
19
+ },
20
+ {
21
+ id: "2",
22
+ title: "주의 사항",
23
+ content: <Text size="sm">시술 전후 주의 사항을 확인해주세요.</Text>,
24
+ },
25
+ {
26
+ id: "3",
27
+ title: "자주 묻는 질문",
28
+ content: <Text size="sm">FAQ 내용이 들어갑니다.</Text>,
29
+ },
30
+ ];
31
+
32
+ export const Default: Story = {
33
+ args: {
34
+ items: sampleItems,
35
+ },
36
+ };
37
+
38
+ export const SingleOpen: Story = {
39
+ args: {
40
+ items: sampleItems,
41
+ multiple: false,
42
+ },
43
+ };
44
+
45
+ export const WithDefaultOpen: Story = {
46
+ args: {
47
+ items: sampleItems,
48
+ defaultOpen: ["1", "3"],
49
+ },
50
+ };
51
+
52
+ export const WithIcon: Story = {
53
+ args: {
54
+ items: sampleItems.map((item, index) => ({
55
+ ...item,
56
+ icon: <Badge size="sm">{index + 1}</Badge>,
57
+ })),
58
+ },
59
+ };
60
+
61
+ export const WithRightSection: Story = {
62
+ args: {
63
+ items: sampleItems.map((item) => ({
64
+ ...item,
65
+ rightSection: (
66
+ <Badge variant="light" color="blue" size="sm">
67
+ NEW
68
+ </Badge>
69
+ ),
70
+ })),
71
+ },
72
+ };
73
+
74
+ export const WithDescription: Story = {
75
+ args: {
76
+ items: sampleItems.map((item) => ({
77
+ ...item,
78
+ description: "항목에 대한 부가 설명입니다.",
79
+ })),
80
+ },
81
+ };
82
+
83
+ export const WithDisabled: Story = {
84
+ args: {
85
+ items: [
86
+ ...sampleItems,
87
+ {
88
+ id: "4",
89
+ title: "비활성화된 항목",
90
+ content: <Text size="sm">이 항목은 열 수 없습니다.</Text>,
91
+ disabled: true,
92
+ },
93
+ ],
94
+ },
95
+ };
96
+
97
+ export const Contained: Story = {
98
+ args: {
99
+ items: sampleItems,
100
+ variant: "contained",
101
+ },
102
+ };
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+ import {
5
+ Accordion as MantineAccordion,
6
+ AccordionProps as MantineAccordionProps,
7
+ Text,
8
+ Group,
9
+ Stack,
10
+ } from "@mantine/core";
11
+ import { ChevronDownIcon } from "../atoms";
12
+
13
+ // ============================================
14
+ // Types
15
+ // ============================================
16
+
17
+ export interface AccordionItemData {
18
+ id: string;
19
+ title: string;
20
+ description?: string;
21
+ icon?: ReactNode;
22
+ rightSection?: ReactNode;
23
+ content: ReactNode;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ export interface AccordionProps {
28
+ items: AccordionItemData[];
29
+ defaultOpen?: string[];
30
+ multiple?: boolean;
31
+ variant?: MantineAccordionProps["variant"];
32
+ onChange?: (value: string | string[] | null) => void;
33
+ }
34
+
35
+ // ============================================
36
+ // Component
37
+ // ============================================
38
+
39
+ export function Accordion({
40
+ items,
41
+ defaultOpen = [],
42
+ multiple = true,
43
+ variant = "separated",
44
+ onChange,
45
+ }: AccordionProps) {
46
+ if (multiple) {
47
+ return (
48
+ <MantineAccordion
49
+ variant={variant}
50
+ multiple
51
+ defaultValue={defaultOpen}
52
+ chevron={<ChevronDownIcon />}
53
+ styles={accordionStyles}
54
+ onChange={onChange as (value: string[]) => void}
55
+ classNames={{ label: "!py-4" }}
56
+ >
57
+ {items.map((item) => (
58
+ <AccordionItem key={item.id} item={item} />
59
+ ))}
60
+ </MantineAccordion>
61
+ );
62
+ }
63
+
64
+ return (
65
+ <MantineAccordion
66
+ variant={variant}
67
+ defaultValue={defaultOpen[0]}
68
+ chevron={<ChevronDownIcon />}
69
+ styles={accordionStyles}
70
+ onChange={onChange as (value: string | null) => void}
71
+ >
72
+ {items.map((item) => (
73
+ <AccordionItem key={item.id} item={item} />
74
+ ))}
75
+ </MantineAccordion>
76
+ );
77
+ }
78
+
79
+ // ============================================
80
+ // Sub Components
81
+ // ============================================
82
+
83
+ function AccordionItem({ item }: { item: AccordionItemData }) {
84
+ return (
85
+ <MantineAccordion.Item value={item.id}>
86
+ <MantineAccordion.Control disabled={item.disabled} icon={item.icon}>
87
+ <Group justify="space-between" wrap="nowrap" style={{ flex: 1 }}>
88
+ <Text fw={500}>{item.title}</Text>
89
+ {item.rightSection && <div onClick={(e) => e.stopPropagation()}>{item.rightSection}</div>}
90
+ </Group>
91
+ </MantineAccordion.Control>
92
+ <MantineAccordion.Panel>
93
+ <Stack gap="md">
94
+ {item.description && (
95
+ <Text size="sm" c="dimmed">
96
+ {item.description}
97
+ </Text>
98
+ )}
99
+ {item.content}
100
+ </Stack>
101
+ </MantineAccordion.Panel>
102
+ </MantineAccordion.Item>
103
+ );
104
+ }
105
+
106
+ // ============================================
107
+ // Shared Styles
108
+ // ============================================
109
+
110
+ const accordionStyles = {
111
+ control: { padding: "17px 24px" },
112
+ item: { "--item-filled-color": "#F3F9FF" },
113
+ };
114
+
115
+ // ============================================
116
+ // Compound Component (유연한 사용을 위한 대안)
117
+ // ============================================
118
+
119
+ Accordion.Root = ((props: MantineAccordionProps) => (
120
+ <MantineAccordion chevron={<ChevronDownIcon />} styles={accordionStyles} {...props} />
121
+ )) as typeof MantineAccordion;
122
+ Accordion.Item = MantineAccordion.Item;
123
+ Accordion.Control = MantineAccordion.Control;
124
+ Accordion.Panel = MantineAccordion.Panel;
@@ -0,0 +1,169 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { AccordionDraggable, AccordionDraggableItem } from "./AccordionDraggable";
3
+ import { InputText, InputTextarea } from "../atoms";
4
+ import { Stack, Text } from "@mantine/core";
5
+ import { DropResult } from "@hello-pangea/dnd";
6
+ import { useState } from "react";
7
+
8
+ const meta: Meta<typeof AccordionDraggable> = {
9
+ title: "Blocks/AccordionDraggable",
10
+ component: AccordionDraggable,
11
+ tags: ["autodocs"],
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof AccordionDraggable>;
16
+
17
+ const sampleItems: AccordionDraggableItem[] = [
18
+ {
19
+ id: "1",
20
+ title: "이름",
21
+ content: (
22
+ <Stack gap="sm">
23
+ <InputText label="라벨명" placeholder="라벨명을 입력하세요" />
24
+ </Stack>
25
+ ),
26
+ isRequired: true,
27
+ isUsed: true,
28
+ },
29
+ {
30
+ id: "2",
31
+ title: "전화번호",
32
+ content: (
33
+ <Stack gap="sm">
34
+ <InputText label="라벨명" placeholder="라벨명을 입력하세요" />
35
+ </Stack>
36
+ ),
37
+ isRequired: true,
38
+ isUsed: true,
39
+ },
40
+ {
41
+ id: "3",
42
+ title: "상담 내용",
43
+ content: (
44
+ <Stack gap="sm">
45
+ <InputTextarea label="설명" placeholder="설명을 입력하세요" />
46
+ </Stack>
47
+ ),
48
+ isRequired: false,
49
+ isUsed: false,
50
+ },
51
+ ];
52
+
53
+ const handleReorder = (result: DropResult) => {
54
+ console.log("reorder", result);
55
+ };
56
+
57
+ function DefaultExample() {
58
+ const [items, setItems] = useState<AccordionDraggableItem[]>(sampleItems);
59
+ const [activeId, setActiveId] = useState<string | null>(null);
60
+
61
+ const handleInteractiveReorder = (result: DropResult) => {
62
+ if (!result.destination) return;
63
+ const reordered = [...items];
64
+ const [moved] = reordered.splice(result.source.index, 1);
65
+ reordered.splice(result.destination.index, 0, moved);
66
+ setItems(reordered);
67
+ };
68
+
69
+ return (
70
+ <AccordionDraggable
71
+ items={items}
72
+ activeId={activeId}
73
+ onActiveChange={setActiveId}
74
+ onReorder={handleInteractiveReorder}
75
+ />
76
+ );
77
+ }
78
+
79
+ export const Default: Story = {
80
+ render: () => <DefaultExample />,
81
+ };
82
+
83
+ export const WithDeleteButton: Story = {
84
+ args: {
85
+ items: sampleItems,
86
+ onReorder: handleReorder,
87
+ showDeleteButton: true,
88
+ onDelete: (id: string) => console.log("delete", id),
89
+ },
90
+ };
91
+
92
+ export const WithToggles: Story = {
93
+ args: {
94
+ items: sampleItems,
95
+ onReorder: handleReorder,
96
+ showDeleteButton: false,
97
+ showRequiredToggle: true,
98
+ showUsedToggle: true,
99
+ onRequiredChange: (id: string, value: boolean) => console.log("required", id, value),
100
+ onUsedChange: (id: string, value: boolean) => console.log("used", id, value),
101
+ },
102
+ };
103
+
104
+ export const WithRightSection: Story = {
105
+ args: {
106
+ items: sampleItems.map((item) => ({
107
+ ...item,
108
+ rightSection: (
109
+ <Text size="xs" c="dimmed">
110
+ 커스텀 영역
111
+ </Text>
112
+ ),
113
+ })),
114
+ onReorder: handleReorder,
115
+ showDeleteButton: false,
116
+ },
117
+ };
118
+
119
+ export const Empty: Story = {
120
+ args: {
121
+ items: [],
122
+ onReorder: handleReorder,
123
+ emptyMessage: "등록된 항목이 없습니다",
124
+ },
125
+ };
126
+
127
+ function InteractiveExample() {
128
+ const [items, setItems] = useState<AccordionDraggableItem[]>(sampleItems);
129
+ const [activeId, setActiveId] = useState<string | null>(null);
130
+
131
+ const handleInteractiveReorder = (result: DropResult) => {
132
+ if (!result.destination) return;
133
+ const reordered = [...items];
134
+ const [moved] = reordered.splice(result.source.index, 1);
135
+ reordered.splice(result.destination.index, 0, moved);
136
+ setItems(reordered);
137
+ };
138
+
139
+ const handleDelete = (id: string) => {
140
+ setItems((prev) => prev.filter((item) => item.id !== id));
141
+ };
142
+
143
+ const handleRequiredChange = (id: string, value: boolean) => {
144
+ setItems((prev) => prev.map((item) => (item.id === id ? { ...item, isRequired: value } : item)));
145
+ };
146
+
147
+ const handleUsedChange = (id: string, value: boolean) => {
148
+ setItems((prev) => prev.map((item) => (item.id === id ? { ...item, isUsed: value } : item)));
149
+ };
150
+
151
+ return (
152
+ <AccordionDraggable
153
+ items={items}
154
+ activeId={activeId}
155
+ onActiveChange={setActiveId}
156
+ onReorder={handleInteractiveReorder}
157
+ onDelete={handleDelete}
158
+ onRequiredChange={handleRequiredChange}
159
+ onUsedChange={handleUsedChange}
160
+ showDeleteButton={true}
161
+ showRequiredToggle={true}
162
+ showUsedToggle={true}
163
+ />
164
+ );
165
+ }
166
+
167
+ export const Interactive: Story = {
168
+ render: () => <InteractiveExample />,
169
+ };
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import { GripVerticalIcon, TrashIcon } from "../atoms";
4
+ import { Accordion } from ".";
5
+ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
6
+ import { Box, Checkbox, Group, Stack, Text } from "@mantine/core";
7
+ import React, { ReactNode, useEffect, useState } from "react";
8
+
9
+ // ============================================
10
+ // Types
11
+ // ============================================
12
+
13
+ export interface AccordionDraggableItem {
14
+ id: string;
15
+ title: string;
16
+ content: ReactNode;
17
+ icon?: ReactNode;
18
+ rightSection?: ReactNode;
19
+ isRequired?: boolean;
20
+ isUsed?: boolean;
21
+ }
22
+
23
+ export interface AccordionDraggableProps {
24
+ items: AccordionDraggableItem[];
25
+ droppableId?: string;
26
+ activeId?: string | null;
27
+ onActiveChange?: (id: string | null) => void;
28
+ onReorder: (result: DropResult) => void;
29
+ onDelete?: (id: string) => void;
30
+ onRequiredChange?: (id: string, value: boolean) => void;
31
+ onUsedChange?: (id: string, value: boolean) => void;
32
+ emptyMessage?: string;
33
+ showDeleteButton?: boolean;
34
+ showRequiredToggle?: boolean;
35
+ showUsedToggle?: boolean;
36
+ draggingBgColor?: string;
37
+ }
38
+
39
+ // ============================================
40
+ // Droppable wrapper — 마운트 후 활성화하여 hydration 불일치 방지
41
+ // ============================================
42
+
43
+ function StrictModeDroppable({ children, ...props }: React.ComponentProps<typeof Droppable>) {
44
+ const [isEnabled, setIsEnabled] = useState(false);
45
+
46
+ useEffect(() => {
47
+ const animation = requestAnimationFrame(() => setIsEnabled(true));
48
+ return () => {
49
+ cancelAnimationFrame(animation);
50
+ setIsEnabled(false);
51
+ };
52
+ }, []);
53
+
54
+ if (!isEnabled) return null;
55
+
56
+ return <Droppable {...props}>{children}</Droppable>;
57
+ }
58
+
59
+ // ============================================
60
+ // Component
61
+ // ============================================
62
+
63
+ export function AccordionDraggable({
64
+ items,
65
+ droppableId = "draggable-accordion",
66
+ activeId = null,
67
+ onActiveChange,
68
+ onReorder,
69
+ onDelete,
70
+ onRequiredChange,
71
+ onUsedChange,
72
+ emptyMessage = "항목이 없습니다",
73
+ showDeleteButton = true,
74
+ showRequiredToggle = false,
75
+ showUsedToggle = false,
76
+ draggingBgColor = "var(--color-dragging)",
77
+ }: AccordionDraggableProps) {
78
+ if (items.length === 0) {
79
+ return (
80
+ <Box style={{ padding: 40, textAlign: "center" }}>
81
+ <Text c="dimmed">{emptyMessage}</Text>
82
+ </Box>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <DragDropContext onDragEnd={onReorder}>
88
+ <StrictModeDroppable droppableId={droppableId}>
89
+ {(provided) => (
90
+ <Box ref={provided.innerRef} {...provided.droppableProps}>
91
+ <Accordion.Root
92
+ variant="separated"
93
+ value={activeId}
94
+ onChange={onActiveChange}
95
+ classNames={{ label: "!py-0", content: "!p-6 !pb-10" }}
96
+ >
97
+ {items.map((item, index) => (
98
+ <Draggable key={item.id} draggableId={item.id} index={index}>
99
+ {(provided) => (
100
+ <Box
101
+ ref={provided.innerRef}
102
+ {...provided.draggableProps}
103
+ style={{
104
+ ...provided.draggableProps.style,
105
+ marginBottom: 8,
106
+ }}
107
+ >
108
+ <Accordion.Item
109
+ value={item.id}
110
+ data-active={activeId === item.id || undefined}
111
+ style={{ backgroundColor: draggingBgColor }}
112
+ className="group data-[active]:!bg-[var(--color-active,transparent)]"
113
+ >
114
+ <Accordion.Control
115
+ icon={item.icon}
116
+ className="group-data-[active]:!border-b group-data-[active]:!border-gray-200"
117
+ >
118
+ <Group gap="xs" wrap="nowrap" style={{ width: "100%" }}>
119
+ {/* 드래그 핸들 */}
120
+ <span {...provided.dragHandleProps} onClick={(e) => e.stopPropagation()}>
121
+ <GripVerticalIcon />
122
+ </span>
123
+
124
+ {/* 제목 */}
125
+ <Text fw={500} className="flex-1 !text-[15px]">
126
+ {item.title}
127
+ </Text>
128
+
129
+ {/* 오른쪽 섹션 */}
130
+ {item.rightSection && (
131
+ <Box
132
+ onClick={(e) => e.stopPropagation()}
133
+ style={{
134
+ marginRight: 16,
135
+ padding: "4px 8px",
136
+ borderRadius: 4,
137
+ backgroundColor: "var(--color-right-section)",
138
+ }}
139
+ >
140
+ {item.rightSection}
141
+ </Box>
142
+ )}
143
+
144
+ {/* 사용/필수 토글 */}
145
+ {(showRequiredToggle || showUsedToggle) && !item.rightSection && (
146
+ <Group
147
+ gap="sm"
148
+ wrap="nowrap"
149
+ className="mr-6 h-5 border-r border-[#E1E4E8] pr-6"
150
+ onClick={(e) => e.stopPropagation()}
151
+ >
152
+ {/* 사용 토글 */}
153
+ {showUsedToggle && onUsedChange && (
154
+ <Checkbox
155
+ checked={item.isUsed ?? false}
156
+ onChange={(e) => onUsedChange(item.id, e.currentTarget.checked)}
157
+ label="사용"
158
+ />
159
+ )}
160
+ {/* 필수 토글 */}
161
+ {showRequiredToggle && onRequiredChange && (
162
+ <Checkbox
163
+ checked={item.isRequired ?? false}
164
+ onChange={(e) => onRequiredChange(item.id, e.currentTarget.checked)}
165
+ label="필수"
166
+ />
167
+ )}
168
+ </Group>
169
+ )}
170
+
171
+ {/* 삭제 버튼 */}
172
+ {showDeleteButton && onDelete && !item.rightSection && (
173
+ <span
174
+ className="mr-6 h-5 border-r border-[#E1E4E8] pr-6"
175
+ onClick={(e) => {
176
+ e.stopPropagation();
177
+ onDelete(item.id);
178
+ }}
179
+ >
180
+ <TrashIcon color="red" />
181
+ </span>
182
+ )}
183
+ </Group>
184
+ </Accordion.Control>
185
+ <Accordion.Panel>
186
+ <Stack gap="24">{item.content}</Stack>
187
+ </Accordion.Panel>
188
+ </Accordion.Item>
189
+ </Box>
190
+ )}
191
+ </Draggable>
192
+ ))}
193
+ {provided.placeholder}
194
+ </Accordion.Root>
195
+ </Box>
196
+ )}
197
+ </StrictModeDroppable>
198
+ </DragDropContext>
199
+ );
200
+ }
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import BoxContainer from "./BoxContainer";
3
+ import { Text } from "@mantine/core";
4
+
5
+ const meta: Meta<typeof BoxContainer> = {
6
+ title: "Blocks/BoxContainer",
7
+ component: BoxContainer,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ layout: "fullscreen",
11
+ },
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof BoxContainer>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ children: <Text>기본 컨텐츠 영역입니다.</Text>,
20
+ },
21
+ };
22
+
23
+ export const WithMultipleChildren: Story = {
24
+ args: {
25
+ children: (
26
+ <>
27
+ <Text fw={700} size="xl" mb="md">
28
+ 페이지 제목
29
+ </Text>
30
+ <Text c="dimmed">페이지 설명 텍스트가 들어갑니다.</Text>
31
+ </>
32
+ ),
33
+ },
34
+ };
@@ -0,0 +1,16 @@
1
+ import { Box } from "@mantine/core";
2
+ import { PropsWithChildren } from "react";
3
+
4
+ export default function BoxContainer({ children }: PropsWithChildren) {
5
+ return (
6
+ <Box
7
+ component="main"
8
+ style={{
9
+ flexGrow: 1,
10
+ padding: 24,
11
+ }}
12
+ >
13
+ {children}
14
+ </Box>
15
+ );
16
+ }