diff --git a/src/components/comment/styles/CommentCard.module.css b/src/components/comment/styles/CommentCard.module.css index 9e8f735..70fb593 100644 --- a/src/components/comment/styles/CommentCard.module.css +++ b/src/components/comment/styles/CommentCard.module.css @@ -1,6 +1,17 @@ .card { + position: relative; display: flex; gap: 12px; + padding-top: 16px; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 16px; + right: 0; + border-top: 1px solid var(--color-background-tertiary); } .avatar { diff --git a/src/components/input/CommentInput.tsx b/src/components/input/CommentInput.tsx index 8c7ef0c..86fbdde 100644 --- a/src/components/input/CommentInput.tsx +++ b/src/components/input/CommentInput.tsx @@ -8,7 +8,18 @@ import styles from './styles/CommentInput.module.css'; * ActionTextArea를 위아래 보더 스타일로 감싸서 댓글 영역에 맞는 디자인을 제공합니다. * 전송 버튼과 높이 자동 조절은 ActionTextArea에서 상속됩니다. */ -export default function CommentInput({ className, ...props }: CommentInputProps) { +export default function CommentInput({ className, profileImage, ...props }: CommentInputProps) { + if (profileImage) { + return ( +
+
{profileImage}
+
+ +
+
+ ); + } + return ( ; +export type CommentInputProps = Omit & { + /** 입력 필드 왼쪽에 표시할 프로필 이미지 */ + profileImage?: ReactNode; +}; export type AccountInputProps = { /** 표시할 이메일 주소 (읽기 전용) */ diff --git a/src/components/sidebar/MobileDrawer.stories.tsx b/src/components/sidebar/MobileDrawer.stories.tsx index 13c710f..82741ea 100644 --- a/src/components/sidebar/MobileDrawer.stories.tsx +++ b/src/components/sidebar/MobileDrawer.stories.tsx @@ -43,6 +43,7 @@ export const Open: Story = { } label="자유게시판" + href="/boards" /> ), diff --git a/src/components/sidebar/MobileHeader.stories.tsx b/src/components/sidebar/MobileHeader.stories.tsx index 8e579e4..a17befa 100644 --- a/src/components/sidebar/MobileHeader.stories.tsx +++ b/src/components/sidebar/MobileHeader.stories.tsx @@ -12,6 +12,10 @@ const meta = { viewport: { defaultViewport: 'mobile1' }, }, tags: ['autodocs'], + argTypes: { + logoWidth: { control: 'number' }, + logoHeight: { control: 'number' }, + }, decorators: [ (Story) => (
@@ -36,3 +40,10 @@ export const LoggedIn: Story = { onProfileClick: fn(), }, }; + +export const CustomLogoSize: Story = { + args: { + logoWidth: 80, + logoHeight: 16, + }, +}; diff --git a/src/components/sidebar/MobileHeader.tsx b/src/components/sidebar/MobileHeader.tsx index 6c04084..1f535fe 100644 --- a/src/components/sidebar/MobileHeader.tsx +++ b/src/components/sidebar/MobileHeader.tsx @@ -17,6 +17,10 @@ type MobileHeaderProps = { onMenuClick?: () => void; /** 프로필 버튼 클릭 시 호출되는 콜백 */ onProfileClick?: () => void; + /** 로고 너비 (기본값: 102) */ + logoWidth?: number; + /** 로고 높이 (기본값: 20) */ + logoHeight?: number; }; /** @@ -29,12 +33,14 @@ export default function MobileHeader({ profileImage, onMenuClick, onProfileClick, + logoWidth = 102, + logoHeight = 20, }: MobileHeaderProps) { if (!isLoggedIn) { return (
- COWORKERS + COWORKERS
); diff --git a/src/components/sidebar/Sidebar.stories.tsx b/src/components/sidebar/Sidebar.stories.tsx index 586f5ef..7991ce4 100644 --- a/src/components/sidebar/Sidebar.stories.tsx +++ b/src/components/sidebar/Sidebar.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import { fn } from 'storybook/test'; import Image from 'next/image'; import Sidebar from './Sidebar'; import SidebarButton from './SidebarButton'; @@ -19,6 +20,10 @@ const meta = { layout: 'fullscreen', }, tags: ['autodocs'], + argTypes: { + defaultCollapsed: { control: 'boolean' }, + isLoggedIn: { control: 'boolean' }, + }, } satisfies Meta; export default meta; @@ -26,6 +31,8 @@ type Story = StoryObj; export const LoggedIn: Story = { args: { + isLoggedIn: true, + onProfileClick: fn(), profileImage: (
), @@ -54,6 +61,7 @@ export const LoggedIn: Story = { } label="자유게시판" iconOnly={isCollapsed} + href="/boards" /> ), @@ -83,22 +91,22 @@ export const LoggedIn: Story = { export const LoggedOut: Story = { args: { - footer: (isCollapsed: boolean) => - isCollapsed ? ( - 로그인 - ) : ( -
-
- 로그인 -
- ), + isLoggedIn: false, + onProfileClick: fn(), + }, +}; + +export const LoggedOutCollapsed: Story = { + args: { + isLoggedIn: false, + defaultCollapsed: true, + onProfileClick: fn(), + }, +}; + +export const DefaultCollapsed: Story = { + args: { + ...LoggedIn.args, + defaultCollapsed: true, }, }; diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index ecc6f29..baa0da2 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -11,6 +11,7 @@ import logoLarge from '@/assets/logos/logoLarge.svg'; import logoIcon from '@/assets/logos/logoIcon.svg'; import foldLeftLarge from '@/assets/icons/fold/foldLeftLarge.svg'; import foldRightLarge from '@/assets/icons/fold/foldRightLarge.svg'; +import humanBig from '@/assets/buttons/human/humanBig.svg'; type SidebarProps = { teamSelect?: ReactNode | ((isCollapsed: boolean) => ReactNode); @@ -20,6 +21,9 @@ type SidebarProps = { profileImage?: ReactNode; profileName?: string; profileTeam?: string; + defaultCollapsed?: boolean; + isLoggedIn?: boolean; + onProfileClick?: () => void; }; /** @@ -37,14 +41,70 @@ export default function Sidebar({ profileImage, profileName, profileTeam, + defaultCollapsed, + isLoggedIn, + onProfileClick, }: SidebarProps) { - const [isCollapsed, setIsCollapsed] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed ?? false); const renderSlot = (slot: SlotNode) => { if (!slot) return null; return typeof slot === 'function' ? slot(isCollapsed) : slot; }; + const renderFooter = () => { + if (footer) { + return ( +
+ {renderSlot(footer)} +
+ ); + } + + if (!isLoggedIn) { + return ( + + + {!isCollapsed && ( + + + + )} + + + 로그인 + + + ); + } + + return ( +
+
{profileImage}
+ + {!isCollapsed && ( + + {profileName} + {profileTeam} + + )} + +
+ ); + }; + return (
- {footer ? ( -
{renderSlot(footer)}
- ) : profileImage ? ( -
-
{profileImage}
- - {!isCollapsed && ( - - {profileName} - {profileTeam} - - )} - -
- ) : null} + {renderFooter()} ); } diff --git a/src/components/sidebar/SidebarButton.stories.tsx b/src/components/sidebar/SidebarButton.stories.tsx index e19419f..8e095e9 100644 --- a/src/components/sidebar/SidebarButton.stories.tsx +++ b/src/components/sidebar/SidebarButton.stories.tsx @@ -6,6 +6,7 @@ import Image from 'next/image'; import SidebarButton from './SidebarButton'; import chessSmall from '@/assets/icons/chess/chessSmall.svg'; import chessBig from '@/assets/icons/chess/chessBig.svg'; +import boardSmall from '@/assets/icons/board/boardSmall.svg'; const meta = { title: 'Components/SidebarButton', @@ -26,6 +27,9 @@ const meta = { iconOnly: { control: 'boolean', }, + href: { + control: 'text', + }, }, decorators: [ (Story) => ( @@ -54,6 +58,14 @@ export const IconOnly: Story = { }, }; +export const WithLink: Story = { + args: { + icon: , + label: '자유게시판', + href: '/boards', + }, +}; + export const Overview: Story = { render: () => (
@@ -71,6 +83,11 @@ export const Overview: Story = { label="경영관리팀" iconOnly /> + } + label="자유게시판" + href="/boards" + />
), parameters: { diff --git a/src/components/sidebar/SidebarButton.tsx b/src/components/sidebar/SidebarButton.tsx index 9793f07..ea79078 100644 --- a/src/components/sidebar/SidebarButton.tsx +++ b/src/components/sidebar/SidebarButton.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link'; import clsx from 'clsx'; import styles from './styles/SidebarButton.module.css'; @@ -6,7 +7,7 @@ import type { SidebarButtonProps } from './types/types'; /** * 사이드바 내 메뉴 항목 버튼. * iconOnly가 true이면 아이콘만 표시하고 라벨은 aria-label로 전환됩니다. - * 사이드바 접힘 상태에서 사용할 수 있습니다. + * href가 있으면 Link로 렌더링됩니다. */ export default function SidebarButton({ icon, @@ -14,16 +15,37 @@ export default function SidebarButton({ isActive, iconOnly, onClick, + href, }: SidebarButtonProps) { + const className = clsx(styles.button, isActive && styles.active, iconOnly && styles.iconOnly); + const content = ( + <> + {icon} + {!iconOnly && label} + + ); + + if (href) { + return ( + + {content} + + ); + } + return ( ); } diff --git a/src/components/sidebar/styles/MobileDrawer.module.css b/src/components/sidebar/styles/MobileDrawer.module.css index 4a347c5..1516ef8 100644 --- a/src/components/sidebar/styles/MobileDrawer.module.css +++ b/src/components/sidebar/styles/MobileDrawer.module.css @@ -6,7 +6,7 @@ display: none; } -@media (max-width: 767px) { +@media (max-width: 1199px) { .overlay { display: block; position: fixed; diff --git a/src/components/sidebar/styles/MobileHeader.module.css b/src/components/sidebar/styles/MobileHeader.module.css index ac0c030..295d1cf 100644 --- a/src/components/sidebar/styles/MobileHeader.module.css +++ b/src/components/sidebar/styles/MobileHeader.module.css @@ -7,12 +7,24 @@ background: var(--color-background-inverse); } -@media (max-width: 767px) { +@media (max-width: 1199px) { .header { display: flex; } } +@media (min-width: 768px) and (max-width: 1199px) { + .header { + height: 72px; + padding: 0 24px; + } + + .logo img { + width: 140px; + height: 28px; + } +} + .loggedIn { justify-content: space-between; } diff --git a/src/components/sidebar/styles/Sidebar.module.css b/src/components/sidebar/styles/Sidebar.module.css index ea5d6dd..8e87f70 100644 --- a/src/components/sidebar/styles/Sidebar.module.css +++ b/src/components/sidebar/styles/Sidebar.module.css @@ -1,13 +1,17 @@ .sidebar { + position: fixed; + top: 0; + left: 0; width: 270px; height: 100vh; border-right: 1px solid var(--color-background-tertiary); display: flex; flex-direction: column; background: var(--color-background-inverse); + z-index: 50; } -@media (max-width: 767px) { +@media (max-width: 1199px) { .sidebar { display: none; } @@ -80,6 +84,14 @@ padding: 16px 0; border-top: 1px solid var(--color-background-tertiary); flex-shrink: 0; + cursor: pointer; +} + +.loginText { + font-family: Pretendard, sans-serif; + font-weight: 600; + font-size: 14px; + color: var(--color-text-tertiary); } .collapsed .footer { diff --git a/src/components/sidebar/styles/SidebarButton.module.css b/src/components/sidebar/styles/SidebarButton.module.css index 0820fa9..cb942b0 100644 --- a/src/components/sidebar/styles/SidebarButton.module.css +++ b/src/components/sidebar/styles/SidebarButton.module.css @@ -13,6 +13,7 @@ font-size: 14px; line-height: 17px; color: var(--color-text-secondary); + text-decoration: none; cursor: pointer; } diff --git a/src/components/sidebar/types/types.ts b/src/components/sidebar/types/types.ts index 7cea58c..67e7df1 100644 --- a/src/components/sidebar/types/types.ts +++ b/src/components/sidebar/types/types.ts @@ -11,4 +11,6 @@ export type SidebarButtonProps = { iconOnly?: boolean; /** 클릭 시 호출되는 콜백 */ onClick?: () => void; + /** 링크 URL (설정 시 태그로 렌더링) */ + href?: string; };