diff --git a/package.json b/package.json
index aa4ec8f..774e4bd 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"clsx": "^2.1.1",
+ "framer-motion": "^12.34.0",
"next": "16.1.3",
"react": "19.2.3",
"react-calendar": "^6.0.0",
@@ -26,31 +27,31 @@
"zod": "^4.3.5"
},
"devDependencies": {
+ "@chromatic-com/storybook": "^5.0.0",
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
+ "@storybook/addon-a11y": "^10.2.3",
+ "@storybook/addon-docs": "^10.2.3",
+ "@storybook/addon-vitest": "^10.2.3",
+ "@storybook/nextjs-vite": "^10.2.3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@vitest/browser-playwright": "^4.0.18",
+ "@vitest/coverage-v8": "^4.0.18",
"eslint": "^9",
"eslint-config-next": "16.1.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
+ "eslint-plugin-storybook": "^10.2.3",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
+ "playwright": "^1.58.1",
"prettier": "^3.8.0",
- "typescript": "^5",
"storybook": "^10.2.3",
- "@storybook/nextjs-vite": "^10.2.3",
- "@chromatic-com/storybook": "^5.0.0",
- "@storybook/addon-vitest": "^10.2.3",
- "@storybook/addon-a11y": "^10.2.3",
- "@storybook/addon-docs": "^10.2.3",
+ "typescript": "^5",
"vite": "^7.3.1",
- "eslint-plugin-storybook": "^10.2.3",
- "vitest": "^4.0.18",
- "playwright": "^1.58.1",
- "@vitest/browser-playwright": "^4.0.18",
- "@vitest/coverage-v8": "^4.0.18"
+ "vitest": "^4.0.18"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 81805e3..3d792c8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ framer-motion:
+ specifier: ^12.34.0
+ version: 12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next:
specifier: 16.1.3
version: 16.1.3(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -2024,6 +2027,20 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
+ framer-motion@12.34.0:
+ resolution: {integrity: sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2557,6 +2574,12 @@ packages:
module-alias@2.2.3:
resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==}
+ motion-dom@12.34.0:
+ resolution: {integrity: sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==}
+
+ motion-utils@12.29.2:
+ resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
+
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -5427,6 +5450,15 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ framer-motion@12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ motion-dom: 12.34.0
+ motion-utils: 12.29.2
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
fsevents@2.3.2:
optional: true
@@ -5930,6 +5962,12 @@ snapshots:
module-alias@2.2.3: {}
+ motion-dom@12.34.0:
+ dependencies:
+ motion-utils: 12.29.2
+
+ motion-utils@12.29.2: {}
+
mrmime@2.0.1: {}
ms@2.1.3: {}
diff --git a/src/components/input/AccountInput.stories.tsx b/src/components/input/AccountInput.stories.tsx
new file mode 100644
index 0000000..f9c41cd
--- /dev/null
+++ b/src/components/input/AccountInput.stories.tsx
@@ -0,0 +1,74 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+
+import AccountInput from './AccountInput';
+
+const meta = {
+ title: 'Components/AccountInput',
+ component: AccountInput,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ args: {
+ email: 'user@example.com',
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithButton: Story = {
+ args: {
+ email: 'user@example.com',
+ children: (
+
+ ),
+ },
+};
+
+export const Overview: Story = {
+ render: () => (
+
+ ),
+ parameters: {
+ controls: { disable: true },
+ },
+};
diff --git a/src/components/input/ActionTextArea.stories.tsx b/src/components/input/ActionTextArea.stories.tsx
new file mode 100644
index 0000000..cb7ea88
--- /dev/null
+++ b/src/components/input/ActionTextArea.stories.tsx
@@ -0,0 +1,54 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+
+import { fn } from 'storybook/test';
+
+import ActionTextArea from './ActionTextArea';
+
+const meta = {
+ title: 'Components/ActionTextArea',
+ component: ActionTextArea,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ args: {
+ placeholder: '텍스트를 입력해 주세요.',
+ onSubmit: fn(),
+ onChange: fn(),
+ },
+ argTypes: {
+ disabled: {
+ control: 'boolean',
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithPlaceholder: Story = {
+ args: {
+ placeholder: '댓글을 입력해 주세요.',
+ },
+};
+
+export const Overview: Story = {
+ render: () => (
+
+ ),
+ parameters: {
+ controls: { disable: true },
+ },
+};
diff --git a/src/components/input/ChangePassword.stories.tsx b/src/components/input/ChangePassword.stories.tsx
new file mode 100644
index 0000000..4fcb328
--- /dev/null
+++ b/src/components/input/ChangePassword.stories.tsx
@@ -0,0 +1,116 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+
+import { fn } from 'storybook/test';
+
+import ChangePassword from './ChangePassword';
+
+const meta = {
+ title: 'Components/ChangePassword',
+ component: ChangePassword,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ args: {
+ isEditing: false,
+ newPasswordProps: {
+ onChange: fn(),
+ },
+ confirmPasswordProps: {
+ onChange: fn(),
+ },
+ },
+ argTypes: {
+ isEditing: {
+ control: 'boolean',
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Editing: Story = {
+ args: {
+ isEditing: true,
+ },
+};
+
+export const WithError: Story = {
+ args: {
+ isEditing: true,
+ confirmPasswordProps: {
+ errorMessage: '비밀번호가 일치하지 않습니다.',
+ },
+ },
+};
+
+export const WithButtons: Story = {
+ args: {
+ isEditing: true,
+ children: (
+
+
+
+
+ ),
+ },
+};
+
+export const Overview: Story = {
+ render: () => (
+
+ ),
+ parameters: {
+ controls: { disable: true },
+ },
+};
diff --git a/src/components/input/CommentInput.stories.tsx b/src/components/input/CommentInput.stories.tsx
new file mode 100644
index 0000000..bb33c3d
--- /dev/null
+++ b/src/components/input/CommentInput.stories.tsx
@@ -0,0 +1,42 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+
+import { fn } from 'storybook/test';
+
+import CommentInput from './CommentInput';
+
+const meta = {
+ title: 'Components/CommentInput',
+ component: CommentInput,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ args: {
+ placeholder: '댓글을 달아주세요.',
+ onSubmit: fn(),
+ onChange: fn(),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Overview: Story = {
+ render: () => (
+
+
+
+ ),
+ parameters: {
+ controls: { disable: true },
+ },
+};
diff --git a/src/components/input/TextArea.stories.tsx b/src/components/input/TextArea.stories.tsx
new file mode 100644
index 0000000..92a17bd
--- /dev/null
+++ b/src/components/input/TextArea.stories.tsx
@@ -0,0 +1,65 @@
+import type { Meta, StoryObj } from '@storybook/nextjs-vite';
+
+import { fn } from 'storybook/test';
+
+import TextArea from './TextArea';
+
+const meta = {
+ title: 'Components/TextArea',
+ component: TextArea,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ args: {
+ placeholder: '텍스트를 입력해 주세요.',
+ onChange: fn(),
+ },
+ argTypes: {
+ rows: {
+ control: 'number',
+ },
+ disabled: {
+ control: 'boolean',
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithRows: Story = {
+ args: {
+ rows: 5,
+ placeholder: '여러 줄 입력',
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ disabled: true,
+ value: '비활성 상태',
+ },
+};
+
+export const Overview: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+ parameters: {
+ controls: { disable: true },
+ },
+};
diff --git a/src/components/sidebar/Sidebar.stories.tsx b/src/components/sidebar/Sidebar.stories.tsx
index e4b0a3a..586f5ef 100644
--- a/src/components/sidebar/Sidebar.stories.tsx
+++ b/src/components/sidebar/Sidebar.stories.tsx
@@ -4,7 +4,7 @@ import Image from 'next/image';
import Sidebar from './Sidebar';
import SidebarButton from './SidebarButton';
import SidebarTeamSelect from './SidebarTeamSelect';
-import SidebarAddButton from './SidebarAddButton';
+import GnbAddButton from '@/components/Button/domain/GnbAddButton/GnbAddButton';
import chessSmall from '@/assets/icons/chess/chessSmall.svg';
import chessBig from '@/assets/icons/chess/chessBig.svg';
import boardSmall from '@/assets/icons/board/boardSmall.svg';
@@ -41,7 +41,7 @@ export const LoggedIn: Story = {
),
addButton: (isCollapsed: boolean) => (
<>
- {!isCollapsed && }
+ {!isCollapsed && {}} />}
ReactNode` 형태로 접힘 상태를 받을 수 있습니다.
*/
+type SlotNode = ReactNode | ((isCollapsed: boolean) => ReactNode);
+
export default function Sidebar({
teamSelect,
addButton,
@@ -37,15 +40,42 @@ export default function Sidebar({
}: SidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
+ const renderSlot = (slot: SlotNode) => {
+ if (!slot) return null;
+ return typeof slot === 'function' ? slot(isCollapsed) : slot;
+ };
+
return (
-
+
);
}
diff --git a/src/components/sidebar/styles/Sidebar.module.css b/src/components/sidebar/styles/Sidebar.module.css
index 7d29474..ea5d6dd 100644
--- a/src/components/sidebar/styles/Sidebar.module.css
+++ b/src/components/sidebar/styles/Sidebar.module.css
@@ -14,7 +14,6 @@
}
.collapsed {
- width: 72px;
position: relative;
}
@@ -64,6 +63,8 @@
padding: 0 16px;
gap: 8px;
flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
}
.collapsed .content {