Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/comment/styles/CommentCard.module.css
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion src/components/input/CommentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.withProfile}>
<div className={styles.profileImage}>{profileImage}</div>
<div className={styles.inputArea}>
<ActionTextArea className={clsx(styles.textarea, className)} {...props} />
</div>
</div>
);
}
Comment on lines +11 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conditional rendering for profileImage creates two distinct return paths. While functional, consider extracting the common ActionTextArea rendering logic to avoid duplication and improve readability. This would make the component more concise and easier to maintain.

export default function CommentInput({ className, profileImage, ...props }: CommentInputProps) {
  const actionTextArea = (
    <ActionTextArea className={clsx(styles.textarea, className)} {...props} />
  );

  if (profileImage) {
    return (
      <div className={styles.withProfile}>
        <div className={styles.profileImage}>{profileImage}</div>
        <div className={styles.inputArea}>{actionTextArea}</div>
      </div>
    );
  }

  return (
    <ActionTextArea
      wrapperClassName={styles.wrapper}
      className={clsx(styles.textarea, className)}
      {...props}
    />
  );
}


return (
<ActionTextArea
wrapperClassName={styles.wrapper}
Expand Down
19 changes: 19 additions & 0 deletions src/components/input/styles/CommentInput.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
border-bottom: 1px solid var(--color-background-tertiary);
}

.withProfile {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border: none;
}

.withProfile .inputArea {
flex: 1;
min-width: 0;
border-top: 1px solid var(--color-background-tertiary);
border-bottom: 1px solid var(--color-background-tertiary);
}

.profileImage {
flex-shrink: 0;
}

.textarea {
border: none;
border-radius: 0;
Expand Down
5 changes: 4 additions & 1 deletion src/components/input/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export type ActionTextAreaProps = TextAreaProps & {
wrapperClassName?: string;
};

export type CommentInputProps = Omit<ActionTextAreaProps, 'wrapperClassName'>;
export type CommentInputProps = Omit<ActionTextAreaProps, 'wrapperClassName'> & {
/** 입력 필드 왼쪽에 표시할 프로필 이미지 */
profileImage?: ReactNode;
};

export type AccountInputProps = {
/** 표시할 이메일 주소 (읽기 전용) */
Expand Down
1 change: 1 addition & 0 deletions src/components/sidebar/MobileDrawer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Open: Story = {
<SidebarButton
icon={<Image src={boardSmall} alt="" width={20} height={20} />}
label="자유게시판"
href="/boards"
/>
</>
),
Expand Down
11 changes: 11 additions & 0 deletions src/components/sidebar/MobileHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const meta = {
viewport: { defaultViewport: 'mobile1' },
},
tags: ['autodocs'],
argTypes: {
logoWidth: { control: 'number' },
logoHeight: { control: 'number' },
},
decorators: [
(Story) => (
<div style={{ maxWidth: 375 }}>
Expand All @@ -36,3 +40,10 @@ export const LoggedIn: Story = {
onProfileClick: fn(),
},
};

export const CustomLogoSize: Story = {
args: {
logoWidth: 80,
logoHeight: 16,
},
};
8 changes: 7 additions & 1 deletion src/components/sidebar/MobileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type MobileHeaderProps = {
onMenuClick?: () => void;
/** 프로필 버튼 클릭 시 호출되는 콜백 */
onProfileClick?: () => void;
/** 로고 너비 (기본값: 102) */
logoWidth?: number;
/** 로고 높이 (기본값: 20) */
logoHeight?: number;
};

/**
Expand All @@ -29,12 +33,14 @@ export default function MobileHeader({
profileImage,
onMenuClick,
onProfileClick,
logoWidth = 102,
logoHeight = 20,
}: MobileHeaderProps) {
if (!isLoggedIn) {
return (
<header className={styles.header}>
<div className={styles.logo}>
<Image src={logoSmall} alt="COWORKERS" width={102} height={20} />
<Image src={logoSmall} alt="COWORKERS" width={logoWidth} height={logoHeight} />
</div>
</header>
);
Expand Down
42 changes: 25 additions & 17 deletions src/components/sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,13 +20,19 @@ const meta = {
layout: 'fullscreen',
},
tags: ['autodocs'],
argTypes: {
defaultCollapsed: { control: 'boolean' },
isLoggedIn: { control: 'boolean' },
},
} satisfies Meta<typeof Sidebar>;

export default meta;
type Story = StoryObj<typeof meta>;

export const LoggedIn: Story = {
args: {
isLoggedIn: true,
onProfileClick: fn(),
profileImage: (
<div style={{ width: 40, height: 40, borderRadius: 12, background: '#cbd5e1' }} />
),
Expand Down Expand Up @@ -54,6 +61,7 @@ export const LoggedIn: Story = {
}
label="자유게시판"
iconOnly={isCollapsed}
href="/boards"
/>
</>
),
Expand Down Expand Up @@ -83,22 +91,22 @@ export const LoggedIn: Story = {

export const LoggedOut: Story = {
args: {
footer: (isCollapsed: boolean) =>
isCollapsed ? (
<span style={{ fontSize: 12, fontWeight: 600, color: '#0f172a' }}>로그인</span>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
<div
style={{
width: 40,
height: 40,
borderRadius: 12,
background: '#e2e8f0',
flexShrink: 0,
}}
/>
<span style={{ fontSize: 14, fontWeight: 600, color: '#0f172a' }}>로그인</span>
</div>
),
isLoggedIn: false,
onProfileClick: fn(),
},
};

export const LoggedOutCollapsed: Story = {
args: {
isLoggedIn: false,
defaultCollapsed: true,
onProfileClick: fn(),
},
};

export const DefaultCollapsed: Story = {
args: {
...LoggedIn.args,
defaultCollapsed: true,
},
};
84 changes: 62 additions & 22 deletions src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -20,6 +21,9 @@ type SidebarProps = {
profileImage?: ReactNode;
profileName?: string;
profileTeam?: string;
defaultCollapsed?: boolean;
isLoggedIn?: boolean;
onProfileClick?: () => void;
};

/**
Expand All @@ -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 (
<div className={styles.footer} onClick={onProfileClick}>
{renderSlot(footer)}
</div>
);
}

if (!isLoggedIn) {
return (
<motion.div className={styles.footer} onClick={onProfileClick} layout>
<AnimatePresence>
{!isCollapsed && (
<motion.div
className={styles.profileImage}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<Image src={humanBig} alt="" width={40} height={40} />
</motion.div>
)}
</AnimatePresence>
<motion.span className={styles.loginText} layout transition={{ duration: 0.3 }}>
로그인
</motion.span>
</motion.div>
);
}

return (
<div className={styles.footer} onClick={onProfileClick}>
<div className={styles.profileImage}>{profileImage}</div>
<AnimatePresence>
{!isCollapsed && (
<motion.div
className={styles.profileInfo}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.2 }}
>
<span className={styles.profileName}>{profileName}</span>
<span className={styles.profileTeam}>{profileTeam}</span>
</motion.div>
)}
</AnimatePresence>
</div>
);
Comment on lines +55 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The renderFooter function has become quite large and handles multiple conditional rendering scenarios (footer prop, logged in/out states). Consider breaking this down into smaller, more focused helper functions (e.g., renderLoggedInFooter, renderLoggedOutFooter) to improve readability and maintainability. This will make the logic easier to follow and test.

};

return (
<motion.aside
className={clsx(styles.sidebar, isCollapsed && styles.collapsed)}
Expand Down Expand Up @@ -102,27 +162,7 @@ export default function Sidebar({
</motion.div>
</AnimatePresence>
</div>
{footer ? (
<div className={styles.footer}>{renderSlot(footer)}</div>
) : profileImage ? (
<div className={styles.footer}>
<div className={styles.profileImage}>{profileImage}</div>
<AnimatePresence>
{!isCollapsed && (
<motion.div
className={styles.profileInfo}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.2 }}
>
<span className={styles.profileName}>{profileName}</span>
<span className={styles.profileTeam}>{profileTeam}</span>
</motion.div>
)}
</AnimatePresence>
</div>
) : null}
{renderFooter()}
</motion.aside>
);
}
17 changes: 17 additions & 0 deletions src/components/sidebar/SidebarButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -26,6 +27,9 @@ const meta = {
iconOnly: {
control: 'boolean',
},
href: {
control: 'text',
},
},
decorators: [
(Story) => (
Expand Down Expand Up @@ -54,6 +58,14 @@ export const IconOnly: Story = {
},
};

export const WithLink: Story = {
args: {
icon: <Image src={boardSmall} alt="" width={20} height={20} />,
label: '자유게시판',
href: '/boards',
},
};

export const Overview: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: 240 }}>
Expand All @@ -71,6 +83,11 @@ export const Overview: Story = {
label="경영관리팀"
iconOnly
/>
<SidebarButton
icon={<Image src={boardSmall} alt="" width={20} height={20} />}
label="자유게시판"
href="/boards"
/>
</div>
),
parameters: {
Expand Down
Loading
Loading