) {
+ const isMultipleSelect = (selected: T | T[]): selected is T[] => {
+ return Array.isArray(selected);
+ };
+
+ return (
+
+ {label && {label}}
+
+ {options.map(option => {
+ const isSelected = isMultipleSelect(selected)
+ ? selected.includes(option.value)
+ : selected === option.value;
+ return (
+ {
+ changeValue(option.value);
+ }}
+ >
+ {option.label}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/Filter/index.tsx b/src/components/Filter/index.tsx
new file mode 100644
index 00000000..906b17f7
--- /dev/null
+++ b/src/components/Filter/index.tsx
@@ -0,0 +1,10 @@
+import { FilterDateRange } from './DateRange';
+import { CategoryItemWrapper, CategoryLabel, FilterCategory, FilterFrame } from './Default';
+
+export const Filter = {
+ Frame: FilterFrame,
+ CategoryLabel,
+ CategoryItemWrapper,
+ Category: FilterCategory,
+ DateRange: FilterDateRange,
+};
diff --git a/src/components/Icon/Add.tsx b/src/components/Icon/Add.tsx
new file mode 100644
index 00000000..da6ebefb
--- /dev/null
+++ b/src/components/Icon/Add.tsx
@@ -0,0 +1,19 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Add({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/AlertCircle.tsx b/src/components/Icon/AlertCircle.tsx
new file mode 100644
index 00000000..3dedef66
--- /dev/null
+++ b/src/components/Icon/AlertCircle.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function AlertCircle({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ArrowLeftLarge.tsx b/src/components/Icon/ArrowLeftLarge.tsx
new file mode 100644
index 00000000..9ce051d6
--- /dev/null
+++ b/src/components/Icon/ArrowLeftLarge.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ArrowLeftLarge({ color: c = color['grey-80'], size = 24 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ArrowLeftSmall.tsx b/src/components/Icon/ArrowLeftSmall.tsx
new file mode 100644
index 00000000..b2df773e
--- /dev/null
+++ b/src/components/Icon/ArrowLeftSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ArrowLeftSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/BannersVert.tsx b/src/components/Icon/BannersVert.tsx
new file mode 100644
index 00000000..bbbf5e9b
--- /dev/null
+++ b/src/components/Icon/BannersVert.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function BannersVert({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Calendar.tsx b/src/components/Icon/Calendar.tsx
new file mode 100644
index 00000000..3383c1c1
--- /dev/null
+++ b/src/components/Icon/Calendar.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Calendar({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/CancelLarge.tsx b/src/components/Icon/CancelLarge.tsx
new file mode 100644
index 00000000..6591e1c0
--- /dev/null
+++ b/src/components/Icon/CancelLarge.tsx
@@ -0,0 +1,19 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function CancelLarge({ color: c = color['grey-80'], size = 24 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/CancelSmall.tsx b/src/components/Icon/CancelSmall.tsx
new file mode 100644
index 00000000..24eac749
--- /dev/null
+++ b/src/components/Icon/CancelSmall.tsx
@@ -0,0 +1,20 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function CancelSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChatFocused.tsx b/src/components/Icon/ChatFocused.tsx
new file mode 100644
index 00000000..19cedbeb
--- /dev/null
+++ b/src/components/Icon/ChatFocused.tsx
@@ -0,0 +1,39 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChatFocused({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChatPlus.tsx b/src/components/Icon/ChatPlus.tsx
new file mode 100644
index 00000000..b723ba3d
--- /dev/null
+++ b/src/components/Icon/ChatPlus.tsx
@@ -0,0 +1,23 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChatPlus({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/CheckLarge.tsx b/src/components/Icon/CheckLarge.tsx
new file mode 100644
index 00000000..87c22f8f
--- /dev/null
+++ b/src/components/Icon/CheckLarge.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function CheckLarge({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/CheckSmall.tsx b/src/components/Icon/CheckSmall.tsx
new file mode 100644
index 00000000..698b3016
--- /dev/null
+++ b/src/components/Icon/CheckSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function CheckSmall({ color: c = color['grey-80'], size = 16 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChevronDownSmall.tsx b/src/components/Icon/ChevronDownSmall.tsx
new file mode 100644
index 00000000..7a26476a
--- /dev/null
+++ b/src/components/Icon/ChevronDownSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChevronDownSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChevronLeftSmall.tsx b/src/components/Icon/ChevronLeftSmall.tsx
new file mode 100644
index 00000000..e901f040
--- /dev/null
+++ b/src/components/Icon/ChevronLeftSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChevronLeftSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChevronRightSmall.tsx b/src/components/Icon/ChevronRightSmall.tsx
new file mode 100644
index 00000000..750bb69f
--- /dev/null
+++ b/src/components/Icon/ChevronRightSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChevronRightSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ChevronUpSmall.tsx b/src/components/Icon/ChevronUpSmall.tsx
new file mode 100644
index 00000000..61a44e1c
--- /dev/null
+++ b/src/components/Icon/ChevronUpSmall.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ChevronUpSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Clock.tsx b/src/components/Icon/Clock.tsx
new file mode 100644
index 00000000..8554c094
--- /dev/null
+++ b/src/components/Icon/Clock.tsx
@@ -0,0 +1,25 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Clock({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Copy.tsx b/src/components/Icon/Copy.tsx
new file mode 100644
index 00000000..85b544a5
--- /dev/null
+++ b/src/components/Icon/Copy.tsx
@@ -0,0 +1,22 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Copy({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/DashboardBar.tsx b/src/components/Icon/DashboardBar.tsx
new file mode 100644
index 00000000..23cdd51e
--- /dev/null
+++ b/src/components/Icon/DashboardBar.tsx
@@ -0,0 +1,33 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function DashboardBar({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Dot.tsx b/src/components/Icon/Dot.tsx
new file mode 100644
index 00000000..f3cb8d35
--- /dev/null
+++ b/src/components/Icon/Dot.tsx
@@ -0,0 +1,18 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Dot({ color: c = color['grey-80'], size = 8 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/DotsHori.tsx b/src/components/Icon/DotsHori.tsx
new file mode 100644
index 00000000..b4539aff
--- /dev/null
+++ b/src/components/Icon/DotsHori.tsx
@@ -0,0 +1,27 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function DotsHori({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/DotsVert.tsx b/src/components/Icon/DotsVert.tsx
new file mode 100644
index 00000000..e02705ca
--- /dev/null
+++ b/src/components/Icon/DotsVert.tsx
@@ -0,0 +1,27 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function DotsVert({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Earth.tsx b/src/components/Icon/Earth.tsx
new file mode 100644
index 00000000..362f7022
--- /dev/null
+++ b/src/components/Icon/Earth.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Earth({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Edit.tsx b/src/components/Icon/Edit.tsx
new file mode 100644
index 00000000..32a365d9
--- /dev/null
+++ b/src/components/Icon/Edit.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Edit({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/EditSquare.tsx b/src/components/Icon/EditSquare.tsx
new file mode 100644
index 00000000..c481209a
--- /dev/null
+++ b/src/components/Icon/EditSquare.tsx
@@ -0,0 +1,22 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function EditSquare({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Eye.tsx b/src/components/Icon/Eye.tsx
new file mode 100644
index 00000000..d2d6d1d3
--- /dev/null
+++ b/src/components/Icon/Eye.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Eye({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/EyeClosed.tsx b/src/components/Icon/EyeClosed.tsx
new file mode 100644
index 00000000..403a85b6
--- /dev/null
+++ b/src/components/Icon/EyeClosed.tsx
@@ -0,0 +1,22 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function EyeClosed({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Filter.tsx b/src/components/Icon/Filter.tsx
new file mode 100644
index 00000000..a632f83a
--- /dev/null
+++ b/src/components/Icon/Filter.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Filter({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/InformationCircleLarge.tsx b/src/components/Icon/InformationCircleLarge.tsx
new file mode 100644
index 00000000..233a6783
--- /dev/null
+++ b/src/components/Icon/InformationCircleLarge.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function InformationCircleLarge({ color: c = color['grey-80'], size = 24 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/InformationCircleSmall.tsx b/src/components/Icon/InformationCircleSmall.tsx
new file mode 100644
index 00000000..cd707d87
--- /dev/null
+++ b/src/components/Icon/InformationCircleSmall.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function InformationCircleSmall({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Layout.tsx b/src/components/Icon/Layout.tsx
new file mode 100644
index 00000000..d71b5e80
--- /dev/null
+++ b/src/components/Icon/Layout.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Layout({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/List.tsx b/src/components/Icon/List.tsx
new file mode 100644
index 00000000..f3cd13a9
--- /dev/null
+++ b/src/components/Icon/List.tsx
@@ -0,0 +1,45 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function List({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Logout.tsx b/src/components/Icon/Logout.tsx
new file mode 100644
index 00000000..63c5b4e0
--- /dev/null
+++ b/src/components/Icon/Logout.tsx
@@ -0,0 +1,23 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Logout({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Minus.tsx b/src/components/Icon/Minus.tsx
new file mode 100644
index 00000000..1ecc8a8e
--- /dev/null
+++ b/src/components/Icon/Minus.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Minus({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/QuestionCircle.tsx b/src/components/Icon/QuestionCircle.tsx
new file mode 100644
index 00000000..2377493e
--- /dev/null
+++ b/src/components/Icon/QuestionCircle.tsx
@@ -0,0 +1,31 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function QuestionCircle({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Search.tsx b/src/components/Icon/Search.tsx
new file mode 100644
index 00000000..ca4a6115
--- /dev/null
+++ b/src/components/Icon/Search.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Search({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Setting.tsx b/src/components/Icon/Setting.tsx
new file mode 100644
index 00000000..db5ed936
--- /dev/null
+++ b/src/components/Icon/Setting.tsx
@@ -0,0 +1,27 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Setting({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Shop.tsx b/src/components/Icon/Shop.tsx
new file mode 100644
index 00000000..8bc2aac8
--- /dev/null
+++ b/src/components/Icon/Shop.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Shop({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Sort.tsx b/src/components/Icon/Sort.tsx
new file mode 100644
index 00000000..be059444
--- /dev/null
+++ b/src/components/Icon/Sort.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Sort({ color: c = color['grey-30'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/SortAscending.tsx b/src/components/Icon/SortAscending.tsx
new file mode 100644
index 00000000..c40ffe47
--- /dev/null
+++ b/src/components/Icon/SortAscending.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function SortAscending({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/SortDescending.tsx b/src/components/Icon/SortDescending.tsx
new file mode 100644
index 00000000..15818f6f
--- /dev/null
+++ b/src/components/Icon/SortDescending.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function SortDescending({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Switch.tsx b/src/components/Icon/Switch.tsx
new file mode 100644
index 00000000..75a870d6
--- /dev/null
+++ b/src/components/Icon/Switch.tsx
@@ -0,0 +1,35 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Switch({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Tag.tsx b/src/components/Icon/Tag.tsx
new file mode 100644
index 00000000..7a9be9dd
--- /dev/null
+++ b/src/components/Icon/Tag.tsx
@@ -0,0 +1,25 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Tag({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ThumbsUp.tsx b/src/components/Icon/ThumbsUp.tsx
new file mode 100644
index 00000000..aa10ac8b
--- /dev/null
+++ b/src/components/Icon/ThumbsUp.tsx
@@ -0,0 +1,21 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function ThumbsUp({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ToastError.tsx b/src/components/Icon/ToastError.tsx
new file mode 100644
index 00000000..6c4fdbbc
--- /dev/null
+++ b/src/components/Icon/ToastError.tsx
@@ -0,0 +1,17 @@
+import { type IconProps } from './type';
+
+export function ToastError({ size = 18 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/ToastSuccess.tsx b/src/components/Icon/ToastSuccess.tsx
new file mode 100644
index 00000000..54b1c88b
--- /dev/null
+++ b/src/components/Icon/ToastSuccess.tsx
@@ -0,0 +1,25 @@
+import { type IconProps } from './type';
+
+export function ToastSuccess({ size = 18 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Trash.tsx b/src/components/Icon/Trash.tsx
new file mode 100644
index 00000000..dfc116a3
--- /dev/null
+++ b/src/components/Icon/Trash.tsx
@@ -0,0 +1,29 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Trash({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/Upload.tsx b/src/components/Icon/Upload.tsx
new file mode 100644
index 00000000..dc1b62ac
--- /dev/null
+++ b/src/components/Icon/Upload.tsx
@@ -0,0 +1,23 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function Upload({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/UserCircle.tsx b/src/components/Icon/UserCircle.tsx
new file mode 100644
index 00000000..12c0f495
--- /dev/null
+++ b/src/components/Icon/UserCircle.tsx
@@ -0,0 +1,27 @@
+import { type IconProps } from './type';
+import { color } from '../styles';
+
+export function UserCircle({ color: c = color['grey-80'], size = 20 }: IconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts
new file mode 100644
index 00000000..4d0333f1
--- /dev/null
+++ b/src/components/Icon/index.ts
@@ -0,0 +1,103 @@
+import { Add } from './Add';
+import { AlertCircle } from './AlertCircle';
+import { ArrowLeftLarge } from './ArrowLeftLarge';
+import { ArrowLeftSmall } from './ArrowLeftSmall';
+import { BannersVert } from './BannersVert';
+import { Calendar } from './Calendar';
+import { CancelLarge } from './CancelLarge';
+import { CancelSmall } from './CancelSmall';
+import { ChatFocused } from './ChatFocused';
+import { ChatPlus } from './ChatPlus';
+import { CheckLarge } from './CheckLarge';
+import { CheckSmall } from './CheckSmall';
+import { ChevronDownSmall } from './ChevronDownSmall';
+import { ChevronLeftSmall } from './ChevronLeftSmall';
+import { ChevronRightSmall } from './ChevronRightSmall';
+import { ChevronUpSmall } from './ChevronUpSmall';
+import { Clock } from './Clock';
+import { Copy } from './Copy';
+import { DashboardBar } from './DashboardBar';
+import { Dot } from './Dot';
+import { DotsHori } from './DotsHori';
+import { DotsVert } from './DotsVert';
+import { Earth } from './Earth';
+import { Edit } from './Edit';
+import { EditSquare } from './EditSquare';
+import { Eye } from './Eye';
+import { EyeClosed } from './EyeClosed';
+import { Filter } from './Filter';
+import { InformationCircleLarge } from './InformationCircleLarge';
+import { InformationCircleSmall } from './InformationCircleSmall';
+import { Layout } from './Layout';
+import { List } from './List';
+import { Logout } from './Logout';
+import { Minus } from './Minus';
+import { QuestionCircle } from './QuestionCircle';
+import { Search } from './Search';
+import { Setting } from './Setting';
+import { Shop } from './Shop';
+import { Sort } from './Sort';
+import { SortAscending } from './SortAscending';
+import { SortDescending } from './SortDescending';
+import { Switch } from './Switch';
+import { Tag } from './Tag';
+import { ThumbsUp } from './ThumbsUp';
+import { ToastError } from './ToastError';
+import { ToastSuccess } from './ToastSuccess';
+import { Trash } from './Trash';
+import { Upload } from './Upload';
+import { UserCircle } from './UserCircle';
+
+const Icon = {
+ Add,
+ AlertCircle,
+ Filter,
+ DotsVert,
+ CheckLarge,
+ Sort,
+ SortDescending,
+ SortAscending,
+ ChevronRightSmall,
+ ChevronDownSmall,
+ ChevronLeftSmall,
+ ChevronUpSmall,
+ Switch,
+ Logout,
+ DotsHori,
+ Layout,
+ BannersVert,
+ CheckSmall,
+ DashboardBar,
+ Tag,
+ Edit,
+ Shop,
+ ArrowLeftSmall,
+ ThumbsUp,
+ Setting,
+ InformationCircleSmall,
+ Minus,
+ UserCircle,
+ Upload,
+ Calendar,
+ Earth,
+ CancelSmall,
+ Search,
+ Clock,
+ QuestionCircle,
+ ChatFocused,
+ Eye,
+ Copy,
+ EyeClosed,
+ ChatPlus,
+ EditSquare,
+ List,
+ Trash,
+ InformationCircleLarge,
+ CancelLarge,
+ ArrowLeftLarge,
+ Dot,
+ ToastError,
+ ToastSuccess,
+};
+
+export default Icon;
diff --git a/src/components/Icon/type.d.ts b/src/components/Icon/type.d.ts
new file mode 100644
index 00000000..86617ac4
--- /dev/null
+++ b/src/components/Icon/type.d.ts
@@ -0,0 +1,4 @@
+export interface IconProps {
+ color?: string;
+ size?: number;
+}
diff --git a/src/components/Input/DateInput.tsx b/src/components/Input/DateInput.tsx
new file mode 100644
index 00000000..e64b12f8
--- /dev/null
+++ b/src/components/Input/DateInput.tsx
@@ -0,0 +1,58 @@
+import { useMemo } from 'react';
+
+import { BaseInput, type InputBaseProps, InputContainer } from './InputContainer';
+import Icon from '../Icon';
+import { color } from '../styles';
+
+export type DateInputProps = Omit;
+
+export function DateInput({
+ label,
+ placeholder,
+ description,
+ name,
+ value,
+ error,
+ disabled,
+ onChange,
+ defaultValue,
+ required = false,
+ width,
+ tooltip,
+ onClick,
+ ...props
+}: DateInputProps) {
+ const inputStyle = useMemo(() => {
+ return {
+ color: disabled ? color['grey-50'] : color['grey-60'],
+ size: 20,
+ };
+ }, [disabled]);
+
+ return (
+ }
+ {...props}
+ >
+
+
+ );
+}
diff --git a/src/components/Input/InputContainer.tsx b/src/components/Input/InputContainer.tsx
new file mode 100644
index 00000000..31cb1de6
--- /dev/null
+++ b/src/components/Input/InputContainer.tsx
@@ -0,0 +1,197 @@
+import { type InputHTMLAttributes, type ReactElement, cloneElement, ReactNode } from 'react';
+
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { B3, B5, B7 } from '../Text';
+import { Tooltip, type TooltipProps } from '../Tooltip';
+import { color, typography } from '../styles';
+
+export type InputTooltipProps = Omit;
+
+export interface InputBaseProps extends Omit, 'width'> {
+ label?: string;
+ description?: string;
+ error?: string;
+ children: ReactElement;
+ width?: number;
+ tooltip?: InputTooltipProps;
+ rightSection?: ReactNode;
+ leftSection?: ReactNode;
+ cursorPointer?: boolean;
+}
+
+export function InputContainer({
+ label,
+ description,
+ required = false,
+ error,
+ children,
+ width,
+ tooltip,
+ rightSection,
+ leftSection,
+ onClick,
+ cursorPointer,
+ ...props
+}: InputBaseProps) {
+ return (
+
+ {label && (
+
+
+ {label} {required && *}
+
+ {tooltip != null && (
+
+
+
+
+
+ )}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+ {rightSection || leftSection ? (
+
+ {leftSection && {leftSection}}
+ {cloneElement(children, {
+ isRightSection: !!rightSection,
+ isLeftSection: !!leftSection,
+ })}
+ {rightSection && (
+ {rightSection}
+ )}
+
+ ) : (
+ children
+ )}
+ {error && (
+
+
+ {error}
+
+ )}
+
+ );
+}
+
+const InputWrapper = styled.div<{ width?: number; cursorPointer?: boolean }>`
+ width: ${({ width }) => (width ? `${width}px` : '100%')};
+ position: relative;
+ cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'default')};
+`;
+
+const Star = styled.span`
+ color: #ff5362;
+`;
+
+const Description = styled.div`
+ display: flex;
+ margin-bottom: 8px;
+ line-height: 17px;
+`;
+
+const LabelContainer = styled.div`
+ display: flex;
+ margin-bottom: 8px;
+ align-items: center;
+ gap: 4px;
+ height: 20px;
+ align-items: center;
+`;
+
+export const ErrorContainer = styled.div`
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+ gap: 4px;
+`;
+
+export const BaseInput = styled.input<{
+ error?: string;
+ isRightSection?: boolean;
+ isLeftSection?: boolean;
+ height?: string | number;
+ cursorPointer?: boolean;
+}>`
+ width: 100%;
+ padding: ${({ isRightSection, isLeftSection }) => {
+ if (isLeftSection) return '6px 12px 6px 34px';
+ return isRightSection ? '6px 36px 6px 12px' : '6px 12px';
+ }};
+ outline: none;
+ border: 1px solid ${color['grey-50']};
+ font-style: normal;
+ font-weight: 500;
+ font-size: ${typography.size.xs}px;
+ color: ${color['grey-80']};
+ background: ${color.white};
+ border-radius: 8px;
+ height: ${p => (typeof p.height === 'number' ? `${p.height}px` : p.height || '34px')};
+
+ &:disabled {
+ border: none;
+ background: ${color['grey-20']};
+ color: ${color['grey-50']};
+ cursor: not-allowed;
+ }
+
+ &::placeholder {
+ color: ${color['grey-50']};
+ font-size: ${typography.size.xs}px;
+ }
+
+ ${({ error }) =>
+ error
+ ? `
+ border: 1px solid ${color['error-30']};
+ background: ${color['error-10']};
+ `
+ : `
+ &:focus-visible,
+ &:focus {
+ border: 1px solid ${color['grey-80']};
+ }
+ `}
+ cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'default')};
+`;
+
+const QuestionIconWrapper = styled.i`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+`;
+
+const InputChildrenWrapper = styled.div`
+ position: relative;
+`;
+
+const RightSectionWrapper = styled.div`
+ position: absolute;
+ display: flex;
+ top: 50%;
+ transform: translateY(-50%);
+ right: 8px;
+`;
+
+const LeftSectionWrapper = styled.div`
+ position: absolute;
+ display: flex;
+ top: 50%;
+ transform: translateY(-50%);
+ padding-left: 10px;
+`;
diff --git a/src/components/Input/PasswordInput.tsx b/src/components/Input/PasswordInput.tsx
new file mode 100644
index 00000000..2fdd9c76
--- /dev/null
+++ b/src/components/Input/PasswordInput.tsx
@@ -0,0 +1,82 @@
+import { forwardRef, useMemo, useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { BaseInput, type InputBaseProps, InputContainer } from './InputContainer';
+import Icon from '../Icon';
+import { color } from '../styles';
+
+export type PasswordInputProps = Omit;
+
+export const PasswordInput = forwardRef(
+ (
+ {
+ label,
+ placeholder,
+ description,
+ name,
+ value,
+ error,
+ disabled,
+ onChange,
+ defaultValue,
+ required = false,
+ width,
+ tooltip,
+ ...props
+ },
+ ref,
+ ) => {
+ const inputStyle = useMemo(() => {
+ return {
+ color: disabled ? color['grey-50'] : color['grey-60'],
+ size: 18,
+ };
+ }, [disabled]);
+
+ const [showPassword, setShowPassword] = useState(false);
+ const toggleShowPassword = () => {
+ setShowPassword(!showPassword);
+ };
+
+ return (
+
+ {showPassword ? : }
+
+ }
+ {...props}
+ >
+
+
+ );
+ },
+);
+
+const PasswordInputIcon = styled.button`
+ margin: auto;
+ background: inherit;
+ border: none;
+ overflow: visible;
+ height: 100%;
+ cursor: pointer;
+ display: flex;
+`;
diff --git a/src/components/Input/TextInput.tsx b/src/components/Input/TextInput.tsx
new file mode 100644
index 00000000..18dcfc20
--- /dev/null
+++ b/src/components/Input/TextInput.tsx
@@ -0,0 +1,47 @@
+import { forwardRef } from 'react';
+
+import { BaseInput, type InputBaseProps, InputContainer } from './InputContainer';
+
+export type TextInputProps = Omit;
+
+export const TextInput = forwardRef(
+ (
+ {
+ label,
+ placeholder,
+ description,
+ name,
+ value,
+ error,
+ disabled,
+ onChange,
+ defaultValue,
+ required = false,
+ width,
+ ...props
+ },
+ ref,
+ ) => {
+ return (
+
+
+
+ );
+ },
+);
diff --git a/src/components/Input/hook.ts b/src/components/Input/hook.ts
new file mode 100644
index 00000000..90efeae4
--- /dev/null
+++ b/src/components/Input/hook.ts
@@ -0,0 +1,35 @@
+import { useEffect, type RefObject } from 'react';
+
+const useClickOutside = (
+ ref: RefObject,
+ handler: (event: MouseEvent | TouchEvent) => void,
+) => {
+ useEffect(() => {
+ let startedInside = false;
+ let startedWhenMounted = false;
+
+ const listener = (event: MouseEvent | TouchEvent) => {
+ if (startedInside || !startedWhenMounted) return;
+ if (ref.current == null || ref.current.contains(event.target as Node)) return;
+
+ handler(event);
+ };
+
+ const validateEventStart = (event: MouseEvent | TouchEvent) => {
+ startedWhenMounted = !(ref.current == null);
+ startedInside = !(ref.current == null) && ref.current.contains(event.target as Node);
+ };
+
+ document.addEventListener('mousedown', validateEventStart);
+ document.addEventListener('touchstart', validateEventStart);
+ document.addEventListener('click', listener);
+
+ return () => {
+ document.removeEventListener('mousedown', validateEventStart);
+ document.removeEventListener('touchstart', validateEventStart);
+ document.removeEventListener('click', listener);
+ };
+ }, [ref, handler]);
+};
+
+export default useClickOutside;
diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx
new file mode 100644
index 00000000..a74eb70b
--- /dev/null
+++ b/src/components/Input/index.tsx
@@ -0,0 +1,6 @@
+import { type InputBaseProps, InputContainer } from './InputContainer';
+import { PasswordInput, type PasswordInputProps } from './PasswordInput';
+import { TextInput, type TextInputProps } from './TextInput';
+
+export { InputContainer, TextInput, PasswordInput };
+export type { InputBaseProps, TextInputProps, PasswordInputProps };
diff --git a/src/components/Modal/Alert.tsx b/src/components/Modal/Alert.tsx
new file mode 100644
index 00000000..ecfc5ad5
--- /dev/null
+++ b/src/components/Modal/Alert.tsx
@@ -0,0 +1,32 @@
+import { MessageModal } from './MessageModal';
+import { Button } from '..';
+
+export interface AlertInfo {
+ title: string;
+ contents?: string;
+}
+
+export interface AlertProps extends AlertInfo {
+ opened: boolean;
+ close: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+export function Alert({ title, contents, opened, close }: AlertProps) {
+ return (
+
+ {close.label}
+
+ }
+ opened={opened}
+ borderRadius={8}
+ onClose={close.onClick}
+ />
+ );
+}
diff --git a/src/components/Modal/Confirm.tsx b/src/components/Modal/Confirm.tsx
new file mode 100644
index 00000000..0b7a5d7b
--- /dev/null
+++ b/src/components/Modal/Confirm.tsx
@@ -0,0 +1,41 @@
+import { useTranslation } from 'react-i18next';
+
+import { MessageModal } from './MessageModal';
+import { Button } from '..';
+
+export interface ConfirmProps {
+ title: string;
+ contents?: string;
+ opened: boolean;
+ cancel: {
+ label?: string;
+ onClick: () => void;
+ };
+ confirm: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+export function Confirm({ title, contents, opened, cancel, confirm }: ConfirmProps) {
+ const { t } = useTranslation(['common']);
+ return (
+
+
+
+ >
+ }
+ opened={opened}
+ borderRadius={8}
+ onClose={cancel.onClick}
+ />
+ );
+}
diff --git a/src/components/Modal/Drawer.tsx b/src/components/Modal/Drawer.tsx
new file mode 100644
index 00000000..c86a82aa
--- /dev/null
+++ b/src/components/Modal/Drawer.tsx
@@ -0,0 +1,59 @@
+import { type ReactElement } from 'react';
+
+import styled from '@emotion/styled';
+
+import { ModalContainer } from './ModalContainer';
+import Icon from '../Icon';
+import { H2 } from '../Text';
+import { color } from '../styles';
+
+const Container = styled.div`
+ background-color: ${color.white};
+ display: flex;
+ flex-direction: column;
+ width: 480px;
+ padding: 40px 30px 30px 30px;
+ height: 100vh;
+ gap: 40px;
+`;
+
+const Header = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+`;
+
+const CloseIconContainer = styled.div`
+ cursor: pointer;
+`;
+
+interface Props {
+ title: string;
+ opened: boolean;
+ onClose: () => void;
+ children: ReactElement | ReactElement[];
+ closeOnBackdropClick?: boolean;
+}
+
+export function Drawer({ title, opened, onClose, children, closeOnBackdropClick }: Props) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/Modal/MessageModal.tsx b/src/components/Modal/MessageModal.tsx
new file mode 100644
index 00000000..66b22e79
--- /dev/null
+++ b/src/components/Modal/MessageModal.tsx
@@ -0,0 +1,59 @@
+import styled from '@emotion/styled';
+
+import { ModalContainer } from './ModalContainer';
+import { B2, H2 } from '../Text';
+import { color } from '../styles';
+import { ReactNode } from 'react';
+
+const Base = styled.div`
+ background-color: ${color.white};
+ display: flex;
+ width: 600px;
+ padding: 24px;
+ flex-direction: column;
+ gap: 16px;
+ white-space: pre-line;
+`;
+
+const ActionsContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ justify-content: flex-end;
+ white-space: pre-line;
+`;
+
+interface Props {
+ title: string;
+ contents?: string;
+ bottom: ReactNode;
+ opened: boolean;
+ onClose: () => void;
+ borderRadius: number;
+ closeOnBackdropClick?: boolean;
+}
+
+export function MessageModal({
+ title,
+ contents,
+ bottom,
+ opened,
+ onClose,
+ borderRadius,
+ closeOnBackdropClick = false,
+}: Props) {
+ return (
+
+
+ {title}
+ {contents && {contents}}
+ {bottom}
+
+
+ );
+}
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
new file mode 100644
index 00000000..bf1e4414
--- /dev/null
+++ b/src/components/Modal/Modal.tsx
@@ -0,0 +1,91 @@
+import { type ReactElement } from 'react';
+
+import styled from '@emotion/styled';
+
+import { ModalContainer } from './ModalContainer';
+import { BottomBar } from '../BottomBar';
+import Icon from '../Icon';
+import { B1, H1 } from '../Text';
+import { color } from '../styles';
+
+const Container = styled.div`
+ background-color: ${color.white};
+ display: inline-flex;
+ width: 600px;
+ padding: 0;
+ flex-direction: column;
+`;
+
+const HeaderAndBody = styled.div`
+ padding: 50px 40px 20px 40px;
+`;
+
+const Header = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 50px;
+`;
+
+const TitleAndDescription = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+const CloseIconContainer = styled.div`
+ cursor: pointer;
+`;
+
+const Body = styled.div``;
+
+interface Props {
+ title: string;
+ subtitle: string;
+ children: ReactElement;
+ opened: boolean;
+ closeOnBackdropClick?: boolean;
+ cancel: {
+ label: string;
+ onClick: () => void;
+ };
+ confirm: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+export function Modal({
+ title,
+ subtitle,
+ opened,
+ cancel,
+ confirm,
+ children,
+ closeOnBackdropClick,
+}: Props) {
+ return (
+
+
+
+
+
+ {title}
+ {subtitle}
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/components/Modal/ModalContainer.tsx b/src/components/Modal/ModalContainer.tsx
new file mode 100644
index 00000000..cafc45e3
--- /dev/null
+++ b/src/components/Modal/ModalContainer.tsx
@@ -0,0 +1,78 @@
+import React, { type ReactElement, useEffect, useRef } from 'react';
+
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+
+interface Props {
+ opened: boolean;
+ children: ReactElement | ReactElement[];
+ borderRadius: number;
+ onClose: () => void;
+ closeOnBackdropClick?: boolean;
+ drawer?: boolean;
+}
+
+const Dialog = styled.dialog<{ borderRadius: number; drawer?: boolean }>`
+ &::backdrop {
+ background-color: ${color['grey-80']};
+ opacity: 0.5;
+ }
+ margin: auto;
+ border-radius: ${props => props.borderRadius}px;
+ border: none;
+ padding: 0;
+ ${({ drawer }) =>
+ drawer &&
+ `
+ position: fixed;
+ left: 100%;
+ transform: translateX(-100%);
+ height: 100vh;
+ &:modal {
+ max-height: 100vh;
+ }`}
+
+ box-shadow: 0px 0px 1px 0px rgba(132, 132, 132, 0.31),
+ 0px 2px 5px 0px rgba(70, 70, 70, 0.2);
+`;
+
+export function ModalContainer({
+ opened,
+ children,
+ borderRadius,
+ onClose,
+ closeOnBackdropClick = true,
+ drawer = false,
+}: Props) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (opened) {
+ ref.current?.showModal();
+ } else {
+ ref.current?.close();
+ }
+ }, [opened]);
+
+ function onClickBackdrop(e: React.MouseEvent) {
+ if (closeOnBackdropClick) {
+ const target = e.target as HTMLDialogElement;
+ if (target === ref.current) {
+ onClose();
+ }
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
new file mode 100644
index 00000000..fe286e4a
--- /dev/null
+++ b/src/components/Modal/index.tsx
@@ -0,0 +1,4 @@
+export * from './Alert';
+export * from './Confirm';
+export * from './Drawer';
+export * from './Modal';
diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx
new file mode 100644
index 00000000..922a9d71
--- /dev/null
+++ b/src/components/Navigation/index.tsx
@@ -0,0 +1,160 @@
+import { type ReactElement, type ReactNode, cloneElement, createElement, useCallback } from 'react';
+
+import styled from '@emotion/styled';
+
+import { type IconProps } from '../Icon/type';
+import { Text } from '../Text';
+import { color, typography } from '../styles';
+
+interface NavItemType {
+ icon: (icon: IconProps) => ReactElement;
+ label: string;
+ href: string;
+ selected?: boolean;
+}
+
+export interface NavbarGroup {
+ title: string;
+ navItems: NavItemType[];
+}
+
+const Container = styled.nav`
+ display: inline-flex;
+ overflow-y: hidden;
+ height: 100vh;
+ padding: 25px 14px 18px 14px;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-shrink: 0;
+ background: ${color.white};
+ box-shadow: 0px 4px 15px 0px rgba(0, 0, 0, 0.1);
+`;
+
+const NavContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+`;
+
+const LogoContainer = styled.div`
+ display: flex;
+ width: 212px;
+ height: 60px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const NavigationList = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 20px;
+`;
+
+const Section = styled.ol`
+ display: flex;
+ width: 212px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 6px;
+`;
+
+const TitleWrapper = styled.div`
+ width: 100%;
+ padding: 0 8px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+`;
+
+const NavItem = styled.li<{ selectedColor?: string; selected: boolean }>`
+ display: flex;
+ width: 212px;
+ height: 36px;
+ padding: 8px;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ border-radius: 8px;
+ cursor: pointer;
+ ${({ selected, selectedColor }) =>
+ selected
+ ? `
+ background: ${selectedColor};
+ span {
+ font-weight: ${typography.weight.semibold} !important;
+ }
+ `
+ : `&:hover {background: ${color['grey-20']};}`}
+`;
+
+const BottomSection = styled.div`
+ width: 100%;
+`;
+
+export function Navigation({
+ logoImage,
+ logoHref,
+ bottom,
+ navbarGroup,
+ selectedColor,
+ LinkElement,
+}: {
+ logoImage: { src: string; width: number; height: number };
+ logoHref: string;
+ bottom?: ReactNode;
+ navbarGroup: NavbarGroup[];
+ selectedColor?: string;
+ LinkElement?: ReactElement;
+}) {
+ const Link = useCallback(
+ ({ href, children }: { href: string; children: ReactElement }) =>
+ LinkElement != null
+ ? cloneElement(LinkElement, { href }, children)
+ : createElement('a', { href }, children),
+ [],
+ );
+ return (
+
+
+
+
+
+
+
+
+ {navbarGroup.map(section => (
+
+
+
+ {section.title}
+
+
+ {section.navItems.map(item => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+ {item.label}
+
+
+
+ );
+ })}
+
+ ))}
+
+
+ {bottom}
+
+ );
+}
diff --git a/src/components/Pagination/index.tsx b/src/components/Pagination/index.tsx
new file mode 100644
index 00000000..7ce03b5a
--- /dev/null
+++ b/src/components/Pagination/index.tsx
@@ -0,0 +1,103 @@
+import { useMemo } from 'react';
+
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { B2 } from '../Text';
+import { color } from '../styles';
+
+const Item = styled.button<{ selected?: boolean; borderRadius: number }>`
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ border: 1px solid ${props => (props.selected ? color['grey-50'] : color['grey-40'])};
+ border-radius: ${props => props.borderRadius}px;
+ background-color: ${props => (props.selected ? color['grey-30'] : color.white)};
+ cursor: ${props => (props.disabled ? 'default' : 'pointer')};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Container = styled.div`
+ display: flex;
+ gap: 6px;
+`;
+
+interface Props {
+ current: number;
+ total: number;
+ onSelect: (page: number) => void;
+}
+
+const range = (start: number, end: number): number[] => {
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
+};
+
+const COUNT_PER_PAGE = 5;
+
+export function Pagination({ current, total, onSelect }: Props) {
+ const { start, end, visiblePages, showMore, canGoNext, canGoPrev } = useMemo(() => {
+ const start = (Math.ceil(current / COUNT_PER_PAGE) - 1) * COUNT_PER_PAGE + 1;
+ const end = Math.min(start + COUNT_PER_PAGE - 1, total);
+ const visiblePages = range(start, end);
+
+ const showMore = total > COUNT_PER_PAGE;
+
+ const canGoPrev = current > COUNT_PER_PAGE;
+ const canGoNext = end < total;
+
+ return {
+ start,
+ end,
+ visiblePages,
+ showMore,
+ canGoNext,
+ canGoPrev,
+ };
+ }, [current, total]);
+
+ return (
+
+ {showMore && (
+ - {
+ if (canGoPrev) {
+ onSelect(start - 1);
+ }
+ }}
+ >
+
+
+ )}
+ {visiblePages.map(page => (
+ - {
+ onSelect(page);
+ }}
+ >
+ {page}
+
+ ))}
+ {showMore && (
+ - {
+ if (canGoNext) {
+ onSelect(end + 1);
+ }
+ }}
+ >
+
+
+ )}
+
+ );
+}
diff --git a/src/components/Radio/Group.tsx b/src/components/Radio/Group.tsx
new file mode 100644
index 00000000..aad895a0
--- /dev/null
+++ b/src/components/Radio/Group.tsx
@@ -0,0 +1,51 @@
+import styled from '@emotion/styled';
+
+import { RadioItem, type RadioItemProps } from './RadioItem';
+
+export type RadioGroupOption = Omit, 'onSelect' | 'selected'>;
+
+type RadioDirection = 'row' | 'column';
+
+const Container = styled.fieldset<{ direction: RadioDirection; gap: number }>`
+ // reset fieldset css
+ border: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ display: flex;
+ flex-direction: ${({ direction }) => direction};
+ gap: ${({ gap }) => gap}px;
+`;
+
+interface RadioGroupProps extends Omit, 'value' | 'disabled' | 'selected'> {
+ selectedValue?: T;
+ options: Array>;
+ direction?: RadioDirection;
+ gap?: number;
+}
+
+export function RadioGroup({
+ options,
+ selectedValue,
+ onSelect,
+ direction = 'row',
+ gap = 12,
+}: RadioGroupProps) {
+ return (
+
+ {options.map((option, i) => (
+ {
+ onSelect(option.value);
+ }}
+ tooltip={option.tooltip}
+ />
+ ))}
+
+ );
+}
diff --git a/src/components/Radio/RadioItem.tsx b/src/components/Radio/RadioItem.tsx
new file mode 100644
index 00000000..2e9b5237
--- /dev/null
+++ b/src/components/Radio/RadioItem.tsx
@@ -0,0 +1,90 @@
+import { useMemo } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B3 } from '../Text';
+import { Tooltip } from '../Tooltip';
+import { color } from '../styles';
+
+const Circle = styled.input`
+ width: 16px;
+ height: 16px;
+ border-radius: 100px;
+ appearance: none;
+ margin: 0;
+
+ background-color: ${color.white};
+ border: 1px solid ${color['grey-50']};
+
+ &:disabled {
+ background-color: ${color['grey-20']};
+ border: 1px solid ${color['grey-40']};
+ }
+ &:checked {
+ border: 4px solid ${color['main-black']};
+ }
+ &:checked:disabled {
+ border: 4px solid ${color['grey-40']};
+ }
+`;
+
+const Container = styled.div`
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+`;
+
+const Label = styled.label`
+ display: contents;
+`;
+
+export interface RadioItemProps {
+ label?: string;
+ id?: string;
+ value: T;
+ disabled?: boolean;
+ selected: boolean;
+ onSelect: (value: T) => void;
+ tooltip?: string;
+}
+
+export function RadioItem({
+ label,
+ id,
+ value,
+ disabled,
+ selected,
+ onSelect,
+ tooltip,
+}: RadioItemProps) {
+ const htmlId = useMemo(() => id ?? `${label}:${value}`, [label, value]);
+ const RadioCircle = (
+ {
+ onSelect(value);
+ }}
+ />
+ );
+ return (
+
+ {/* TODO: direction top-left๋ก ์์ ๋์ด์ผํจ ADCIO-2034, ADCIO-2081 */}
+ {tooltip ? (
+
+ {RadioCircle}
+
+ ) : (
+ RadioCircle
+ )}
+ {label && (
+
+ )}
+
+ );
+}
diff --git a/src/components/Radio/index.tsx b/src/components/Radio/index.tsx
new file mode 100644
index 00000000..d7c746b8
--- /dev/null
+++ b/src/components/Radio/index.tsx
@@ -0,0 +1,7 @@
+import { RadioGroup } from './Group';
+import { RadioItem } from './RadioItem';
+
+export const Radio = {
+ Group: RadioGroup,
+ Item: RadioItem,
+};
diff --git a/src/components/Select/OptionList.tsx b/src/components/Select/OptionList.tsx
new file mode 100644
index 00000000..4b57c72f
--- /dev/null
+++ b/src/components/Select/OptionList.tsx
@@ -0,0 +1,201 @@
+import { Fragment, type ReactElement, type ReactNode, useEffect, useRef } from 'react';
+
+import styled from '@emotion/styled';
+
+import { type IconProps } from '../Icon/type';
+import { B3, B4 } from '../Text';
+import { color } from '../styles';
+
+export interface BasicOptionItem {
+ label: string;
+ value: T;
+ icon?: (props: IconProps) => ReactElement;
+ disabled?: boolean;
+}
+
+export interface SectionOptionItem {
+ sectionTitle: ReactNode;
+ sectionItems: Array>;
+}
+
+interface BasicOptionList {
+ type: 'basic';
+ items: Array>;
+}
+
+interface SectionOptionList {
+ type: 'section';
+ items: Array>;
+}
+
+interface OptionListProps {
+ width?: number;
+ onChange: (value: T) => void;
+ option: BasicOptionList | SectionOptionList;
+ value: T;
+ maxDropdownItemsToShow?: number;
+ focusedItemIdx?: number;
+}
+
+const LIST_ITEM_HEIGHT = 32;
+
+const ListContainer = styled.ul<{
+ width?: number;
+ maxDropdownItemsToShow: number;
+}>`
+ display: flex;
+ width: ${({ width }) => `${width}px` ?? '100%'};
+ max-height: ${({ maxDropdownItemsToShow }) =>
+ `${maxDropdownItemsToShow * (LIST_ITEM_HEIGHT + 4) + 20 - 4}px`};
+ height: 100%;
+ padding: 10px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ border-radius: 8px;
+ background: ${color.white};
+ box-shadow:
+ 0px 0px 1px 0px rgba(132, 132, 132, 0.31),
+ 0px 2px 5px 0px rgba(70, 70, 70, 0.2);
+ overflow: auto;
+ list-style: none;
+`;
+
+const SectionTitleWrapper = styled.li`
+ padding: 6px 8px;
+`;
+
+const ItemList = styled.li`
+ width: 100%;
+`;
+
+const ItemButton = styled.button<{
+ selected?: boolean;
+ disabled?: boolean;
+ focused: boolean;
+}>`
+ border: none;
+ overflow: visible;
+ cursor: pointer;
+ display: flex;
+ width: 100%;
+ min-height: ${LIST_ITEM_HEIGHT}px;
+ padding: 6px 8px;
+ align-items: center;
+ align-self: stretch;
+ gap: 6px;
+ border-radius: 4px;
+ text-align: left;
+ background-color: ${({ selected, focused }) =>
+ selected ? color['grey-30'] : focused ? color['grey-20'] : color.white};
+ &:hover {
+ background-color: ${({ disabled, selected }) =>
+ disabled ? 'none' : selected ? color['grey-40'] : color['grey-20']};
+ }
+ svg {
+ display: list-item;
+ flex-shrink: 0;
+ }
+ span {
+ word-break: break-word;
+ }
+
+ ${({ disabled }) =>
+ disabled &&
+ `background: none;
+ cursor: not-allowed;
+`}
+`;
+
+function OptionItems({
+ value,
+ items,
+ onChange,
+ focusedItemIdx,
+}: {
+ value: T;
+ items: Array>;
+ onChange: (value: T) => void;
+ focusedItemIdx: number;
+}) {
+ return (
+ <>
+ {items.map((item, i) => {
+ const Icon = item.icon;
+ return (
+
+ {
+ if (!item.disabled) {
+ onChange(item.value);
+ }
+ }}
+ >
+ {Icon != null && (
+
+ )}
+ {item.label}
+
+
+ );
+ })}
+ >
+ );
+}
+
+export function OptionList({
+ onChange,
+ option,
+ value,
+ width,
+ maxDropdownItemsToShow = 6,
+ focusedItemIdx = -1,
+}: OptionListProps) {
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ if (option.items.length === 0) {
+ return;
+ }
+ if (focusedItemIdx === -1) {
+ scrollRef.current!.children[0]?.scrollIntoView({
+ block: 'nearest',
+ });
+ return;
+ }
+ scrollRef.current!.children[focusedItemIdx]?.scrollIntoView({
+ block: 'nearest',
+ });
+ }, [focusedItemIdx, option.items.length]);
+
+ return (
+
+ {option.type === 'section' ? (
+ option.items.map((section, i) => (
+
+
+ {section.sectionTitle}
+
+
+
+ ))
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx
new file mode 100644
index 00000000..9bb17fe3
--- /dev/null
+++ b/src/components/Select/Select.tsx
@@ -0,0 +1,178 @@
+import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { type BasicOptionItem, OptionList } from './OptionList';
+import { SelectInput, type SelectInputBaseProps } from './SelectInput';
+import Icon from '../Icon';
+
+interface SelectProps
+ extends Omit {
+ searchable?: boolean;
+ onCreate?: (value: string | null) => void;
+ selectedValue: T | null;
+ onSelect: (value: T) => void;
+ options: Array>;
+ width?: number;
+ inputWidth?: number;
+}
+
+const SelectContainer = styled.div<{ width?: number }>`
+ position: relative;
+ width: ${({ width }) => `${width}px` ?? '100%'};
+`;
+
+const OptionListWrapper = styled.div`
+ position: absolute;
+ width: 100%;
+ margin-top: 6px;
+ z-index: 100;
+`;
+
+const CREATE_VALUE = 'CREATE_NEW_VALUE';
+
+export function Select({
+ searchable,
+ onCreate,
+ options,
+ selectedValue,
+ onSelect,
+ width,
+ inputWidth,
+ ...input
+}: SelectProps) {
+ const wrapperRef = useRef(null);
+ const dropdownRef = useRef(null);
+
+ const [showDropdown, setShowDropdown] = useState(false);
+ const [searchLabel, setSearchLabel] = useState(null);
+ const [focusedItemIdx, setFocusedItemIdx] = useState(-1);
+
+ const selectedLabel = useMemo(
+ () => options.find(item => item.value === selectedValue)?.label ?? '',
+ [selectedValue],
+ );
+ const optionItems: BasicOptionItem[] = useMemo(() => {
+ if (searchable && searchLabel) {
+ const searchFilteredOptions = options.filter(option =>
+ option.label.toLocaleLowerCase().includes(searchLabel.toLocaleLowerCase()),
+ );
+ if (onCreate != null) {
+ return [
+ ...searchFilteredOptions,
+ { icon: Icon.Add, label: searchLabel, value: CREATE_VALUE },
+ ];
+ }
+ return searchFilteredOptions;
+ }
+ return options;
+ }, [searchable, options, searchLabel]);
+
+ const clearDropdownAndSearch = useCallback(() => {
+ setSearchLabel(null);
+ setFocusedItemIdx(-1);
+ setShowDropdown(false);
+ }, []);
+
+ const handleKeyUpEvent = useCallback(
+ (event: KeyboardEvent) => {
+ switch (event.key) {
+ case 'ArrowDown':
+ return setFocusedItemIdx(prev => (prev === optionItems.length - 1 ? -1 : prev + 1));
+ case 'ArrowUp':
+ return setFocusedItemIdx(prev => (prev === -1 ? optionItems.length - 1 : prev - 1));
+ case 'Enter': {
+ if (focusedItemIdx === -1) {
+ return;
+ }
+
+ const item = optionItems[focusedItemIdx];
+ if (item.value === CREATE_VALUE) {
+ return;
+ }
+ onSelect(item.value as T);
+ clearDropdownAndSearch();
+ }
+ }
+ },
+ [setFocusedItemIdx, onSelect, setShowDropdown, optionItems, focusedItemIdx],
+ );
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (wrapperRef.current != null && !wrapperRef.current.contains(event.target as Node)) {
+ clearDropdownAndSearch();
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [wrapperRef]);
+
+ useEffect(() => {
+ if (!showDropdown || !optionItems?.length || !searchable) {
+ return;
+ }
+
+ const handleKeyEvent = (event: any) => {
+ handleKeyUpEvent(event);
+ };
+
+ document.addEventListener('keyup', handleKeyEvent);
+ return () => {
+ document.removeEventListener('keyup', handleKeyEvent);
+ };
+ }, [dropdownRef, showDropdown, optionItems, searchable, focusedItemIdx]);
+
+ return (
+
+ {
+ if (searchable) {
+ if (!showDropdown) setShowDropdown(true);
+ setSearchLabel(e.currentTarget.value);
+ }
+ }}
+ onFocus={() => {
+ if (searchable) {
+ setSearchLabel('');
+ }
+ }}
+ onClick={() => {
+ if (showDropdown) {
+ setFocusedItemIdx(-1);
+ }
+ setShowDropdown(!showDropdown);
+ }}
+ readOnly={!searchable}
+ {...input}
+ />
+ {showDropdown && !(optionItems.length === 0) && (
+
+ {
+ if (item === CREATE_VALUE) {
+ onCreate?.(searchLabel);
+ onSelect(searchLabel as T);
+ } else {
+ onSelect(item as T);
+ }
+ clearDropdownAndSearch();
+ }}
+ />
+
+ )}
+
+ );
+}
diff --git a/src/components/Select/SelectInput.tsx b/src/components/Select/SelectInput.tsx
new file mode 100644
index 00000000..9881fde0
--- /dev/null
+++ b/src/components/Select/SelectInput.tsx
@@ -0,0 +1,186 @@
+import { type InputHTMLAttributes } from 'react';
+
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { B1, B3, B5 } from '../Text';
+import { Tooltip, type TooltipProps } from '../Tooltip';
+import { color, typography } from '../styles';
+
+type SelectInputTooltipProps = Omit;
+
+/** RightIcon prop can only be used with searchable true.
+ * Without searchable, the right icon is always ChevronDownSmall as CDS design. */
+export interface SelectInputBaseProps extends Omit, 'width'> {
+ label?: string;
+ description?: string;
+ error?: string;
+ width?: number;
+ tooltip?: SelectInputTooltipProps;
+ dropdownOpened?: boolean;
+ searchable?: boolean;
+ showIcon?: boolean;
+}
+
+export function SelectInput({
+ label,
+ description,
+ required = false,
+ error,
+ width,
+ tooltip,
+ dropdownOpened,
+ searchable,
+ onClick,
+ showIcon = true,
+ ...props
+}: SelectInputBaseProps) {
+ return (
+
+ {label && (
+
+
+ {label} {required && *}
+
+ {tooltip != null && (
+
+
+
+
+
+ )}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {showIcon && (
+
+ {searchable ? : }
+
+ )}
+
+ {error && (
+
+
+ {error}
+
+ )}
+
+ );
+}
+
+const SelectInputWrapper = styled.div<{ width?: number; cursor?: string }>`
+ width: ${({ width }) => `${width}px` ?? '100%'};
+ position: relative;
+ cursor: ${({ cursor }) => cursor ?? 'auto'};
+`;
+
+const Star = styled.span`
+ color: #ff5362;
+`;
+
+const Description = styled.div`
+ display: flex;
+ margin-bottom: 8px;
+`;
+
+const LabelContainer = styled.div`
+ display: flex;
+ margin-bottom: 8px;
+ align-items: center;
+ gap: 4px;
+`;
+
+const ErrorContainer = styled.div`
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+ gap: 4px;
+`;
+
+const BaseSelectInput = styled.input<{
+ error?: string;
+ isRightSection?: boolean;
+ cursor?: string;
+}>`
+ width: 100%;
+ padding: ${({ isRightSection }) => (isRightSection ? '6px 36px 6px 12px' : '6px 12px')};
+ outline: none;
+ border: 1px solid ${color['grey-50']};
+ font-style: normal;
+ font-weight: 500;
+ font-size: ${typography.size.xs}px;
+ color: ${color['grey-80']};
+ background: ${color.white};
+ border-radius: 8px;
+ height: 34px;
+ cursor: ${({ cursor }) => cursor ?? 'auto'};
+
+ &:disabled {
+ border: none;
+ background: ${color['grey-10']};
+ color: ${color['grey-50']};
+ cursor: not-allowed;
+ }
+
+ &::placeholder {
+ color: ${color['grey-50']};
+ font-size: ${typography.size.xs}px;
+ }
+
+ ${({ error }) =>
+ error
+ ? `
+ border: 1px solid ${color['error-30']};
+ background: ${color['error-10']};
+ `
+ : `
+ &:focus-visible,
+ &:focus {
+ border: 1px solid ${color['grey-80']};
+ }
+ `}
+`;
+
+const QuestionIconWrapper = styled.i`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+`;
+
+const SelectInputChildrenWrapper = styled.div`
+ position: relative;
+`;
+
+const RightSectionWrapper = styled.button<{ cursor?: string; rotate: string }>`
+ position: absolute;
+ top: 0%;
+ padding: 6px 8px;
+ right: 0;
+ background: inherit;
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ overflow: visible;
+ height: 100%;
+ cursor: ${({ cursor }) => cursor ?? 'pointer'};
+ transform: ${({ rotate }) => rotate ?? 'none'};
+`;
diff --git a/src/components/Select/index.ts b/src/components/Select/index.ts
new file mode 100644
index 00000000..c7a31eed
--- /dev/null
+++ b/src/components/Select/index.ts
@@ -0,0 +1,5 @@
+import { type BasicOptionItem, OptionList, type SectionOptionItem } from './OptionList';
+import { Select } from './Select';
+
+export { OptionList, Select };
+export type { BasicOptionItem, SectionOptionItem };
diff --git a/src/components/Spinner/index.tsx b/src/components/Spinner/index.tsx
new file mode 100644
index 00000000..58428bc9
--- /dev/null
+++ b/src/components/Spinner/index.tsx
@@ -0,0 +1,75 @@
+import { keyframes } from '@emotion/react';
+import { useId } from 'react';
+
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+
+export type SpinnerSize = 's' | 'm' | 'l';
+
+interface SpinnerProps {
+ color?: string;
+ size?: SpinnerSize;
+ speed?: number;
+}
+
+const SpinnerSizeMap = { s: 20, m: 25, l: 30 };
+
+export function Spinner({ color: c = color['main-black'], size = 'm', speed = 1 }: SpinnerProps) {
+ const spinnerSize = SpinnerSizeMap[size];
+ const gradientId = useId();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const spinAnimation = keyframes`
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+`;
+
+const SpinnerSvg = styled.svg<{ speed: number }>`
+ animation: ${spinAnimation} ${({ speed }) => speed}s linear infinite;
+`;
diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx
new file mode 100644
index 00000000..27d4c56f
--- /dev/null
+++ b/src/components/Switch/index.tsx
@@ -0,0 +1,101 @@
+import styled from '@emotion/styled';
+
+import { color, typography } from '../styles';
+
+interface Props {
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+ disabled?: boolean;
+}
+
+export function Switch({ checked, onChange, disabled = false }: Props) {
+ const SwitchTextComponent = checked ? OnLabel : OffLabel;
+
+ return (
+
+ {
+ onChange(!checked);
+ }}
+ disabled={disabled}
+ />
+
+
+ {checked ? 'ON' : 'OFF'}
+
+
+
+ );
+}
+
+type StylesProps = Omit;
+
+const SWITCH_WIDTH = 38;
+const SWITCH_HEIGHT = 18;
+const SWITCH_CIRCLE_SIZE = 12;
+const SWITCH_CIRCLE_GAP = 3;
+
+const Container = styled.label`
+ position: relative;
+ width: ${SWITCH_WIDTH}px;
+ height: ${SWITCH_HEIGHT}px;
+ user-select: none;
+ display: inline-block;
+`;
+
+const Slider = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
+ background: ${props =>
+ props.disabled ? color['grey-50'] : props.checked ? color['main-black'] : color['grey-40']};
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+
+ &:before {
+ content: '';
+ z-index: 10;
+ width: ${SWITCH_CIRCLE_SIZE}px;
+ height: ${SWITCH_CIRCLE_SIZE}px;
+ background: ${({ disabled }) => (disabled ? color['grey-60'] : color.white)};
+ border-radius: 50%;
+ transition: transform ease 0.2s;
+ transform: ${props =>
+ `translateX(${
+ props.checked ? SWITCH_WIDTH - SWITCH_CIRCLE_SIZE - SWITCH_CIRCLE_GAP : SWITCH_CIRCLE_GAP
+ }px)`};
+ }
+`;
+
+const CheckBox = styled.input`
+ opacity: 0;
+ width: 0;
+ height: 0;
+`;
+
+const SliderText = styled.span`
+ position: absolute;
+ font-size: 9px;
+ font-style: normal;
+ font-weight: ${typography.weight.medium};
+ font-family: 'Pretendard Variable', Pretendard;
+ line-height: 1.2;
+`;
+
+const OnLabel = styled(SliderText)`
+ left: 5px;
+ color: ${({ disabled }) => (disabled ? color['grey-60'] : color.white)};
+ opacity: ${({ checked }) => (checked ? 1 : 0)};
+`;
+
+const OffLabel = styled(SliderText)`
+ right: 3.5px;
+ color: ${({ disabled }) => (disabled ? color['grey-60'] : color['grey-50'])};
+ opacity: ${({ checked }) => (checked ? 0 : 1)};
+`;
diff --git a/src/components/Table/TableContainer.tsx b/src/components/Table/TableContainer.tsx
new file mode 100644
index 00000000..7619e6c2
--- /dev/null
+++ b/src/components/Table/TableContainer.tsx
@@ -0,0 +1,19 @@
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+import { ReactNode } from 'react';
+
+export const TableContainer = ({ children }: { children: ReactNode }) => {
+ return ;
+};
+
+const Table = styled.table`
+ display: table;
+ width: 100%;
+ text-align: left;
+ vertical-align: middle;
+ border-collapse: collapse;
+ box-sizing: border-box;
+ border-spacing: 0;
+ background-color: ${color.white};
+`;
diff --git a/src/components/Table/Tbody.tsx b/src/components/Table/Tbody.tsx
new file mode 100644
index 00000000..6f80fa3d
--- /dev/null
+++ b/src/components/Table/Tbody.tsx
@@ -0,0 +1,14 @@
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+import { ReactNode } from 'react';
+
+export const Tbody = ({ children }: { children: ReactNode }) => {
+ return {children};
+};
+
+const TableBody = styled.tbody`
+ border-width: 1px 0 1px 0;
+ border-style: solid;
+ border-color: ${color['grey-30']};
+`;
diff --git a/src/components/Table/Td.tsx b/src/components/Table/Td.tsx
new file mode 100644
index 00000000..25c7069d
--- /dev/null
+++ b/src/components/Table/Td.tsx
@@ -0,0 +1,228 @@
+import { ElementType, ReactNode, type PropsWithChildren } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B2, B5 } from '../Text';
+import { color } from '../styles';
+
+export enum FixedCellType {
+ KEBAB = 'KEBAB_MENU',
+ RADIO = 'RADIO_ITEM',
+ CHECKBOX = 'CHECKBOX',
+ IMAGE = 'IMAGE',
+}
+
+export type TdSizeType = 'l' | 'm' | 's' | 'xs';
+
+const TABLE_TD_HEIGHT: Record = {
+ l: 66,
+ m: 56,
+ s: 50,
+ xs: 40,
+};
+
+const TABLE_TEXT_TD_STYLE: Record = {
+ l: {
+ textComponent: B2,
+ },
+ m: {
+ textComponent: B2,
+ },
+ s: {
+ textComponent: B2,
+ },
+ xs: {
+ textComponent: B5,
+ },
+};
+
+const TableDefaultTd = styled.td<{ width?: number; height: number }>`
+ display: table-cell;
+ align-items: center;
+ flex-shrink: 0;
+ width: ${({ width }) => (width ? `${width}px` : 'auto')};
+ height: ${({ height }) => height}px;
+ & > * {
+ vertical-align: middle;
+ }
+`;
+
+export const TextTd = ({
+ width,
+ size,
+ children,
+ ellipsis,
+}: PropsWithChildren<{
+ width?: number;
+ size: TdSizeType;
+ ellipsis?: boolean;
+}>) => {
+ const tdStyle = TABLE_TEXT_TD_STYLE[size];
+ return (
+
+ {children}
+
+ );
+};
+
+const TableText = styled(TableDefaultTd)<{
+ ellipsis?: boolean;
+}>`
+ padding: 5px 7px 5px 14px;
+ max-height: 66px;
+ vertical-align: middle;
+ border-spacing: 0;
+ ${props =>
+ props.ellipsis &&
+ `max-width: ${props.width ? `${props.width}px` : 'fit-content'};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ white-space: nowrap;`}
+ line-height:normal;
+`;
+
+const TABLE_IMG_TD_SIZE: Record = {
+ l: 8,
+ m: 7,
+ s: 6,
+ xs: 5,
+};
+
+export const ImgTd = ({ size, src }: { size: TdSizeType; src: string }) => {
+ const imgTdSize = TABLE_TD_HEIGHT[size];
+ const imgTdPadding = TABLE_IMG_TD_SIZE[size];
+ return (
+
+
+
+ );
+};
+
+const TableDataImage = styled.td<{ size: number; padding: number }>`
+ width: ${({ size }) => size}px;
+ height: ${({ size }) => size}px;
+ max-width: ${({ size }) => size}px;
+ max-height: ${({ size }) => size}px;
+ padding: ${({ padding }) => padding}px;
+ vertical-align: middle;
+ display: flex;
+`;
+
+const TableImg = styled.img`
+ display: block;
+ box-sizing: border-box;
+ border: none;
+ object-fit: cover;
+ width: 100%;
+ background-size: cover;
+ background-color: ${color['grey-30']};
+ border-radius: 4px;
+`;
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Badge component as children
+ */
+export const BadgeTd = ({ size, children }: { size: TdSizeType; children: ReactNode }) => {
+ return {children};
+};
+
+const TableBadge = styled(TableDefaultTd)`
+ padding: 5px 10px;
+`;
+
+type SwitchTdDirection = 'left' | 'center';
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Switch component as children
+ */
+export const SwitchTd = ({
+ size,
+ children,
+ direction = 'left',
+}: {
+ size: TdSizeType;
+ children: ReactNode;
+ direction?: SwitchTdDirection;
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const TableSwitch = styled(TableDefaultTd)<{ direction: SwitchTdDirection }>`
+ padding: 5px 14px;
+ text-align: ${({ direction }) => direction};
+`;
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Select component as children
+ */
+export const SelectTd = ({ size, children }: { size: TdSizeType; children: ReactNode }) => {
+ return {children};
+};
+
+const TableSelect = styled(TableDefaultTd)`
+ padding: 5px 10px;
+`;
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Checkbox component as children
+ */
+export const CheckboxTd = ({ size, children }: { size: TdSizeType; children: ReactNode }) => {
+ return {children};
+};
+
+const TableCheckbox = styled(TableDefaultTd)`
+ padding: 5px 12px 5px 18px;
+ width: 46px;
+`;
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Radio component as children
+ */
+export const RadioTd = ({ size, children }: { size: TdSizeType; children: ReactNode }) => {
+ return {children};
+};
+
+const TableRadio = styled(TableDefaultTd)`
+ padding: 5px 14px 5px 16px;
+ width: 46px;
+`;
+
+/**
+ * @description
+ * @property {ReactNode} children - use CDS Kebab component as children
+ */
+export const KebabTd = ({ size, children }: { size: TdSizeType; children: ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const TableKebob = styled(TableDefaultTd)`
+ padding: 5px 14px;
+ width: 48px;
+`;
+
+const Td = {
+ Text: TextTd,
+ Img: ImgTd,
+ Badge: BadgeTd,
+ Switch: SwitchTd,
+ Select: SelectTd,
+ Checkbox: CheckboxTd,
+ Radio: RadioTd,
+ Kebob: KebabTd,
+};
+
+export default Td;
diff --git a/src/components/Table/Th.tsx b/src/components/Table/Th.tsx
new file mode 100644
index 00000000..e148d838
--- /dev/null
+++ b/src/components/Table/Th.tsx
@@ -0,0 +1,131 @@
+import { ReactElement, ReactNode, cloneElement } from 'react';
+
+import styled from '@emotion/styled';
+
+import { FixedCellType, type TdSizeType } from './Td';
+import { Checkbox, type CheckboxProps } from '../Checkbox';
+import { B4, B6, type Text } from '../Text';
+
+type WidthRecord = Record;
+export const FIXED_TH_WIDTH: Record = {
+ [FixedCellType.KEBAB]: {
+ l: '48px',
+ m: '46px',
+ s: '44px',
+ xs: '44px',
+ },
+ [FixedCellType.RADIO]: {
+ l: '46px',
+ m: '46px',
+ s: '46px',
+ xs: '46px',
+ },
+ [FixedCellType.CHECKBOX]: {
+ l: '46px',
+ m: '46px',
+ s: '46px',
+ xs: '46px',
+ },
+ [FixedCellType.IMAGE]: {
+ l: '66px',
+ m: '56px',
+ s: '50px',
+ xs: '40px',
+ },
+};
+
+export type ThSizeType = 'l' | 'm' | 's';
+export type TableThStyleType = {
+ [key in ThSizeType]: {
+ height: number;
+ iconSize: number;
+ textComponent: typeof Text;
+ };
+};
+
+const TABLE_TH_STYLE: TableThStyleType = {
+ l: {
+ height: 46,
+ iconSize: 20,
+ textComponent: B4,
+ },
+ m: {
+ height: 40,
+ iconSize: 18,
+ textComponent: B4,
+ },
+ s: {
+ height: 32,
+ iconSize: 16,
+ textComponent: B6,
+ },
+};
+
+export interface Props {
+ text: string;
+ icon?: ReactNode;
+ width?: string;
+ size: ThSizeType;
+}
+
+export const DefaultTh = ({ text, icon, size, width = 'auto' }: Props) => {
+ const thStyle = TABLE_TH_STYLE[size];
+ return (
+
+
+ {text}
+ {icon &&
+ cloneElement(icon as ReactElement, {
+ size: thStyle.iconSize,
+ })}
+
+
+ );
+};
+
+export const CheckboxTh = ({
+ size,
+ checkboxType,
+}: {
+ size: ThSizeType;
+ checkboxType: CheckboxProps;
+}) => {
+ const thStyle = TABLE_TH_STYLE[size];
+ return (
+
+
+
+
+
+ );
+};
+
+const Th = {
+ Default: DefaultTh,
+ Checkbox: CheckboxTh,
+} as const;
+
+export default Th;
+
+const ThContents = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 2px;
+`;
+
+const TableHeader = styled.th<{ width: string; height: number }>`
+ width: ${({ width }) => width};
+ height: ${({ height }) => height}px;
+ vertical-align: middle;
+`;
+
+const TableDefaultHeader = styled(TableHeader)`
+ padding: 5px 14px;
+`;
+
+const CheckboxTableHeader = styled(TableHeader)`
+ padding: 5px 12px 5px 18px;
+`;
diff --git a/src/components/Table/Thead.tsx b/src/components/Table/Thead.tsx
new file mode 100644
index 00000000..ac9fc110
--- /dev/null
+++ b/src/components/Table/Thead.tsx
@@ -0,0 +1,19 @@
+import { ReactNode } from 'react';
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+
+export interface TheadProps {
+ children: ReactNode;
+ height?: number;
+}
+
+export const Thead = ({ children }: TheadProps) => {
+ return {children};
+};
+
+const TableHead = styled.thead`
+ border-top: 1px solid ${color['grey-50']};
+ border-bottom: 1px solid ${color['grey-50']};
+ word-break: keep-all;
+`;
diff --git a/src/components/Table/Tr.tsx b/src/components/Table/Tr.tsx
new file mode 100644
index 00000000..1be5051b
--- /dev/null
+++ b/src/components/Table/Tr.tsx
@@ -0,0 +1,31 @@
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+import { ReactNode } from 'react';
+
+export interface Props {
+ children: ReactNode;
+ cursorPointer?: boolean;
+ onClick?: () => void;
+}
+
+export const Tr = ({ children, cursorPointer = false, onClick }: Props) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const TableRow = styled.tr`
+ display: table-row;
+ cursor: ${props => (props.cursorPointer ? 'pointer' : 'default')};
+ border-width: 1px 0 1px 0;
+ border-style: inherit;
+ border-color: inherit;
+ max-height: 66px !important;
+ padding: 0;
+ border-top: 1px solid ${color['grey-50']};
+ border-bottom: 1px solid ${color['grey-50']};
+ border-color: inherit;
+`;
diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx
new file mode 100644
index 00000000..b54b8aad
--- /dev/null
+++ b/src/components/Table/index.tsx
@@ -0,0 +1,10 @@
+import { TableContainer } from './TableContainer';
+import { Tbody } from './Tbody';
+import Td from './Td';
+import Th from './Th';
+import { Thead } from './Thead';
+import { Tr } from './Tr';
+
+const Table = { Container: TableContainer, Thead, Th, Tbody, Tr, Td } as const;
+
+export default Table;
diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx
new file mode 100644
index 00000000..673ca93b
--- /dev/null
+++ b/src/components/Tabs/index.tsx
@@ -0,0 +1,71 @@
+import styled from '@emotion/styled';
+
+import { B3 } from '../Text';
+import { color } from '../styles';
+
+const TAB_WIDTH = 74;
+
+const Container = styled.div`
+ display: flex;
+ align-items: flex-end;
+ width: ${TAB_WIDTH * 5}px;
+ border-bottom: 2px solid ${color['grey-50']};
+`;
+
+const Item = styled.div`
+ display: flex;
+ border-radius: 4px 4px 0px 0px;
+ width: ${TAB_WIDTH}px;
+ padding: 11px 12px;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+`;
+
+const SelectedTab = styled(Item)`
+ border-bottom: 2px solid ${color['main-black']};
+ margin-bottom: -2px;
+`;
+
+const NotSelectedTab = styled(Item)`
+ &:hover {
+ background-color: ${color['grey-10']};
+ }
+ cursor: pointer;
+`;
+
+interface TabItem {
+ label: string;
+ value: string;
+}
+
+interface Props {
+ onChange: (v: string) => void;
+ selectedTab: string;
+ items: TabItem[];
+}
+
+export function Tabs({ items, onChange, selectedTab }: Props) {
+ return (
+
+ {items.map((tab, i) => {
+ const selected = tab.value === selectedTab;
+ const Component = selected ? SelectedTab : NotSelectedTab;
+ const color = selected ? 'main-black' : 'grey-60';
+ return (
+ {
+ onChange(tab.value);
+ }}
+ key={i}
+ aria-selected={selected ? 'true' : 'false'}
+ >
+
+ {tab.label}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Text/index.tsx b/src/components/Text/index.tsx
new file mode 100644
index 00000000..aec01686
--- /dev/null
+++ b/src/components/Text/index.tsx
@@ -0,0 +1,125 @@
+import { ReactNode, CSSProperties } from 'react';
+import { type HTMLAttributes } from 'react';
+
+import { color, fontStyle, typography } from '../styles';
+
+interface TypographyProps {
+ children: ReactNode;
+ size?: keyof typeof typography.size;
+ fw?: keyof typeof typography.weight;
+ c?: keyof typeof color;
+ style?: CSSProperties;
+ ellipsis?: boolean;
+}
+
+interface StyleProps extends HTMLAttributes {
+ c?: keyof typeof color;
+ ellipsis?: boolean;
+}
+
+/** * fontStyle ์๋ view๋ฅผ ์ํ ์ปดํฌ๋ํธ */
+export function Text(props: TypographyProps) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+export function H1({ children, c = 'grey-80' }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function H2({ children, c = 'grey-80' }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B1({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B2({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B3({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B4({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B5({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B6({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function B7({ children, c = 'grey-80', ellipsis }: StyleProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/Thumbnail/index.tsx b/src/components/Thumbnail/index.tsx
new file mode 100644
index 00000000..2c85e9b7
--- /dev/null
+++ b/src/components/Thumbnail/index.tsx
@@ -0,0 +1,30 @@
+import styled from '@emotion/styled';
+
+import { color } from '../styles';
+
+const Container = styled.div<{ size: number }>`
+ width: ${p => p.size}px;
+ height: ${p => p.size}px;
+ border-radius: 4px;
+ background-color: ${color['grey-30']};
+ box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08) inset;
+`;
+
+const Image = styled.img`
+ object-fit: cover;
+ border-radius: 4px;
+`;
+
+interface Props {
+ url: string | null;
+ alt?: string;
+ size?: number;
+}
+
+export function Thumbnail({ url, alt = 'image alt', size = 54 }: Props) {
+ return (
+
+ {url && }
+
+ );
+}
diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx
new file mode 100644
index 00000000..ef4cf69c
--- /dev/null
+++ b/src/components/Toast/index.tsx
@@ -0,0 +1,107 @@
+import { ReactNode } from 'react';
+import toast, { Toaster } from 'react-hot-toast';
+
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { B3, B7 } from '../Text';
+import { color } from '../styles';
+
+export const ToastPrepare = () => {
+ return ;
+};
+
+interface ToastData {
+ title?: string;
+ message: string;
+ durationMs?: number;
+}
+
+const ToastBase = styled.div`
+ display: flex;
+ width: 420px;
+ padding: 12px;
+ justify-content: space-between;
+ background-color: ${color.white};
+ border-radius: 9px;
+ box-shadow:
+ 0px 0px 1px 0px rgba(132, 132, 132, 0.31),
+ 0px 2px 5px 0px rgba(70, 70, 70, 0.2);
+`;
+
+const Left = styled.div`
+ display: flex;
+ height: 100%;
+ gap: 8px;
+`;
+
+const IconContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 18px;
+ line-height: 18px;
+`;
+
+const CloseContainer = styled.div`
+ cursor: pointer;
+ height: 20px;
+ width: 20px;
+`;
+
+const TitleAndMessageContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+`;
+
+const TitleContainer = styled.div`
+ display: flex;
+ height: 17px;
+ line-height: 17px;
+`;
+
+const MessageContainer = styled.div`
+ display: flex;
+ height: 100%;
+`;
+
+function renderToast(icon: ReactNode, message: ToastData) {
+ toast.custom(
+ t => (
+
+
+ {icon}
+
+ {message.title && (
+
+ {message.title}
+
+ )}
+
+ {message.message}
+
+
+
+ {
+ toast.remove(t.id);
+ }}
+ >
+
+
+
+ ),
+ { position: 'top-right', duration: message.durationMs || 3000 },
+ );
+}
+
+export const Toast = {
+ success: (message: ToastData) => {
+ renderToast(, message);
+ },
+ error: (message: ToastData) => {
+ renderToast(, message);
+ },
+};
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
new file mode 100644
index 00000000..e708cef0
--- /dev/null
+++ b/src/components/Tooltip/index.tsx
@@ -0,0 +1,149 @@
+import { ReactNode } from 'react';
+import { css } from '@emotion/react';
+import { type ReactElement } from 'react';
+
+import styled from '@emotion/styled';
+
+import { color, typography } from '../styles';
+
+type Direction = 'top' | 'bottom' | 'left' | 'right';
+export interface TooltipProps {
+ direction?: Direction;
+ withArrow?: boolean;
+ children: ReactNode;
+ content: string | ReactElement;
+}
+
+type TooltipStylesProps = Omit;
+
+export function Tooltip({ direction = 'top', withArrow = true, children, content }: TooltipProps) {
+ return (
+
+ {children}
+
+ {content}
+
+
+ );
+}
+
+const TooltipContainer = styled.div`
+ position: relative;
+ &:hover {
+ .tooltip-box {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 0.3s ease-in-out;
+ }
+ }
+`;
+
+const TOOLTIP_ARROW_WIDTH = 9;
+const TOOLTIP_ARROW_HEIGHT = 5;
+const DISTANCE_OF_ARROW_ELEMENT = 4;
+
+const directionStylesheet = ({
+ direction,
+ withArrow,
+}: {
+ direction?: Direction;
+ withArrow?: boolean;
+}) => {
+ const directionStyle = {
+ bubblePosition: `calc(100% + ${
+ withArrow ? TOOLTIP_ARROW_HEIGHT + DISTANCE_OF_ARROW_ELEMENT : DISTANCE_OF_ARROW_ELEMENT
+ }px)`,
+ arrowPosition: `calc(100% - ${DISTANCE_OF_ARROW_ELEMENT}px)`,
+ arrowRadius: '2px',
+ } as const;
+
+ switch (direction) {
+ case 'top':
+ return css`
+ bottom: ${directionStyle.bubblePosition};
+ left: 50%;
+ transform: translate(-50%, 1px);
+
+ &::after {
+ top: ${directionStyle.arrowPosition};
+ left: 50%;
+ transform: translate(-50%, -1px) rotate(45deg);
+ border-bottom-right-radius: ${directionStyle.arrowRadius};
+ }
+ `;
+ case 'bottom':
+ return css`
+ top: ${directionStyle.bubblePosition};
+ left: 50%;
+ transform: translate(-50%, 1px);
+
+ &::after {
+ bottom: ${directionStyle.arrowPosition};
+ left: 50%;
+ transform: translate(-50%, 1px) rotate(45deg);
+ border-top-left-radius: ${directionStyle.arrowRadius};
+ }
+ `;
+ case 'left':
+ return css`
+ top: 50%;
+ right: ${directionStyle.bubblePosition};
+ transform: translate(1px, -50%);
+
+ &::after {
+ top: 40%;
+ left: ${directionStyle.arrowPosition};
+ transform: translateX(-1px) rotate(45deg);
+ border-top-right-radius: ${directionStyle.arrowRadius};
+ }
+ `;
+ case 'right':
+ return css`
+ top: 50%;
+ left: ${directionStyle.bubblePosition};
+ transform: translate(1px, -50%);
+
+ &::after {
+ top: 40%;
+ right: ${directionStyle.arrowPosition};
+ transform: translateX(1px) rotate(45deg);
+ border-bottom-left-radius: ${directionStyle.arrowRadius};
+ }
+ `;
+ default:
+ return css``;
+ }
+};
+
+const TooltipBox = styled.div`
+ position: relative;
+ cursor: help;
+ z-index: 10000;
+ opacity: 0;
+
+ ${({ direction, withArrow }) => directionStylesheet({ direction, withArrow })}
+
+ position: absolute;
+ visibility: hidden;
+ width: max-content;
+ height: max-content;
+
+ background: ${color['main-black']};
+ color: ${color.white};
+ font-size: ${typography.size.xxs}px;
+ font-weight: ${typography.weight.regular};
+ padding: 7px 10px;
+ border-radius: 7px;
+
+ ${({ withArrow }) =>
+ withArrow &&
+ css`
+ &::after {
+ content: '';
+ position: absolute;
+ width: ${TOOLTIP_ARROW_WIDTH}px;
+ height: ${TOOLTIP_ARROW_WIDTH}px; // arrow์ base์ธ ์ ์ฌ๊ฐํ ์ธ๋ก height
+ background: ${color['main-black']};
+ }
+ `}
+`;
diff --git a/src/components/Topbar/ProfileMenu.tsx b/src/components/Topbar/ProfileMenu.tsx
new file mode 100644
index 00000000..461f4df3
--- /dev/null
+++ b/src/components/Topbar/ProfileMenu.tsx
@@ -0,0 +1,256 @@
+import { type ReactElement, type ReactNode, useEffect, useRef, useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { B1, B2, B3, B4, B5 } from '../Text';
+import { color } from '../styles';
+
+interface ProfileProps {
+ avatar?: string;
+ name: string;
+ description?: string;
+}
+
+export interface ProfileOption {
+ label: string;
+ onClick: () => void;
+ icon: ReactElement;
+}
+
+interface ProfileOptionListProps {
+ title: ReactNode;
+ name: string;
+ email: string;
+ options?: ProfileOption[];
+}
+
+export type ProfileMenuProps = ProfileProps & ProfileOptionListProps;
+
+const Container = styled.div`
+ position: relative;
+ display: inline-block;
+`;
+
+const ProfileContainer = styled.div<{ clicked?: boolean }>`
+ display: inline-flex;
+ padding: 8px 14px 8px 13px;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 18px;
+ border-radius: 50px;
+ background: ${color.white};
+ box-shadow: ${({ clicked }) => clicked && `0 0 0 1.5px ${color['grey-30']} inset,`} 0px 4px 10px
+ 0px rgba(0, 0, 0, 0.05);
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+`;
+
+const AvatarWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+`;
+const Info = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ min-width: 86px;
+ span:last-child {
+ line-height: 12px;
+ }
+`;
+
+const DropdownButton = styled.div<{ clicked?: boolean }>`
+ width: 20px;
+ height: 20px;
+ margin: auto;
+ flex-shrink: 0;
+ cursor: pointer;
+ transform: ${({ clicked }) => (clicked ? 'rotate(-180deg);' : 'rotate(0);')};
+`;
+
+const Image = styled.div<{ url?: string }>`
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ object-fit: cover;
+ background-image: url(${({ url }) => url});
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-color: #7a7a7a;
+`;
+
+const MenuProfileContainer = styled.div`
+ width: 100%;
+ padding: 6px 10px 10px 10px;
+ background: ${color.white};
+ box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.05);
+ border-radius: 8px;
+`;
+
+const ClientDataWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0 8px;
+ * {
+ line-height: normal;
+ }
+`;
+const MenuDivider = styled.hr`
+ border-width: 0px 0px thin;
+ border-style: solid;
+ border-color: ${color['grey-30']};
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ margin-top: 16px;
+`;
+
+const MenuWrapper = styled.ul`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 4px;
+`;
+
+const MenuButton = styled.button`
+ background: inherit;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+`;
+
+const MenuItem = styled.li`
+ display: flex;
+ gap: 6px;
+ padding: 6px 8px;
+ align-items: center;
+ border-radius: 8px;
+ &:hover {
+ background: ${color['grey-20']};
+ }
+ &:active {
+ background: ${color['grey-30']};
+ }
+`;
+
+const TitleWrapper = styled.div`
+ height: 32px;
+ padding-top: 12px;
+`;
+
+const OptionWrapper = styled.div`
+ position: absolute;
+ z-index: 1000;
+ margin-top: 9px;
+ right: 0;
+ min-width: 100%;
+`;
+
+const InfoName = styled.div`
+ height: 21px;
+ display: flex;
+ align-items: center;
+`;
+
+function ProfileOptionList({ title, name, email, options }: ProfileOptionListProps) {
+ return (
+
+
+
+ {title}
+
+ {name}
+ {email}
+
+
+
+ {options?.map(menu => {
+ const Icon = menu.icon;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+function Profile({
+ avatar,
+ name,
+ description,
+ onClick,
+ clicked,
+}: {
+ avatar?: string;
+ name: string;
+ description?: string;
+ onClick: () => void;
+ clicked: boolean;
+}) {
+ return (
+
+
+
+
+
+
+ {name}
+
+ {description}
+
+
+
+
+
+
+
+ );
+}
+
+export function ProfileMenu({ avatar, name, description, ...optionListProps }: ProfileMenuProps) {
+ const [showMenu, setShowMenu] = useState(false);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (wrapperRef.current != null && !wrapperRef.current.contains(event.target as Node)) {
+ setShowMenu(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [wrapperRef]);
+
+ return (
+
+ {
+ setShowMenu(!showMenu);
+ }}
+ clicked={showMenu}
+ />
+
+ {showMenu && }
+
+
+ );
+}
diff --git a/src/components/Topbar/Title.tsx b/src/components/Topbar/Title.tsx
new file mode 100644
index 00000000..f5d2ae58
--- /dev/null
+++ b/src/components/Topbar/Title.tsx
@@ -0,0 +1,33 @@
+import styled from '@emotion/styled';
+
+import Icon from '../Icon';
+import { H1 } from '../Text';
+import { Tooltip } from '../Tooltip';
+
+const TitleSection = styled.div`
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+`;
+
+const QuestionIconWrapper = styled.i`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+`;
+
+export function Title({ title, description }: { title: string; description?: string }) {
+ return (
+
+ {title}
+ {description && (
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/Topbar/index.tsx b/src/components/Topbar/index.tsx
new file mode 100644
index 00000000..5859122a
--- /dev/null
+++ b/src/components/Topbar/index.tsx
@@ -0,0 +1,5 @@
+import { ProfileMenu, type ProfileMenuProps } from './ProfileMenu';
+import { Title } from './Title';
+
+export { ProfileMenu, Title };
+export type { ProfileMenuProps };
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 00000000..45f4f33e
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,23 @@
+export * from './Badge';
+export * from './BottomBar';
+export * from './Button';
+export * from './Checkbox';
+export * from './ColorPicker';
+export * from './EmptyState';
+export * from './Icon';
+export * from './Input';
+export * from './Modal';
+export * from './Navigation';
+export * from './Pagination';
+export * from './Radio';
+export * from './Select';
+export * from './styles';
+export * from './Switch';
+export * from './Tabs';
+export * from './Text';
+export * from './Thumbnail';
+export * from './Toast';
+export * from './Tooltip';
+export * from './Topbar';
+export * from './Filter';
+export * from './Spinner';
diff --git a/src/components/styles.ts b/src/components/styles.ts
new file mode 100644
index 00000000..2aed0c74
--- /dev/null
+++ b/src/components/styles.ts
@@ -0,0 +1,108 @@
+// Corca Design System
+// Last Modified Date: 2023/11/09
+
+export const typography = {
+ size: {
+ xxxl: 30,
+ xxl: 26,
+ xl: 20,
+ l: 18,
+ m: 16,
+ s: 14,
+ xs: 13,
+ xxs: 12,
+ xxxs: 10,
+ },
+ weight: {
+ bold: 700,
+ semibold: 600,
+ medium: 500,
+ regular: 400,
+ },
+ fontFamily:
+ '"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, \'Noto Sans KR\', sans-serif;',
+};
+
+export const color = {
+ // Main color
+ 'main-black': '#292929',
+ 'main-yellow': '#FFD464',
+
+ // Grey color
+ 'grey-80': '#363738',
+ 'grey-70': '#515354',
+ 'grey-60': '#737678',
+ 'grey-50': '#AAACAF',
+ 'grey-40': '#CED1D6',
+ 'grey-30': '#E4E5E9',
+ 'grey-20': '#F3F4F6',
+ 'grey-10': '#F8F9FB',
+
+ // ETC
+ white: '#FFFFFF',
+ focus: '#3B79D7',
+ 'error-30': '#B10E1C',
+ 'error-20': '#FC685F',
+ 'error-10': '#FFE7E6',
+
+ 'active-30': '#037847',
+ 'active-20': '#14BA6D',
+ 'active-10': '#ECFDF3',
+};
+
+export const fontStyle = {
+ h1: {
+ fontSize: 22,
+ fontWeight: typography.weight.bold,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ h2: {
+ fontSize: 20,
+ fontWeight: typography.weight.semibold,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b1: {
+ fontSize: 16,
+ fontWeight: typography.weight.medium,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b2: {
+ fontSize: 14,
+ fontWeight: typography.weight.regular,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b3: {
+ fontSize: 13,
+ fontWeight: typography.weight.regular,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b4: {
+ fontSize: 12,
+ fontWeight: typography.weight.semibold,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b5: {
+ fontSize: 12,
+ fontWeight: typography.weight.regular,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b6: {
+ fontSize: 10,
+ fontWeight: typography.weight.semibold,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+ b7: {
+ fontSize: 14,
+ fontWeight: typography.weight.semibold,
+ color: color['grey-80'],
+ fontFamily: typography.fontFamily,
+ },
+};
diff --git a/src/global.css b/src/global.css
new file mode 100644
index 00000000..f8ce88f9
--- /dev/null
+++ b/src/global.css
@@ -0,0 +1,33 @@
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Display:wght@200&display=swap');
+@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
+@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard-dynamic-subset.css');
+@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.css');
+
+body,
+input,
+pre,
+textarea,
+button {
+ margin: 0;
+ font-family:
+ 'Pretendard Variable',
+ Pretendard,
+ -apple-system,
+ BlinkMacSystemFont,
+ system-ui,
+ Roboto,
+ 'Noto Sans KR',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: #525252;
+}
+* {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: none;
+ box-sizing: border-box;
+ text-decoration: none;
+ scrollbar-width: none;
+}
diff --git a/src/index.d.ts b/src/index.d.ts
new file mode 100644
index 00000000..ff087dfd
--- /dev/null
+++ b/src/index.d.ts
@@ -0,0 +1,9 @@
+declare module '*.jpg' {
+ const path: string;
+ export default path;
+}
+
+declare module '*.png' {
+ const path: string;
+ export default path;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 00000000..6ed8b549
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,2 @@
+export * from './utils';
+export * from './components';
diff --git a/src/stories/Badge.stories.tsx b/src/stories/Badge.stories.tsx
new file mode 100644
index 00000000..baff370e
--- /dev/null
+++ b/src/stories/Badge.stories.tsx
@@ -0,0 +1,62 @@
+import styled from '@emotion/styled';
+
+import { Badge, badgeVariants } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Badge',
+ component: Badge,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=392-1871&mode=design&t=ML3YPcDZJzLhfFhw-0',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ dotted: true,
+ label: 'Example Badge',
+ variant: 'green',
+};
+
+export function Variant() {
+ return (
+ <>
+
+ {badgeVariants.map(variant => (
+
+ ))}
+
+
+ {badgeVariants.map(variant => (
+
+ ))}
+
+ >
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
+
+const BadgeContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
diff --git a/src/stories/Bottombar.stories.tsx b/src/stories/Bottombar.stories.tsx
new file mode 100644
index 00000000..b9bb63bb
--- /dev/null
+++ b/src/stories/Bottombar.stories.tsx
@@ -0,0 +1,87 @@
+import { StoryFn } from '@storybook/react';
+
+import styled from '@emotion/styled';
+
+import { BottomBar, color } from '../components';
+
+export default {
+ title: 'Components/BottomBar',
+ component: BottomBar,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=1130-20072',
+ },
+ },
+};
+
+export function Default() {
+ return (
+ <>
+ {
+ alert('Dismiss. ' + NOTE);
+ },
+ }}
+ confirm={{
+ label: 'Confirm',
+ onClick: () => {
+ alert('Confirm. ' + NOTE);
+ },
+ }}
+ />
+ {
+ alert('Dismiss. ' + NOTE);
+ },
+ }}
+ confirm={{
+ label: 'Confirm',
+ onClick: () => {
+ alert('Confirm. ' + NOTE);
+ },
+ }}
+ destroy={{
+ label: 'Destroy',
+ onClick: () => {
+ alert('Destroy. ' + NOTE);
+ },
+ }}
+ />
+ >
+ );
+}
+
+const Container = styled.div`
+ width: '100%';
+ min-height: 250px;
+ background-color: ${color['grey-10']};
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+`;
+
+const Wrapper = styled.span`
+ width: 85%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
+
+const NOTE = 'Alert action is temporary for storybook and can be customized.';
diff --git a/src/stories/Button.stories.tsx b/src/stories/Button.stories.tsx
new file mode 100644
index 00000000..153f2e8f
--- /dev/null
+++ b/src/stories/Button.stories.tsx
@@ -0,0 +1,197 @@
+import styled from '@emotion/styled';
+
+import { Button } from '../components';
+import Icon from '../components/Icon';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Button',
+ component: Button,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-14796',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ size: 'small',
+ variant: 'filled',
+ children: 'Example Button',
+ disabled: false,
+};
+
+Basic.argTypes = {
+ icon: {
+ name: 'icon',
+ control: { type: 'select' },
+ },
+};
+
+export function Filled() {
+ return (
+ <>
+
+
+
+ }>
+ small filled
+
+
+
+
+
+ }>
+ large filled
+
+
+
+
+
+
+
+ }>
+ small filled
+
+
+
+
+
+ }>
+ large filled
+
+
+
+ >
+ );
+}
+
+export function Outline() {
+ return (
+ <>
+
+
+
+ }>
+ small outline
+
+
+
+
+
+ }>
+ large outline
+
+
+
+
+
+
+
+ }>
+ small outline
+
+
+
+
+
+ }>
+ large outline
+
+
+
+ >
+ );
+}
+
+export function Text() {
+ return (
+ <>
+
+
+
+ }>
+ small text
+
+
+
+
+
+ }>
+ small text
+
+
+
+
+
+
+
+ } disabled>
+ small text
+
+
+
+
+
+ } disabled>
+ large text
+
+
+
+ >
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
+
+const ButtonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ gap: 15px;
+`;
diff --git a/src/stories/Checkbox.stories.tsx b/src/stories/Checkbox.stories.tsx
new file mode 100644
index 00000000..7ffce88f
--- /dev/null
+++ b/src/stories/Checkbox.stories.tsx
@@ -0,0 +1,40 @@
+import styled from '@emotion/styled';
+
+import { Checkbox } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Checkbox',
+ component: Checkbox,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=918-6544&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ selected: false,
+ disabled: false,
+ label: 'Label',
+};
+
+const Container = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/src/stories/Color.stories.tsx b/src/stories/Color.stories.tsx
new file mode 100644
index 00000000..7b9afbff
--- /dev/null
+++ b/src/stories/Color.stories.tsx
@@ -0,0 +1,113 @@
+import styled from '@emotion/styled';
+
+import { B1, B3, H1, color } from '../components';
+
+export default {
+ title: 'Foundation/Color - Source',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-16999&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+};
+
+export function Color() {
+ return (
+
+ Main color
+
+
+
+
+
+ Grey color
+
+
+
+
+
+
+
+
+
+
+
+ ETC color
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const ColorContainer = styled.div`
+ padding: 47px;
+`;
+
+const ColorWrapper = styled.div`
+ margin-bottom: 30px;
+ display: flex;
+ gap: 15px;
+`;
+
+const EtcColorWrapper = styled.div`
+ margin-bottom: 30px;
+ gap: 25px;
+ display: flex;
+ flex-direction: column;
+`;
+
+const EtcColorContentsWrapper = styled.div`
+ gap: 15px;
+ display: flex;
+`;
+
+type ColorType = keyof typeof color;
+
+const Palette = ({ name }: { name: ColorType }) => {
+ return (
+
+
+
+ {name}
+ {color[name]}
+
+
+ );
+};
+
+const PaletteContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+`;
+
+const PaletteWrapper = styled.div<{ colorName: ColorType }>`
+ width: 110px;
+ height: 55px;
+ border-radius: 8px;
+ background-color: ${({ colorName }) => color[colorName]};
+`;
+
+const PaletteTextWrapper = styled.div`
+ display: flex;
+ padding: 0px 2px;
+ flex-direction: column;
+ align-items: flex-start;
+`;
diff --git a/src/stories/DatePicker.stories.tsx b/src/stories/DatePicker.stories.tsx
new file mode 100644
index 00000000..68ed9338
--- /dev/null
+++ b/src/stories/DatePicker.stories.tsx
@@ -0,0 +1,120 @@
+import { StoryFn } from '@storybook/react';
+import { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import DatePicker from '../components/DatePicker';
+
+export default {
+ title: 'Components/DatePicker',
+ component: DatePicker,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=850-5263&mode=dev',
+ },
+ },
+};
+
+export function SingleDate() {
+ const [dateKo, setDateKo] = useState(null);
+ const [dateEn, setDateEn] = useState(null);
+ const [dateErr, setDateErr] = useState(null);
+ return (
+ <>
+ Korean
+
+ English
+
+ Disabled
+ {
+ console.log('disabled');
+ }}
+ language={'en'}
+ inputProps={{ width: 400 }}
+ disabled
+ />
+ Error (error will not disappear since it is just visual display not interactive demo.)
+
+ >
+ );
+}
+
+export function DateTime() {
+ const [dateKo, setDateKo] = useState(null);
+ const [dateEn, setDateEn] = useState(null);
+ const [dateErr, setDateErr] = useState(null);
+ return (
+ <>
+ Korean
+
+ English
+
+ Disabled
+ {
+ console.log('disabled');
+ }}
+ language={'ko'}
+ inputProps={{ width: 400 }}
+ disabled
+ />
+ Error (error will not disappear since it is just visual display not interactive demo.)
+
+ >
+ );
+}
+
+const Container = styled.div`
+ width: 100%;
+ height: 100%;
+ min-height: 600px;
+ display: flex;
+ flex-direction: column;
+ align-items: left;
+ text-align: left;
+ justify-content: flex-start;
+ padding-top: 100px;
+ gap: 20px;
+`;
diff --git a/src/stories/EmptyState.stories.tsx b/src/stories/EmptyState.stories.tsx
new file mode 100644
index 00000000..db3791f8
--- /dev/null
+++ b/src/stories/EmptyState.stories.tsx
@@ -0,0 +1,55 @@
+import styled from '@emotion/styled';
+
+import { EmptyState } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Empty state',
+ component: EmptyState,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+ ๊ฐ๋ฐ์ ์ฐธ๊ณ : ์ค์ ํ์ด์ง ๋์์ธ ์ ์ฉ์ ํ์ด์ง ์ปจํ
์ด๋์ shadow ์ ์ฉ
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=469-4458&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => (
+
+
+
+);
+
+export const Default = Template.bind({});
+Default.args = {
+ label: 'Label',
+ description: 'description',
+ button: {
+ text: 'Label',
+ onClick: () => {},
+ },
+};
+
+const Container = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 20px;
+`;
+
+const LayoutContainer = styled.div`
+ width: 100%;
+ box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
+`;
diff --git a/src/stories/Filter.stories.tsx b/src/stories/Filter.stories.tsx
new file mode 100644
index 00000000..916d039d
--- /dev/null
+++ b/src/stories/Filter.stories.tsx
@@ -0,0 +1,199 @@
+import React, { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B3, H2, Select } from '../components';
+import { Filter } from '../components/Filter';
+import { type FilterCategoryType } from '../components/Filter/Default';
+
+export default {
+ title: 'Components/Filter',
+ tags: ['autodocs'],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=1013-35382&mode=design&t=S101dBYOOd2CT4Mo-0',
+ },
+ },
+};
+
+const MOCK_FILTER_CATEGORIES: Array> = [
+ {
+ label: '์ํ',
+ options: [
+ {
+ label: 'ํ์ฑ',
+ value: '0',
+ },
+ {
+ label: '๋นํ์ฑ1',
+ value: '1',
+ },
+ {
+ label: '๋นํ์ฑ2',
+ value: '2',
+ },
+ {
+ label: '๋นํ์ฑ3',
+ value: '3',
+ disabled: true,
+ },
+ ],
+ },
+ {
+ label: '์์ฑ์ผ',
+ options: [
+ {
+ label: '์ ์ฒด',
+ value: '0',
+ },
+ {
+ label: '1์ฃผ',
+ value: '1',
+ },
+ {
+ label: '1๊ฐ์',
+ value: '2',
+ },
+ {
+ label: '3๊ฐ์',
+ value: '3',
+ },
+ {
+ label: '์ง์ ์
๋ ฅ',
+ value: '์ง์ ์
๋ ฅ',
+ },
+ ],
+ },
+];
+
+type FilterValues = Record;
+
+const INIT_FILTER_VALUE: FilterValues = {
+ ์ํ: '0',
+ ์์ฑ์ผ: '0',
+};
+
+type CustomValue = [Date, Date] | [null, null];
+const INIT_CUSTOM_VALUE: CustomValue = [null, null];
+
+const SelectedValueWrapper = styled.div`
+ min-height: 40px;
+`;
+
+const Filters = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+export function Basic() {
+ const [filterValues, setFilterValues] = useState(INIT_FILTER_VALUE);
+ const [customValue, setCustomValue] = useState(INIT_CUSTOM_VALUE);
+
+ return (
+ <>
+
+ ์ ํ : {JSON.stringify(filterValues)}
+ {customValue[0] != null && `${customValue[0].toDateString()}๋ถํฐ `}
+ {customValue[1] != null && `${customValue[1].toDateString()}๊น์ง`}
+
+ {
+ alert('close');
+ }}
+ reset={() => {
+ setFilterValues(INIT_FILTER_VALUE);
+ setCustomValue(INIT_CUSTOM_VALUE);
+ }}
+ resetLabel="์ด๊ธฐํ"
+ >
+ {MOCK_FILTER_CATEGORIES.map(category => {
+ return (
+
+ {
+ setFilterValues(prev => ({
+ ...prev,
+ [category.label]: value,
+ }));
+ }}
+ />
+ {filterValues[category.label] === '์ง์ ์
๋ ฅ' && (
+ {
+ setCustomValue(value);
+ }}
+ startDateLabel="๋ถํฐ"
+ endDateLabel="๊น์ง"
+ language="ko"
+ />
+ )}
+
+ );
+ })}
+
+ >
+ );
+}
+
+export function MultiSelectFilter() {
+ const INIT_FILTER_VALUE = ['0'];
+ const [filterValues, setFilterValues] = useState(INIT_FILTER_VALUE);
+ const category = MOCK_FILTER_CATEGORIES[0];
+ return (
+ <>
+
+ CDS ํผ๊ทธ๋ง X, ๊ฐ๋ฐ๋จ ์์
+ ์ ํ : {filterValues.join(', ')}
+
+
+ {
+ alert('close');
+ }}
+ reset={() => {
+ setFilterValues(INIT_FILTER_VALUE);
+ }}
+ resetLabel="์ด๊ธฐํ"
+ >
+ {
+ setFilterValues(prev => {
+ const newValue = prev.filter(v => v !== value);
+ if (prev.includes(value)) {
+ return newValue ?? [];
+ }
+ return [...newValue, value];
+ });
+ }}
+ />
+
+ {
+ alert('close');
+ }}
+ reset={() => {
+ setFilterValues(INIT_FILTER_VALUE);
+ }}
+ resetLabel="์ด๊ธฐํ"
+ >
+ ์ปค์คํ
์นดํ
๊ณ ๋ฆฌ ์์
+
+
+
+
+ >
+ );
+}
diff --git a/src/stories/Icon.stories.tsx b/src/stories/Icon.stories.tsx
new file mode 100644
index 00000000..f4ab9a67
--- /dev/null
+++ b/src/stories/Icon.stories.tsx
@@ -0,0 +1,80 @@
+import { ReactNode } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B3 } from '../components';
+import Icon from '../components/Icon';
+
+export default {
+ title: 'Foundation/Icon',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-17316&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+};
+
+interface IconProps {
+ children: ReactNode;
+ name: string;
+}
+
+function IconBlock({ children, name }: IconProps) {
+ return (
+
+ {children}
+ {name}
+
+ );
+}
+
+const IconBlockContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ width: 120px;
+ height: 'max-content';
+ padding: 8px;
+ margin: 8px;
+ text-align: 'center';
+ border-radius: 6px;
+ background-color: #fff;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.16);
+`;
+
+const BlockIconWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 60px;
+`;
+
+export function Default() {
+ const icons = Object.keys(Icon).map(name => {
+ const renderIcon = Icon[name as keyof typeof Icon];
+ return (
+
+ {renderIcon({})}{' '}
+
+ );
+ });
+
+ return (
+
+ {icons}
+
+ );
+}
+
+const DefaultStoryContainer = styled.div`
+ padding: 20px;
+`;
+
+const DefaultStoryWrapper = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ grid-gap: 12px;
+`;
diff --git a/src/stories/IconButton.stories.tsx b/src/stories/IconButton.stories.tsx
new file mode 100644
index 00000000..6c0ea05a
--- /dev/null
+++ b/src/stories/IconButton.stories.tsx
@@ -0,0 +1,46 @@
+import { Meta, StoryFn } from '@storybook/react';
+
+import styled from '@emotion/styled';
+
+import { IconButton } from '../components';
+import Icon from '../components/Icon';
+
+export default {
+ title: 'Components/IconButton',
+ component: IconButton,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-14796&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+} as Meta;
+
+export function Default() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/src/stories/Input.stories.tsx b/src/stories/Input.stories.tsx
new file mode 100644
index 00000000..c38263a2
--- /dev/null
+++ b/src/stories/Input.stories.tsx
@@ -0,0 +1,211 @@
+import { StoryFn } from '@storybook/react';
+
+import styled from '@emotion/styled';
+
+import {
+ B7,
+ H2,
+ InputContainer,
+ PasswordInput,
+ TextInput,
+ type TextInputProps,
+ color,
+} from '../components';
+import Icon from '../components/Icon';
+import { BaseInput, type InputTooltipProps } from '../components/Input/InputContainer';
+
+export default {
+ title: 'Components/Input',
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=538-9681&mode=design&t=5x2Eq9JDnANEoXu9-0',
+ },
+ },
+};
+
+const baseArgs = {
+ label: 'Label',
+ placeholder: 'Placeholder',
+ width: 400,
+ disabled: false,
+};
+
+const tooltipArgs: InputTooltipProps = {
+ content: 'Tooltip',
+ direction: 'bottom',
+ withArrow: true,
+};
+
+const argsType = {
+ error: {
+ name: 'error',
+ control: { type: 'text' },
+ },
+ description: {
+ name: 'description',
+ control: { type: 'text' },
+ },
+};
+
+const TextInputTemplate: StoryFn = args => ;
+
+export const TextInputDefault = TextInputTemplate.bind({});
+TextInputDefault.args = baseArgs;
+TextInputDefault.argTypes = argsType;
+
+const PasswordInputTemplate: StoryFn = args => ;
+
+export const PasswordInputDefault = PasswordInputTemplate.bind({});
+PasswordInputDefault.args = baseArgs;
+PasswordInputDefault.argTypes = argsType;
+
+export function WithIcon() {
+ return (
+ <>
+
+ Left Section
+ }>
+
+
+
+
+
+ Right Section
+ }>
+
+
+
+ >
+ );
+}
+
+export function Component() {
+ const textInputArgs = [
+ [{}, { disabled: true }],
+ [
+ { tooltip: tooltipArgs },
+ {
+ disabled: true,
+ tooltip: tooltipArgs,
+ },
+ ],
+ [{ description: 'Description' }, { disabled: true, description: 'Description' }],
+ [
+ {
+ description: 'Description',
+ tooltip: tooltipArgs,
+ },
+ {
+ disabled: true,
+ description: 'Description',
+ tooltip: tooltipArgs,
+ },
+ ],
+ ] as Array>>;
+
+ const errorTextInputArgs = [
+ {},
+ {
+ tooltip: tooltipArgs,
+ },
+ {
+ description: 'Description',
+ },
+ {
+ description: 'Description',
+ tooltip: tooltipArgs,
+ },
+ ] as Array>>;
+
+ return (
+
+
+
+ Active
+
+
+ Disabled
+
+
+
+
+
+ {textInputArgs.map((group, groupIndex) => (
+
+ {group.map((props, index) => (
+
+ ))}
+
+ ))}
+
+
+
+ {errorTextInputArgs.map((props, index) => (
+
+
+
+ ))}
+
+
+
+ );
+}
+
+const Container = styled.div`
+ width: 100%;
+ padding: 100px;
+ background-color: ${color['grey-10']};
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 50px;
+`;
+
+const ComponentContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const ComponentContents = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+`;
+
+const ComponentWrapper = styled.div`
+ display: flex;
+ gap: 37px;
+`;
+
+const ComponentContentsWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 100px;
+`;
+
+const ComponentTextWrapper = styled.div`
+ display: flex;
+ padding-bottom: 35px;
+`;
+
+const TextWrapper = styled.div`
+ width: 400px;
+ display: flex;
+ justify-content: center;
+`;
+
+const IconTextWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 50px;
+`;
diff --git a/src/stories/Modal.stories.tsx b/src/stories/Modal.stories.tsx
new file mode 100644
index 00000000..da18ec1f
--- /dev/null
+++ b/src/stories/Modal.stories.tsx
@@ -0,0 +1,70 @@
+import { Alert as CdsAlert, Confirm as CdsConfirm, Drawer, Modal } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Modal',
+ tags: ['autodocs'],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=953-38810&mode=design&t=qaeh1DmnXnMBnUdB-0',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ title: 'Title',
+ subtitle: 'Description',
+ opened: true,
+ cancel: {
+ label: '์ทจ์',
+ onClick: () => {},
+ },
+ confirm: {
+ label: '๋ค์',
+ onClick: () => {},
+ },
+ children: <>Body>,
+};
+
+const AlertTemplate: StoryFn = args => ;
+
+export const Alert = AlertTemplate.bind({});
+Alert.args = {
+ opened: true,
+ title: 'title',
+ contents: 'contents',
+ close: {
+ label: '๋ซ๊ธฐ',
+ onClick: () => {},
+ },
+};
+
+const ConfirmTemplate: StoryFn = args => ;
+
+export const Confirm = ConfirmTemplate.bind({});
+Confirm.args = {
+ opened: true,
+ title: 'title',
+ contents: 'contents',
+ cancel: {
+ label: '๋ซ๊ธฐ',
+ onClick: () => {},
+ },
+ confirm: {
+ label: 'ํ์ธ',
+ onClick: () => {},
+ },
+};
+
+const DrawerTemplate: StoryFn = args => ;
+
+export const Side = DrawerTemplate.bind({});
+Side.args = {
+ title: 'Title',
+ opened: true,
+ children: <>Body>,
+};
diff --git a/src/stories/Pagination.stories.tsx b/src/stories/Pagination.stories.tsx
new file mode 100644
index 00000000..38d4cd3f
--- /dev/null
+++ b/src/stories/Pagination.stories.tsx
@@ -0,0 +1,150 @@
+import { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B3, Pagination } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Pagination',
+ component: Pagination,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-14950&mode=design&t=8qZkE0rAl2OEve56-0',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => {
+ const [page, setPage] = useState(1);
+ return ;
+};
+
+export const Basic = Template.bind({});
+Basic.args = {
+ total: 10,
+};
+Basic.argTypes = {
+ current: {
+ name: 'current',
+ control: { type: 'select' },
+ },
+};
+
+export function PageLength() {
+ const PageTexts = {
+ ONLY_ONE_PAGE: 'Only 1 page',
+ ONE_TO_FIVE_PAGES: '1~5 pages',
+ FIVE_PAGES_AND_UP: '5 pages ~',
+ UP_TO_TEN_PAGES: '~ 10 pages',
+ TEN_PAGES_AND_UP: '10 pages ~',
+ };
+
+ const [page1, setPage1] = useState({
+ id: 1,
+ current: 1,
+ total: 1,
+ text: PageTexts.ONLY_ONE_PAGE,
+ });
+ const [page2, setPage2] = useState({
+ id: 2,
+ current: 1,
+ total: 5,
+ text: PageTexts.ONE_TO_FIVE_PAGES,
+ });
+ const [page3, setPage3] = useState({
+ id: 3,
+ current: 5,
+ total: 10,
+ text: PageTexts.FIVE_PAGES_AND_UP,
+ });
+ const [page4, setPage4] = useState({
+ id: 4,
+ current: 6,
+ total: 8,
+ text: PageTexts.UP_TO_TEN_PAGES,
+ });
+ const [page5, setPage5] = useState({
+ id: 5,
+ current: 6,
+ total: 20,
+ text: PageTexts.TEN_PAGES_AND_UP,
+ });
+
+ const pages = [page1, page2, page3, page4, page5];
+ const setPages = [setPage1, setPage2, setPage3, setPage4, setPage5];
+
+ const handleSelect = (id: number, current: number) => {
+ setPages.forEach((setPage, index) => {
+ if (pages[index].id === id) {
+ setPage({ ...pages[index], current });
+ }
+ });
+ };
+
+ return (
+
+
+ {pages.map((page, index) => (
+
+ {page.text}
+
+ ))}
+
+
+ {pages.map(page => (
+ {
+ handleSelect(page.id, current);
+ }}
+ />
+ ))}
+
+
+ );
+}
+
+const Container = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const PageLengthContainer = styled.div`
+ display: flex;
+ gap: 20px;
+`;
+
+const PageLengthTextContainer = styled.div`
+ display: flex;
+ gap: 20px;
+ flex-direction: column;
+ align-items: flex-end;
+`;
+
+const PageLengthPaginationContainer = styled.div`
+ display: flex;
+ gap: 20px;
+ flex-direction: column;
+`;
+
+const PageLengthTextWrapper = styled.div`
+ display: flex;
+ height: 32px;
+ flex-direction: column;
+ justify-content: center;
+`;
diff --git a/src/stories/Radio.stories.tsx b/src/stories/Radio.stories.tsx
new file mode 100644
index 00000000..a33e1ce8
--- /dev/null
+++ b/src/stories/Radio.stories.tsx
@@ -0,0 +1,188 @@
+import { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { B2, H2, Radio } from '../components';
+import { type RadioGroupOption } from '../components/Radio/Group';
+import { type RadioItemProps } from '../components/Radio/RadioItem';
+import type { Meta, StoryFn } from '@storybook/react';
+
+const MOCK_ITEMS: Array<{ title: string; items: Array> }> = [
+ {
+ title: 'Unchecked with Lable',
+ items: [
+ {
+ label: 'Label',
+ value: 'label-unselected-disable',
+ disabled: true,
+ onSelect: () => {},
+ selected: false,
+ },
+ ],
+ },
+ {
+ title: 'Checked with Lable',
+ items: [
+ {
+ label: 'Label',
+ value: 'label-selected-able',
+ disabled: false,
+ onSelect: () => {},
+ selected: true,
+ },
+ {
+ label: 'Label',
+ value: 'label-selected-disable',
+ disabled: true,
+ onSelect: () => {},
+ selected: true,
+ },
+ ],
+ },
+ {
+ title: 'Unchecked',
+ items: [
+ {
+ value: 'label-unselected-able',
+ disabled: false,
+ onSelect: () => {},
+ selected: false,
+ },
+ {
+ value: 'label-unselected-disable',
+ disabled: true,
+ onSelect: () => {},
+ selected: false,
+ },
+ ],
+ },
+ {
+ title: 'Checked',
+ items: [
+ {
+ value: 'label-selected-able',
+ disabled: false,
+ onSelect: () => {},
+ selected: true,
+ },
+ {
+ value: 'label-selected-disable',
+ disabled: true,
+ onSelect: () => {},
+ selected: true,
+ },
+ ],
+ },
+ {
+ title: 'Tooltip',
+ items: [
+ {
+ value: 'label-selected-able-Tooltip',
+ disabled: false,
+ onSelect: () => {},
+ selected: true,
+ tooltip: '์์ ํดํ์
๋๋ค.',
+ },
+ {
+ value: 'label-selected-disable-Tooltip',
+ disabled: true,
+ onSelect: () => {},
+ selected: true,
+ tooltip: '์์ ํดํ์
๋๋ค.',
+ },
+ ],
+ },
+];
+
+const MOCK_GROUP: Array> = [
+ {
+ label: 'LabelA',
+ value: 'A',
+ },
+ {
+ label: 'LabelB',
+ value: 'B',
+ },
+];
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 20px;
+`;
+const ItemWrapper = styled.div`
+ display: flex;
+ gap: 10px;
+`;
+
+export default {
+ title: 'Components/Radio',
+ component: Radio.Item,
+ tags: ['autodocs'],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=859-2540&mode=design&t=bhY2SksBs7AqptYx-0',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ label: 'default',
+ value: '',
+ selected: true,
+ disabled: false,
+};
+
+export function RadioItem() {
+ return (
+
+ {MOCK_ITEMS.map(({ title, items }, index) => (
+
+ {title}
+
+
+
+ ))}
+
+ );
+}
+
+export const RadioGroup: StoryFn = () => {
+ const [rowSelected, setRowSelected] = useState('');
+ const [columnSelected, setColumnSelected] = useState('');
+
+ return (
+
+ CDS ํผ๊ทธ๋ง X, ๊ฐ๋ฐ๋จ ์ปดํฌ๋ํธ
+ Row Group
+ {
+ setRowSelected(v);
+ }}
+ options={MOCK_GROUP}
+ />
+ Column Group
+ {
+ setColumnSelected(v);
+ }}
+ options={MOCK_GROUP.map(item => ({
+ ...item,
+ value: `${item.value}-column`,
+ }))}
+ />
+
+ );
+};
+
+RadioGroup.bind({});
diff --git a/src/stories/Select.stories.tsx b/src/stories/Select.stories.tsx
new file mode 100644
index 00000000..1dfaf8a8
--- /dev/null
+++ b/src/stories/Select.stories.tsx
@@ -0,0 +1,139 @@
+import { StoryFn } from '@storybook/react';
+import { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { type BasicOptionItem, Select } from '../components';
+import Icon from '../components/Icon';
+
+export default {
+ title: 'Components/Select',
+ component: Select,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-14814&mode=design&t=8a3wutSi3o4LHHer-4',
+ },
+ },
+};
+
+const BASIC: BasicOptionItem[] = [
+ { label: 'Label1 with Icon', value: '1', icon: Icon.QuestionCircle },
+ {
+ label: 'Label2 with Icon and disabled',
+ value: '2',
+ disabled: true,
+ icon: Icon.Shop,
+ },
+ { label: 'Label3 with just disabled', value: '3', disabled: true },
+ { label: 'Label4', value: '4' },
+ { label: 'Label5', value: '5' },
+];
+
+export function Default() {
+ const [defaultValue, setDefaultValue] = useState('');
+ const [createValue, setCreateValue] = useState('');
+ const [searchableValue, setSearchableValue] = useState('');
+ const [withoutIconValue, setWithoutIcon] = useState('');
+ const [selectItems, setSelectItems] = useState(BASIC);
+
+ return (
+ <>
+ {
+ setDefaultValue(value);
+ }}
+ options={BASIC}
+ />
+
+ {
+ setCreateValue(value);
+ }}
+ options={selectItems}
+ onCreate={v => {
+ {
+ setSelectItems(prev => {
+ return [...prev, { label: v, value: v }] as BasicOptionItem[];
+ });
+ }
+ }}
+ searchable
+ />
+ {
+ setSearchableValue(value);
+ }}
+ options={[
+ { value: '1', label: '1' },
+ { value: '2', label: '2' },
+ { value: '3', label: '3' },
+ { value: '4', label: '4' },
+ { value: '5', label: '5' },
+ { value: '6', label: '6' },
+ { value: '7', label: '7' },
+ { value: '8', label: '8' },
+ ]}
+ />
+ {
+ setWithoutIcon(value);
+ }}
+ options={[
+ { value: '1', label: '1' },
+ { value: '2', label: '2' },
+ { value: '3', label: '3' },
+ { value: '4', label: '4' },
+ { value: '5', label: '5' },
+ { value: '6', label: '6' },
+ { value: '7', label: '7' },
+ { value: '8', label: '8' },
+ ]}
+ />
+ {}}
+ options={[{ label: '๊ณต๋ฐฑ์ด์์ด๋ผ๋ฒจ์ด๊ธด์์', value: '' }, ...BASIC]}
+ />
+ >
+ );
+}
+
+const Container = styled.div`
+ width: 100%;
+ height: 100%;
+ min-height: 600px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ padding: 50px;
+ text-align: center;
+ line-height: normal;
+ gap: 20px;
+`;
diff --git a/src/stories/Spinner.stories.tsx b/src/stories/Spinner.stories.tsx
new file mode 100644
index 00000000..3c541472
--- /dev/null
+++ b/src/stories/Spinner.stories.tsx
@@ -0,0 +1,96 @@
+import styled from '@emotion/styled';
+
+import { B3, Spinner, type SpinnerSize, color } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Spinner',
+ component: Spinner,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?node-id=5196%3A37258&mode=dev',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ size: 'm',
+ speed: 1,
+ color: color['main-black'],
+};
+
+export function Size() {
+ const sizes: SpinnerSize[] = ['s', 'm', 'l'];
+
+ return (
+
+ {sizes.map(size => (
+
+
+ {size.toLocaleUpperCase()}
+
+
+
+ ))}
+
+ );
+}
+
+export function Color() {
+ const sizes: SpinnerSize[] = ['s', 'm', 'l'];
+ const colors = ['#0085ff', '#ae15f6', '#ff4545'];
+
+ return (
+
+ {sizes.map(size => (
+
+ {colors.map(color => (
+
+
+
+ ))}
+
+ ))}
+
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
+
+const SizeContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
+
+const SizeContents = styled.div`
+ display: flex;
+ gap: 20px;
+ align-items: center;
+`;
+
+const SizeContentText = styled.div`
+ width: 20px;
+`;
+
+const ColorContainer = styled.div`
+ display: flex;
+ gap: 20px;
+`;
diff --git a/src/stories/Switch.stories.tsx b/src/stories/Switch.stories.tsx
new file mode 100644
index 00000000..757d17a2
--- /dev/null
+++ b/src/stories/Switch.stories.tsx
@@ -0,0 +1,69 @@
+import { useState } from 'react';
+
+import styled from '@emotion/styled';
+
+import { Switch } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Switch',
+ component: Switch,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-15115&mode=design&t=bhY2SksBs7AqptYx-0',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+export const Basic = Template.bind({});
+Basic.args = {
+ disabled: false,
+ checked: true,
+};
+
+export function Active() {
+ const [active, setActive1] = useState(false);
+ const [disabled1, setdDisabled1] = useState(true);
+ const [disabled2, setdDisabled2] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
+
+const SwitchContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
+
+const SwitchWrapper = styled.div`
+ display: flex;
+ gap: 15px;
+`;
diff --git a/src/stories/Table.stories.tsx b/src/stories/Table.stories.tsx
new file mode 100644
index 00000000..b76c7b1d
--- /dev/null
+++ b/src/stories/Table.stories.tsx
@@ -0,0 +1,520 @@
+import { StoryFn } from '@storybook/react';
+import React from 'react';
+
+import styled from '@emotion/styled';
+
+import { B1, Badge, Checkbox, Radio, Select, Switch } from '../components';
+import Icon from '../components/Icon';
+import Table from '../components/Table';
+import { type TdSizeType } from '../components/Table/Td';
+import { FIXED_TH_WIDTH, type ThSizeType } from '../components/Table/Th';
+
+export default {
+ title: 'Components/Table',
+ component: Table,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-15137&mode=design&t=DYlDq6LjK89PKCPh-4',
+ },
+ },
+};
+
+const MOCK_DATA = [
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1: 'Label',
+ Label2: 'Label',
+ Label3: 'Label',
+ Label4: 'Label',
+ Label5: 'Label',
+ Label6: 'Label',
+ Label7: 'Label',
+ Badge: 'Label',
+ },
+];
+
+const MOCK_WITH_ELLIPSIS = [
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+ {
+ img: 'https://picsum.photos/seed/picsum/50/50',
+ Label1:
+ '๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ๊ฐ๋๋ค๋ผ๋ง๋ฐ์ฌ',
+ Label2: '123456',
+ Label3: '์ปค๋ฒ๋ซ ๋ฐํํฐ',
+ Label4: 'โฉ 2,530,000',
+ Label5: '2023๋
10์ 10์ผ (๊ธ)',
+ Badge: 'Label',
+ },
+];
+
+export const Basic = () => {
+ const size = 'm';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {},
+ }}
+ size={size}
+ />
+
+
+
+ {MOCK_DATA.map((d, index) => (
+ {}}>
+
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+
+
+
+
+ {}}
+ options={[
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ]}
+ />
+
+
+ {}} />
+
+
+ {}} />
+
+
+
+
+
+ {}} />
+
+
+ ))}
+
+
+ );
+};
+
+export const ThWithIcon = () => {
+ const thSizeList: ThSizeType[] = ['l', 'm', 's'];
+
+ return (
+
+ {thSizeList.map(size => (
+
+ Th Size - {size.toLocaleUpperCase()}
+
+
+
+
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+
+
+
+ {},
+ }}
+ size={size}
+ />
+
+
+
+ {MOCK_DATA.map((d, index) => (
+ {}}>
+
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+
+
+
+
+ {}}
+ options={[
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ]}
+ />
+
+
+ {}} />
+
+
+ {}} />
+
+
+
+
+
+ {}} />
+
+
+ ))}
+
+
+
+ ))}
+
+ );
+};
+
+export function Size() {
+ const sizeList: Array<{
+ th: ThSizeType;
+ td: TdSizeType;
+ }> = [
+ {
+ th: 'l',
+ td: 'l',
+ },
+ {
+ th: 'm',
+ td: 'm',
+ },
+ {
+ th: 's',
+ td: 's',
+ },
+ {
+ th: 's',
+ td: 'xs',
+ },
+ ];
+
+ return (
+
+ {sizeList.map(({ th, td }, index) => (
+
+
+ Head({th.toLocaleUpperCase()}) & Td(
+ {td.toLocaleUpperCase()})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {},
+ }}
+ size={th}
+ />
+
+
+
+ {MOCK_DATA.map((d, index) => (
+ {}}>
+
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+ {d.Label1}
+
+
+
+
+ {}} />
+
+
+ {}} />
+
+
+
+
+
+ {}} />
+
+
+ ))}
+
+
+
+ ))}
+
+ );
+}
+
+export function WithEllipsis() {
+ const thSize = 'm';
+ const [checkedCheckboxList, setCheckedCheckboxList] = React.useState([]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (checkedCheckboxList.length === MOCK_DATA.length) {
+ setCheckedCheckboxList([]);
+ } else {
+ setCheckedCheckboxList(MOCK_DATA.map((_, i) => i));
+ }
+ },
+ }}
+ size={thSize}
+ />
+
+
+
+ {MOCK_WITH_ELLIPSIS.map((d, index) => (
+ {
+ alert(`clicked - ${index} st row`);
+ }}
+ >
+
+
+ {d.Label1}
+
+ {d.Label2}
+ {d.Label3}
+ {d.Label4}
+ {d.Label5}
+
+
+
+
+ {}}
+ options={[
+ { label: 'Option 1', value: '1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3' },
+ ]}
+ />
+
+
+ {}} />
+
+
+ {}} />
+
+
+
+
+
+ {
+ if (checkedCheckboxList.includes(index)) {
+ setCheckedCheckboxList(checkedCheckboxList.filter(i => i !== index));
+ } else {
+ setCheckedCheckboxList([...checkedCheckboxList, index]);
+ }
+ }}
+ />
+
+
+ ))}
+
+
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ width: 1278px;
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 100px;
+ padding: 30px 0;
+`;
+
+const Contents = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`;
diff --git a/src/stories/Tabs.stories.tsx b/src/stories/Tabs.stories.tsx
new file mode 100644
index 00000000..bc3f67dd
--- /dev/null
+++ b/src/stories/Tabs.stories.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import styled from '@emotion/styled';
+
+import { Tabs } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Tabs',
+ component: Tabs,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=933-7028&mode=design&t=ML3YPcDZJzLhfFhw-0',
+ },
+ docs: {
+ description: {
+ component: 'Tab item์ width๋ 74px๋ก, ํด๋น width๋ณด๋ค ๊ธ์๊ฐ ๊ธธ์ด์ง๋ฉด ...์ผ๋ก ํ์๋ฉ๋๋ค.',
+ },
+ },
+ },
+} as Meta;
+
+export function Default() {
+ const [selected, setSelected] = React.useState('1');
+
+ return (
+ {
+ setSelected(value);
+ }}
+ />
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/src/stories/Thumbnail.stories.tsx b/src/stories/Thumbnail.stories.tsx
new file mode 100644
index 00000000..e8ee1d24
--- /dev/null
+++ b/src/stories/Thumbnail.stories.tsx
@@ -0,0 +1,74 @@
+import styled from '@emotion/styled';
+
+import { B3, Thumbnail } from '../components';
+import type { Meta, StoryFn } from '@storybook/react';
+
+export default {
+ title: 'Components/Thumbnail',
+ component: Thumbnail,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?node-id=435%3A15184&mode=dev',
+ },
+ },
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Basic = Template.bind({});
+Basic.args = {
+ url: 'https://picsum.photos/id/237/200/300',
+ alt: 'cute dog',
+ size: 54,
+};
+
+export function Size() {
+ const sizes = [54, 72, 90];
+
+ return (
+ <>
+ {sizes.map(size => (
+
+
+ {`Size ${size}px`}
+
+
+
+
+
+
+ ))}
+ >
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 20px;
+`;
+
+const SizeTextWrapper = styled.div`
+ display: flex;
+ padding-right: 30px;
+ align-items: center;
+`;
+
+const SizeContentsWrapper = styled.div`
+ display: flex;
+`;
+
+const SizeContents = styled.div`
+ display: flex;
+ gap: 20px;
+`;
diff --git a/src/stories/Toast.stories.tsx b/src/stories/Toast.stories.tsx
new file mode 100644
index 00000000..4e2f142c
--- /dev/null
+++ b/src/stories/Toast.stories.tsx
@@ -0,0 +1,76 @@
+import { StoryFn } from '@storybook/react';
+
+import styled from '@emotion/styled';
+
+import { Button, Toast, ToastPrepare } from '../components';
+
+export default {
+ title: 'Components/Toast',
+ component: Toast,
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+ <>
+
+
+ >
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=1086-18652&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+};
+
+export function Default() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/src/stories/Topbar.stories.tsx b/src/stories/Topbar.stories.tsx
new file mode 100644
index 00000000..aacdecd1
--- /dev/null
+++ b/src/stories/Topbar.stories.tsx
@@ -0,0 +1,69 @@
+import { StoryFn } from '@storybook/react';
+
+import styled from '@emotion/styled';
+
+import { ProfileMenu, type ProfileMenuProps, Title as TitleComponent } from '../components';
+import Icon from '../components/Icon';
+import { type ProfileOption } from '../components/Topbar/ProfileMenu';
+
+export default {
+ title: 'Components/Topbar',
+ tags: ['autodocs'],
+ decorators: [
+ (Story: StoryFn) => (
+
+
+
+ ),
+ ],
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-15200&mode=design&t=0RPfKYm5afDTRSEE-0',
+ },
+ },
+};
+
+const ProfileTemplate: StoryFn = args => ;
+
+const profileOptions: ProfileOption[] = [
+ {
+ label: '๋ก๊ทธ์์',
+ onClick: () => {
+ alert('๋ก๊ทธ์์');
+ },
+ icon: ,
+ },
+ {
+ label: '์
๋ฌ ์ ํ',
+ onClick: () => {
+ alert('์
๋ฌ ์ ํ');
+ },
+ icon: ,
+ },
+];
+
+export const Profile = ProfileTemplate.bind({});
+const profileArgs: ProfileMenuProps = {
+ name: 'Client name',
+ avatar:
+ 'https://mblogthumb-phinf.pstatic.net/MjAxOTA3MTVfMjA0/MDAxNTYzMTc2Mzc5NTAy.lh9RRCYZCuuD_nYPyNdbhiJzdd7_YuUxfyzTWHX1flEg.sL2fBPI0Iglgm1lILEORTWRyb66n6PXgBLf2c2eyuiYg.JPEG.petgeek/cici.toto.mametchi_55864983_674522579671640_592800947380049308_n.jpg?type=w800',
+ description: 'Description',
+ title: '๊ณ์ ',
+ email: 'store@corca.ai',
+ options: profileOptions,
+};
+
+Profile.args = profileArgs;
+
+const TitleTemplate: StoryFn = args => ;
+
+export const Title = TitleTemplate.bind({});
+Title.args = {
+ title: 'Title',
+ description: 'Description',
+};
+
+const Container = styled.div`
+ padding: 10px 80px;
+`;
diff --git a/src/stories/Typography.stories.tsx b/src/stories/Typography.stories.tsx
new file mode 100644
index 00000000..e3ec61d3
--- /dev/null
+++ b/src/stories/Typography.stories.tsx
@@ -0,0 +1,176 @@
+import styled from '@emotion/styled';
+
+import {
+ B1,
+ B2,
+ B3,
+ B4,
+ B5,
+ B6,
+ B7,
+ H1,
+ H2,
+ Text,
+ color,
+ fontStyle,
+ typography,
+} from '../components';
+
+export default {
+ title: 'Foundation/Typography',
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/tN7Q8dJZqVsjo2FWflOKfu/CDS(Corca-Design-System)?type=design&node-id=435-16895&mode=design&t=90XgbxcTRUn2ydEs-4',
+ },
+ },
+};
+
+export function Style() {
+ const textSizeArray: Array = [
+ 'h1',
+ 'h2',
+ 'b1',
+ 'b2',
+ 'b3',
+ 'b4',
+ 'b5',
+ 'b6',
+ 'b7',
+ ];
+
+ return (
+
+ {textSizeArray.map((style, index) => (
+
+ ))}
+
+ );
+}
+
+const numToFontWeight = Object.entries(typography.weight).reduce(
+ (acc, [k, v]) => ({ ...acc, [v]: k }),
+ {},
+) as Array;
+
+const styleSheet = {
+ h1: {
+ name: 'H1/heading1',
+ component: H1,
+ fontSize: fontStyle.h1.fontSize,
+ fontWeight: numToFontWeight[fontStyle.h1.fontWeight],
+ },
+ h2: {
+ name: 'H2/heading2',
+ component: H2,
+ fontSize: fontStyle.h2.fontSize,
+ fontWeight: numToFontWeight[fontStyle.h2.fontWeight],
+ },
+ b1: {
+ name: 'B1/Body1',
+ component: B1,
+ fontSize: fontStyle.b1.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b1.fontWeight],
+ },
+ b2: {
+ name: 'B2/Body2',
+ component: B2,
+ fontSize: fontStyle.b2.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b2.fontWeight],
+ },
+ b3: {
+ name: 'B3/Body3',
+ component: B3,
+ fontSize: fontStyle.b3.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b3.fontWeight],
+ },
+ b4: {
+ name: 'B4/Body4',
+ component: B4,
+ fontSize: fontStyle.b4.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b4.fontWeight],
+ },
+ b5: {
+ name: 'B5/Body5',
+ component: B5,
+ fontSize: fontStyle.b5.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b5.fontWeight],
+ },
+ b6: {
+ name: 'B6/Body6',
+ component: B6,
+ fontSize: fontStyle.b6.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b6.fontWeight],
+ },
+ b7: {
+ name: 'B7/Body7',
+ component: B7,
+ fontSize: fontStyle.b7.fontSize,
+ fontWeight: numToFontWeight[fontStyle.b7.fontWeight],
+ },
+};
+
+function StyleText({ style }: { style: keyof typeof styleSheet }) {
+ const { component: Component, fontSize, fontWeight } = styleSheet[style];
+ return (
+
+ {{styleSheet[style].name}}
+
+ {fontSize}px
+ {fontWeight}
+ Pretendard
+
+
+ );
+}
+
+export function Size() {
+ return (
+
+ {(Object.keys(typography.size) as Array).map((size, index) => (
+
+ font-size-{size}
+ {typography.size[size]}px
+
+ ))}
+
+ );
+}
+
+export function Weight() {
+ return (
+
+ {(Object.keys(typography.weight) as Array).map(
+ (weight, index) => (
+
+ font-size-{weight}
+ {typography.weight[weight]}
+
+ ),
+ )}
+
+ );
+}
+
+const Container = styled.div`
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+`;
+
+const ContentWrapper = styled.div`
+ display: flex;
+ height: 60px;
+ padding: 0 30px;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid ${color['grey-20']};
+`;
+
+const TextWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 50px;
+ width: 300px;
+`;
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 00000000..fd8744c4
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,27 @@
+import { type SupportLocale } from './types/locale.types';
+
+export const formatDateByLanguage = (date: Date, language: SupportLocale) => {
+ if (!date) {
+ return;
+ }
+ const week = ['์ผ', '์', 'ํ', '์', '๋ชฉ', '๊ธ', 'ํ '];
+
+ switch (language) {
+ case 'ko':
+ return `${date.getFullYear()}๋
${
+ date.getMonth() + 1
+ }์ ${date.getDate()}์ผ (${week[date.getDay()]})`;
+ case 'en':
+ return date.toDateString();
+ default:
+ return date.toDateString();
+ }
+};
+
+export const formatDateTimeByLanguage = (date: Date, language: SupportLocale) => {
+ if (!date) {
+ return;
+ }
+ const dateFormat = formatDateByLanguage(date, language);
+ return `${dateFormat} ${date.toLocaleTimeString(language)}`;
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 00000000..f4f677d9
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './date';
+export * from './types/locale.types';
diff --git a/src/utils/types/locale.types.ts b/src/utils/types/locale.types.ts
new file mode 100644
index 00000000..9abab6bb
--- /dev/null
+++ b/src/utils/types/locale.types.ts
@@ -0,0 +1 @@
+export type SupportLocale = 'ko' | 'en' | 'vn';
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..c3dcc406
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "es2015",
+ "module": "esnext",
+ "useDefineForClassFields": true,
+ "lib": ["es2017", "dom"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "skipDefaultLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "baseUrl": ".",
+ "jsx": "preserve",
+ "jsxImportSource": "@emotion/react",
+ "incremental": true,
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "importHelpers": true
+ },
+ "include": ["src"],
+ "exclude": ["**/dist/**, **/node_modules", "**/*.stories.tsx"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 00000000..9d31e2ae
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 00000000..45c873bf
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,5 @@
+{
+ "git": {
+ "deploymentEnabled": false
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..0d3c9262
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,15 @@
+import { resolve } from 'path';
+import { defineConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: resolve(__dirname, './src/index.ts'),
+ formats: ['es'],
+ name: 'cds',
+ fileName: 'index',
+ },
+ },
+ plugins: [dts()],
+});