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.
- package/dist/index.d.mts +339 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.js +1280 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1234 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
- package/src/atoms/Badge.stories.tsx +136 -0
- package/src/atoms/Badge.tsx +46 -0
- package/src/atoms/Button.stories.tsx +142 -0
- package/src/atoms/Button.tsx +50 -0
- package/src/atoms/ComboboxAutocomplete.stories.tsx +43 -0
- package/src/atoms/ComboboxAutocomplete.tsx +6 -0
- package/src/atoms/ComboboxSelect.stories.tsx +56 -0
- package/src/atoms/ComboboxSelect.tsx +6 -0
- package/src/atoms/DateTimePicker.stories.tsx +42 -0
- package/src/atoms/DateTimePicker.tsx +6 -0
- package/src/atoms/Icon.stories.tsx +86 -0
- package/src/atoms/Icon.tsx +81 -0
- package/src/atoms/IconButton.stories.tsx +59 -0
- package/src/atoms/IconButton.tsx +19 -0
- package/src/atoms/InputColor.stories.tsx +42 -0
- package/src/atoms/InputColor.tsx +6 -0
- package/src/atoms/InputDatePicker.stories.tsx +55 -0
- package/src/atoms/InputDatePicker.tsx +6 -0
- package/src/atoms/InputFile.stories.tsx +57 -0
- package/src/atoms/InputFile.tsx +15 -0
- package/src/atoms/InputNumber.stories.tsx +58 -0
- package/src/atoms/InputNumber.tsx +15 -0
- package/src/atoms/InputPassword.stories.tsx +42 -0
- package/src/atoms/InputPassword.tsx +24 -0
- package/src/atoms/InputRadio.stories.tsx +65 -0
- package/src/atoms/InputRadio.tsx +19 -0
- package/src/atoms/InputSegmentedControl.stories.tsx +44 -0
- package/src/atoms/InputSegmentedControl.tsx +15 -0
- package/src/atoms/InputSwitch.stories.tsx +45 -0
- package/src/atoms/InputSwitch.tsx +23 -0
- package/src/atoms/InputSwitchInTable.stories.tsx +43 -0
- package/src/atoms/InputSwitchInTable.tsx +35 -0
- package/src/atoms/InputText.stories.tsx +42 -0
- package/src/atoms/InputText.tsx +7 -0
- package/src/atoms/InputTextarea.stories.tsx +50 -0
- package/src/atoms/InputTextarea.tsx +6 -0
- package/src/atoms/index.ts +20 -0
- package/src/atoms/inputClassNames.ts +20 -0
- package/src/blocks/Accordion.stories.tsx +102 -0
- package/src/blocks/Accordion.tsx +124 -0
- package/src/blocks/AccordionDraggable.stories.tsx +169 -0
- package/src/blocks/AccordionDraggable.tsx +200 -0
- package/src/blocks/BoxContainer.stories.tsx +34 -0
- package/src/blocks/BoxContainer.tsx +16 -0
- package/src/blocks/DataTable.tsx +127 -0
- package/src/blocks/DescriptionRow.stories.tsx +34 -0
- package/src/blocks/DescriptionRow.tsx +22 -0
- package/src/blocks/EditorLayout.stories.tsx +79 -0
- package/src/blocks/EditorLayout.tsx +43 -0
- package/src/blocks/ImageUpload.tsx +292 -0
- package/src/blocks/LabeledField.stories.tsx +34 -0
- package/src/blocks/LabeledField.tsx +31 -0
- package/src/blocks/MediDrawer.stories.tsx +58 -0
- package/src/blocks/MediDrawer.tsx +34 -0
- package/src/blocks/Modal.stories.tsx +42 -0
- package/src/blocks/Modal.tsx +31 -0
- package/src/blocks/ModalDeleteConfirm.stories.tsx +31 -0
- package/src/blocks/ModalDeleteConfirm.tsx +56 -0
- package/src/blocks/ModalTokenExpired.tsx +52 -0
- package/src/blocks/NavigationBanner.tsx +100 -0
- package/src/blocks/PageHeader.tsx +24 -0
- package/src/blocks/PageLoading.stories.tsx +22 -0
- package/src/blocks/PageLoading.tsx +19 -0
- package/src/blocks/PageTitle.stories.tsx +39 -0
- package/src/blocks/PageTitle.tsx +25 -0
- package/src/blocks/Pagination.tsx +49 -0
- package/src/blocks/Panel.stories.tsx +84 -0
- package/src/blocks/Panel.tsx +97 -0
- package/src/blocks/SectionGroupWithTitle.stories.tsx +24 -0
- package/src/blocks/SectionGroupWithTitle.tsx +21 -0
- package/src/blocks/Table.tsx +114 -0
- package/src/blocks/TagInput.stories.tsx +74 -0
- package/src/blocks/TagInput.tsx +93 -0
- package/src/blocks/index.ts +25 -0
- package/src/index.ts +51 -0
- package/src/utils/file.ts +7 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
import { MediDrawer } from "./MediDrawer";
|
|
5
|
+
import { Text } from "@mantine/core";
|
|
6
|
+
import { Button } from "../atoms/Button";
|
|
7
|
+
import DescriptionRow from "./DescriptionRow";
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof MediDrawer> = {
|
|
10
|
+
title: "Blocks/MediDrawer",
|
|
11
|
+
component: MediDrawer,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof MediDrawer>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => {
|
|
20
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
21
|
+
const [isOpened, setIsOpened] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleClose = () => {
|
|
24
|
+
setIsOpened(false);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Button onClick={() => setIsOpened(true)}>Open MediDrawer</Button>
|
|
30
|
+
<MediDrawer title="시술 상세" opened={isOpened} onClose={handleClose}>
|
|
31
|
+
<DescriptionRow label="시작일" value={<Text>2020.12.12 12:22</Text>} />
|
|
32
|
+
<DescriptionRow label="종료일" value={<Text>2026.12.12 10:22</Text>} />
|
|
33
|
+
</MediDrawer>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
parameters: {
|
|
38
|
+
docs: {
|
|
39
|
+
source: {
|
|
40
|
+
code: `const [isOpened, setIsOpened] = useState(false);
|
|
41
|
+
|
|
42
|
+
const handleClose = () => {
|
|
43
|
+
setIsOpened(false);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<Button onClick={() => setIsOpened(true)}>Open MediDrawer</Button>
|
|
49
|
+
<MediDrawer title="시술 상세" opened={isOpened} onClose={handleClose}>
|
|
50
|
+
<DescriptionRow label="시작일" value={<Text>2020.12.12 12:22</Text>} />
|
|
51
|
+
<DescriptionRow label="종료일" value={<Text>2026.12.12 10:22</Text>} />
|
|
52
|
+
</MediDrawer>
|
|
53
|
+
</>
|
|
54
|
+
);`,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { Box, Drawer, DrawerProps, Text } from "@mantine/core";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string;
|
|
8
|
+
children: ReactNode; //body
|
|
9
|
+
footer?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MediDrawer({ title, children, footer, ...drawerProps }: Props & DrawerProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Drawer
|
|
15
|
+
title={
|
|
16
|
+
<Text fw={700} size="lg">
|
|
17
|
+
{title}
|
|
18
|
+
</Text>
|
|
19
|
+
}
|
|
20
|
+
{...drawerProps}
|
|
21
|
+
position="right"
|
|
22
|
+
size="md"
|
|
23
|
+
styles={{
|
|
24
|
+
body: { display: "flex", flexDirection: "column", height: "calc(100% - 64px)", padding: 0 },
|
|
25
|
+
header: { borderBottom: "1px solid var(--color-gray-2)", padding: "16px 24px" },
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<Box style={{ flex: 1, overflowY: "auto", padding: "24px" }}>{children}</Box>
|
|
29
|
+
|
|
30
|
+
{/* 버튼 */}
|
|
31
|
+
{footer && <Box style={{ borderTop: "1px solid var(--color-gray-2)", padding: "16px 24px" }}>{footer}</Box>}
|
|
32
|
+
</Drawer>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Modal from "./Modal";
|
|
3
|
+
import { Text, Stack } from "@mantine/core";
|
|
4
|
+
import { Button } from "../atoms/Button";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Modal> = {
|
|
7
|
+
title: "Blocks/Modal",
|
|
8
|
+
component: Modal,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
args: {
|
|
11
|
+
opened: true,
|
|
12
|
+
onClose: () => {},
|
|
13
|
+
title: "모달 제목",
|
|
14
|
+
children: <Text>모달 내용이 들어갑니다.</Text>,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Modal>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {};
|
|
22
|
+
|
|
23
|
+
export const WithForm: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
title: "정보 수정",
|
|
26
|
+
size: "lg",
|
|
27
|
+
children: (
|
|
28
|
+
<Stack gap="md">
|
|
29
|
+
<Text>수정할 내용을 입력하세요.</Text>
|
|
30
|
+
<Button variant="filled">저장</Button>
|
|
31
|
+
</Stack>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const SmallSize: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
title: "알림",
|
|
39
|
+
size: "sm",
|
|
40
|
+
children: <Text>처리가 완료되었습니다.</Text>,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Modal as MantineModal, ModalProps } from "@mantine/core";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
export default function Modal({
|
|
5
|
+
opened,
|
|
6
|
+
onClose,
|
|
7
|
+
title,
|
|
8
|
+
size,
|
|
9
|
+
styles,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: ModalProps & { children: React.ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<MantineModal
|
|
15
|
+
opened={opened}
|
|
16
|
+
onClose={onClose}
|
|
17
|
+
title={title}
|
|
18
|
+
size={size}
|
|
19
|
+
styles={styles}
|
|
20
|
+
{...props}
|
|
21
|
+
classNames={{
|
|
22
|
+
...props.classNames,
|
|
23
|
+
header: "!-mt-8 !p-6",
|
|
24
|
+
title: `text-center !font-bold ${props.classNames?.title ?? ""}`,
|
|
25
|
+
body: `!p-6 ${props.classNames?.body ?? ""}`,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</MantineModal>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import ModalDeleteConfirm from "./ModalDeleteConfirm";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ModalDeleteConfirm> = {
|
|
5
|
+
title: "Blocks/ModalDeleteConfirm",
|
|
6
|
+
component: ModalDeleteConfirm,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
args: {
|
|
9
|
+
opened: true,
|
|
10
|
+
onClose: () => {},
|
|
11
|
+
onConfirm: () => {},
|
|
12
|
+
message: "삭제된 데이터는 복구할 수 없습니다.",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof ModalDeleteConfirm>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {};
|
|
20
|
+
|
|
21
|
+
export const CustomMessage: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
message: "이 시술 정보를 삭제하시겠습니까? 관련된 모든 데이터가 함께 삭제됩니다.",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ShortMessage: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
message: "선택한 항목을 삭제합니다.",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "../atoms";
|
|
3
|
+
import Modal from "./Modal";
|
|
4
|
+
import { Box, Group } from "@mantine/core";
|
|
5
|
+
|
|
6
|
+
interface ModalProps {
|
|
7
|
+
opened: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onConfirm: () => void;
|
|
10
|
+
message?: string | React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function ModalDeleteConfirm({ opened, onClose, onConfirm, message }: ModalProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Modal
|
|
16
|
+
opened={opened}
|
|
17
|
+
onClose={onClose}
|
|
18
|
+
size="md"
|
|
19
|
+
centered
|
|
20
|
+
withCloseButton={false}
|
|
21
|
+
classNames={{ body: "text-center" }}
|
|
22
|
+
>
|
|
23
|
+
<Box className="flex flex-col items-center gap-5">
|
|
24
|
+
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-red-50">
|
|
25
|
+
<i className="text-error text-2xl" data-fa-i2svg="">
|
|
26
|
+
<svg
|
|
27
|
+
width="24"
|
|
28
|
+
height="24"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
focusable="false"
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
viewBox="0 0 448 512"
|
|
33
|
+
>
|
|
34
|
+
<path
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
d="M135.2 17.7C140.6 6.8 151.7 0 163.8 0H284.2c12.1 0 23.2 6.8 28.6 17.7L320 32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32h96l7.2-14.3zM32 128H416V448c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64V128zm96 64c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16z"
|
|
37
|
+
></path>
|
|
38
|
+
</svg>
|
|
39
|
+
</i>
|
|
40
|
+
</div>
|
|
41
|
+
<h3 className="text-xl leading-tight font-bold text-gray-900">정말 삭제하시겠습니까?</h3>
|
|
42
|
+
|
|
43
|
+
{message && <p className="-mt-2 text-sm leading-relaxed text-gray-600">{message}</p>}
|
|
44
|
+
|
|
45
|
+
<Group justify="center" gap="xs">
|
|
46
|
+
<Button variant="outline" color="gray" size="md" onClick={onClose}>
|
|
47
|
+
취소
|
|
48
|
+
</Button>
|
|
49
|
+
<Button variant="filled" color="red" size="md" onClick={onConfirm}>
|
|
50
|
+
삭제
|
|
51
|
+
</Button>
|
|
52
|
+
</Group>
|
|
53
|
+
</Box>
|
|
54
|
+
</Modal>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Modal from "./Modal";
|
|
5
|
+
import { Box, Text } from "@mantine/core";
|
|
6
|
+
|
|
7
|
+
export function ModalTokenExpired() {
|
|
8
|
+
const [isOpened, setIsOpened] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleTokenExpired = () => {
|
|
12
|
+
setIsOpened(true);
|
|
13
|
+
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
localStorage.removeItem("accessToken");
|
|
16
|
+
localStorage.removeItem("tokenExpiresAt");
|
|
17
|
+
localStorage.removeItem("featureVersion");
|
|
18
|
+
localStorage.removeItem("units");
|
|
19
|
+
window.location.href = "/login";
|
|
20
|
+
}, 3000);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
window.addEventListener("token-expired", handleTokenExpired);
|
|
24
|
+
return () => window.removeEventListener("token-expired", handleTokenExpired);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Modal opened={isOpened} onClose={() => {}} withCloseButton={false} centered size="sm" zIndex={1000}>
|
|
29
|
+
<Box className="flex flex-col items-center gap-5">
|
|
30
|
+
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-red-50">
|
|
31
|
+
<i className="text-error text-2xl" data-fa-i2svg="">
|
|
32
|
+
<svg
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
+
width="32"
|
|
35
|
+
height="32"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="currentColor"
|
|
38
|
+
className="icon icon-tabler icons-tabler-filled icon-tabler-exclamation-circle"
|
|
39
|
+
>
|
|
40
|
+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
41
|
+
<path d="M17 3.34a10 10 0 1 1 -15 8.66l.005 -.324a10 10 0 0 1 14.995 -8.336m-5 11.66a1 1 0 0 0 -1 1v.01a1 1 0 0 0 2 0v-.01a1 1 0 0 0 -1 -1m0 -7a1 1 0 0 0 -1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0 -1 -1" />
|
|
42
|
+
</svg>
|
|
43
|
+
</i>
|
|
44
|
+
</div>
|
|
45
|
+
<Text ta="center" fw={600} size="lg" mb="xs">
|
|
46
|
+
토큰이 만료되었습니다 <br />
|
|
47
|
+
잠시 후 로그인 페이지로 이동합니다.
|
|
48
|
+
</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
</Modal>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Box, Text, ActionIcon, Group } from "@mantine/core";
|
|
5
|
+
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
export interface NavigationBannerItem {
|
|
12
|
+
id: string;
|
|
13
|
+
image: string;
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NavigationBannerProps {
|
|
18
|
+
items: NavigationBannerItem[];
|
|
19
|
+
currentIndex?: number;
|
|
20
|
+
onIndexChange?: (index: number) => void;
|
|
21
|
+
imageSize?: number;
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
showNavigation?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// Component
|
|
29
|
+
// ============================================
|
|
30
|
+
|
|
31
|
+
export function NavigationBanner({
|
|
32
|
+
items,
|
|
33
|
+
currentIndex: controlledIndex,
|
|
34
|
+
onIndexChange,
|
|
35
|
+
imageSize = 275,
|
|
36
|
+
emptyText = "이미지를 추가하세요",
|
|
37
|
+
showNavigation = true,
|
|
38
|
+
className,
|
|
39
|
+
}: NavigationBannerProps) {
|
|
40
|
+
const [internalIndex, setInternalIndex] = useState(0);
|
|
41
|
+
|
|
42
|
+
// Controlled or uncontrolled mode
|
|
43
|
+
const currentIndex = controlledIndex ?? internalIndex;
|
|
44
|
+
const setCurrentIndex = onIndexChange ?? setInternalIndex;
|
|
45
|
+
|
|
46
|
+
// Reset index when items change
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (currentIndex >= items.length && items.length > 0) {
|
|
49
|
+
setCurrentIndex(items.length - 1);
|
|
50
|
+
}
|
|
51
|
+
}, [items.length, currentIndex, setCurrentIndex]);
|
|
52
|
+
|
|
53
|
+
const isItems = items.length > 0;
|
|
54
|
+
const currentItem = items[currentIndex];
|
|
55
|
+
const isMultiple = items.length > 1;
|
|
56
|
+
|
|
57
|
+
const handlePrev = () => {
|
|
58
|
+
const newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
59
|
+
setCurrentIndex(newIndex);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleNext = () => {
|
|
63
|
+
const newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
64
|
+
setCurrentIndex(newIndex);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box className={className}>
|
|
69
|
+
{/* 배너 이미지 영역 */}
|
|
70
|
+
<Box className="overflow-hidden rounded-sm bg-gray-100" style={{ width: imageSize, height: imageSize }}>
|
|
71
|
+
{isItems && currentItem ? (
|
|
72
|
+
<img src={currentItem.image} alt={currentItem.title} className="h-full w-full object-cover" />
|
|
73
|
+
) : (
|
|
74
|
+
<Box className="flex h-full w-full items-center justify-center">
|
|
75
|
+
<Text size="xs" c="dimmed">
|
|
76
|
+
{emptyText}
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
)}
|
|
80
|
+
</Box>
|
|
81
|
+
|
|
82
|
+
{/* 네비게이션 영역 */}
|
|
83
|
+
{showNavigation && (
|
|
84
|
+
<Group justify="center" gap="md" mt="sm" style={{ width: imageSize }}>
|
|
85
|
+
<ActionIcon variant="subtle" color="gray" size="sm" onClick={handlePrev} disabled={!isMultiple}>
|
|
86
|
+
<IconChevronLeft size={18} />
|
|
87
|
+
</ActionIcon>
|
|
88
|
+
|
|
89
|
+
<Text size="xs" fw={600} className="flex-1 text-center tracking-wider uppercase" lineClamp={1}>
|
|
90
|
+
{currentItem?.title || "제목 없음"}
|
|
91
|
+
</Text>
|
|
92
|
+
|
|
93
|
+
<ActionIcon variant="subtle" color="gray" size="sm" onClick={handleNext} disabled={!isMultiple}>
|
|
94
|
+
<IconChevronRight size={18} />
|
|
95
|
+
</ActionIcon>
|
|
96
|
+
</Group>
|
|
97
|
+
)}
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ActionIcon, Title, Box } from "@mantine/core";
|
|
4
|
+
import { IconArrowLeft } from "@tabler/icons-react";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export default function PageHeader({ headerText, back }: { headerText: string; back?: boolean }) {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
|
|
10
|
+
const handleBack = () => {
|
|
11
|
+
router.back();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
16
|
+
{back && (
|
|
17
|
+
<ActionIcon variant="subtle" size="lg" onClick={handleBack}>
|
|
18
|
+
<IconArrowLeft size={20} />
|
|
19
|
+
</ActionIcon>
|
|
20
|
+
)}
|
|
21
|
+
<Title order={2}>{headerText}</Title>
|
|
22
|
+
</Box>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { PageLoading } from "./PageLoading";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof PageLoading> = {
|
|
5
|
+
title: "Blocks/PageLoading",
|
|
6
|
+
component: PageLoading,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "fullscreen",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof PageLoading>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {};
|
|
17
|
+
|
|
18
|
+
export const WithBackground: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
bg: "white",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Box, LoadingOverlay } from "@mantine/core";
|
|
2
|
+
|
|
3
|
+
interface PageLoadingProps {
|
|
4
|
+
bg?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function PageLoading({ bg = "transparent" }: PageLoadingProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Box pos="fixed" top={0} left="var(--sidebar-width)" right={0} bottom={0} style={{ zIndex: 400 }}>
|
|
10
|
+
<LoadingOverlay
|
|
11
|
+
visible
|
|
12
|
+
overlayProps={{
|
|
13
|
+
backgroundOpacity: bg === "transparent" ? 0 : 1,
|
|
14
|
+
color: bg,
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
</Box>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { PageTitle } from "./PageTitle";
|
|
3
|
+
import { Button } from "@mantine/core";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof PageTitle> = {
|
|
6
|
+
title: "Blocks/PageTitle",
|
|
7
|
+
component: PageTitle,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
title: "시술 관리",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof PageTitle>;
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {};
|
|
18
|
+
|
|
19
|
+
export const WithSubText: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
title: "리뷰 관리",
|
|
22
|
+
subText: "고객 리뷰를 관리할 수 있습니다",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const WithRightContent: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
title: "상담 관리",
|
|
29
|
+
rightContent: <Button size="sm">추가</Button>,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const WithSubTextAndRightContent: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
title: "회원 관리",
|
|
36
|
+
subText: "등록된 회원 목록입니다",
|
|
37
|
+
rightContent: <Button size="sm">회원 추가</Button>,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Group, Text, Title, TitleProps } from "@mantine/core";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
export function PageTitle({
|
|
5
|
+
title,
|
|
6
|
+
subText,
|
|
7
|
+
rightContent,
|
|
8
|
+
leftContent,
|
|
9
|
+
...props
|
|
10
|
+
}: TitleProps & { subText?: string; rightContent?: React.ReactNode; leftContent?: React.ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<Group justify="space-between" align="end" mb="xl">
|
|
13
|
+
{leftContent}
|
|
14
|
+
<Title order={2} c="var(--color-brand-dark)" {...props}>
|
|
15
|
+
{title}
|
|
16
|
+
{subText && (
|
|
17
|
+
<Text size="md" c="var(--color-subtitle)" mt="5">
|
|
18
|
+
{subText}
|
|
19
|
+
</Text>
|
|
20
|
+
)}
|
|
21
|
+
</Title>
|
|
22
|
+
{rightContent}
|
|
23
|
+
</Group>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Pagination as MantinePagination, Group } from "@mantine/core";
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
export interface PaginationProps {
|
|
10
|
+
total: number;
|
|
11
|
+
page: number;
|
|
12
|
+
onChange: (page: number) => void;
|
|
13
|
+
siblings?: number;
|
|
14
|
+
boundaries?: number;
|
|
15
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
16
|
+
withEdges?: boolean;
|
|
17
|
+
position?: "left" | "center" | "right";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// Component
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
export function Pagination({
|
|
25
|
+
total,
|
|
26
|
+
page,
|
|
27
|
+
onChange,
|
|
28
|
+
siblings = 1,
|
|
29
|
+
boundaries = 1,
|
|
30
|
+
size = "sm",
|
|
31
|
+
withEdges = true,
|
|
32
|
+
position = "center",
|
|
33
|
+
}: PaginationProps) {
|
|
34
|
+
if (total <= 0) return null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Group justify={position} mt="xl">
|
|
38
|
+
<MantinePagination
|
|
39
|
+
total={total}
|
|
40
|
+
value={page}
|
|
41
|
+
onChange={onChange}
|
|
42
|
+
siblings={siblings}
|
|
43
|
+
boundaries={boundaries}
|
|
44
|
+
size={size}
|
|
45
|
+
withEdges={withEdges}
|
|
46
|
+
/>
|
|
47
|
+
</Group>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Panel } from ".";
|
|
3
|
+
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Panel> = {
|
|
6
|
+
title: "Blocks/Panel",
|
|
7
|
+
component: Panel,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Panel>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
title: "기본 정보",
|
|
17
|
+
children: <Text>컨텐츠가 들어갑니다.</Text>,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const WithHeaderRightContent: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
title: "사용자 목록",
|
|
24
|
+
headerRightContent: <Button size="xs">추가</Button>,
|
|
25
|
+
children: <Text c="dimmed">등록된 사용자가 없습니다.</Text>,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const WithTwoSections: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
children: (
|
|
32
|
+
<>
|
|
33
|
+
<Panel.Section title="기본 정보">
|
|
34
|
+
<Stack gap="sm">
|
|
35
|
+
<TextInput label="이름" placeholder="이름을 입력하세요" />
|
|
36
|
+
<TextInput label="전화번호" placeholder="전화번호를 입력하세요" />
|
|
37
|
+
</Stack>
|
|
38
|
+
</Panel.Section>
|
|
39
|
+
<Panel.Section title="추가 정보">
|
|
40
|
+
<Stack gap="sm">
|
|
41
|
+
<TextInput label="이메일" placeholder="이메일을 입력하세요" />
|
|
42
|
+
<TextInput label="주소" placeholder="주소를 입력하세요" />
|
|
43
|
+
</Stack>
|
|
44
|
+
</Panel.Section>
|
|
45
|
+
</>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithFooter: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
title: "설정",
|
|
53
|
+
children: (
|
|
54
|
+
<Stack gap="sm">
|
|
55
|
+
<TextInput label="이름" placeholder="이름을 입력하세요" />
|
|
56
|
+
<TextInput label="전화번호" placeholder="전화번호를 입력하세요" />
|
|
57
|
+
</Stack>
|
|
58
|
+
),
|
|
59
|
+
footer: (
|
|
60
|
+
<Group justify="flex-end">
|
|
61
|
+
<Button variant="outline">취소</Button>
|
|
62
|
+
<Button>저장</Button>
|
|
63
|
+
</Group>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const bigContentBottomPadding: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
title: "콘텐츠 하단 패딩이 100px일 때",
|
|
71
|
+
contentBottomPadding: 100,
|
|
72
|
+
children: (
|
|
73
|
+
<Stack gap="sm">
|
|
74
|
+
<TextInput label="이름" placeholder="이름을 입력하세요" />
|
|
75
|
+
<TextInput label="전화번호" placeholder="전화번호를 입력하세요" />
|
|
76
|
+
</Stack>
|
|
77
|
+
),
|
|
78
|
+
footer: (
|
|
79
|
+
<Group justify="flex-end">
|
|
80
|
+
<Button>저장</Button>
|
|
81
|
+
</Group>
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
};
|