Skip to content

Conversation

@dioo1461
Copy link
Contributor

@dioo1461 dioo1461 commented Feb 6, 2025

#️⃣ 연관된 이슈>

📝 작업 내용> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

Avatar 컴포넌트의 기본적인 레이아웃을 구현했습니다.

🙏 여기는 꼭 봐주세요! > 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

Summary by CodeRabbit

  • New Features

    • Introduced an enhanced Avatar component that displays up to four images with a count indicator for additional avatars.
    • Added a new AvatarCount component to display the number of additional avatars when more than four are provided.
    • Implemented dedicated AvatarItem component for rendering individual images with error handling.
    • Introduced Storybook configuration for Avatar component, allowing users to explore different size options with interactive controls.
  • Styling Enhancements

    • Updated CSS styles for avatar components, including border color adjustments and improved responsiveness based on size variations.

@dioo1461 dioo1461 added the 🖥️ FE Frontend label Feb 6, 2025
@dioo1461 dioo1461 added this to the 2차 스프린트 milestone Feb 6, 2025
@dioo1461 dioo1461 self-assigned this Feb 6, 2025
@dioo1461 dioo1461 requested a review from hamo-o as a code owner February 6, 2025 12:07
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This pull request introduces several new Avatar-related features. A Storybook configuration is added to showcase the Avatar component with metadata, controls, and a default story. New components—Avatar, AvatarItem, and AvatarCount—are created to render avatars with a maximum display limit, showing additional count information when necessary. A CSS module is also provided to style the avatar components with responsive size variations.

Changes

File(s) Change Summary
frontend/src/components/Avatar/Avatar.stories.tsx Added Storybook configuration for the Avatar component, including metadata (title, background), argTypes for props (size, imageUrls), and type-safe default story exports.
frontend/src/components/Avatar/AvatarCount.tsx, frontend/src/components/Avatar/AvatarItem.tsx, frontend/src/components/Avatar/index.tsx Introduced new Avatar components:
- AvatarItem: Renders individual image elements with size styling.
- AvatarCount: Displays a count when excess images exist.
- Avatar: Renders up to four avatars and conditionally shows the count for additional images.
frontend/src/components/Avatar/index.css.ts Updated avatarItemStyle to change border color and introduced a new selector for the last child element.

Sequence Diagram(s)

sequenceDiagram
    participant UI as User Interface
    participant A as Avatar Component
    participant AI as AvatarItem Component
    participant AC as AvatarCount Component

    UI->>A: Render Avatar(imageUrls, size)
    loop For each imageUrl in first four images
        A->>AI: Render AvatarItem(imageUrl, size)
    end
    alt imageUrls.length > MAX_IMAGE_COUNT
        A->>AC: Render AvatarCount(count = remaining images)
    end
Loading

Poem

Hopping through the code, I spy a new delight,
Avatar tales unfolding in digital light.
Tiny images align with a count so bold,
Styled with care in CSS of gold.
I, a merry rabbit, cheer each change with a skip—coding joy on every trip!
🐰✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ccc2ed and 2424147.

📒 Files selected for processing (1)
  • frontend/src/components/Avatar/index.css.ts (2 hunks)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (7)
frontend/src/components/Avatar/AvatarItem.tsx (1)

4-8: Props interface looks good, but consider adding optional onError handler.

The interface is well-defined with required props. Consider adding an optional onError handler for image load failures.

 interface AvatarItemProps {
   src: string;
   alt: string;
   size: Size;
+  onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;
 }
frontend/src/components/Avatar/Avatar.stories.tsx (2)

5-21: Enhance Storybook configuration with documentation and controls.

Consider adding:

  1. Component description and usage examples
  2. Documentation for each prop
  3. Control for imageUrls with realistic placeholder images
 const meta: Meta = {
   title: 'Components/Avatar',
   component: Avatar,
+  parameters: {
+    docs: {
+      description: {
+        component: 'Avatar component displays a group of user avatars with overflow count.',
+      },
+    },
+    backgrounds: {
+      default: 'dark',
+    },
+  },
   argTypes: {
     size: {
+      description: 'Size of the avatar (sm: 24px, lg: 32px)',
       control: { type: 'radio', options: ['sm', 'lg'] },
     },
     imageUrls: {
-      control: false,
+      description: 'Array of image URLs to display',
+      control: { type: 'array' },
     },
   },
 } satisfies Meta<typeof Avatar>;

27-32: Add realistic placeholder images and more story variants.

The current story uses dummy values. Consider:

  1. Using realistic placeholder images
  2. Adding more variants (e.g., different sizes, overflow cases)
 export const Default: Story = {
   args: {
     size: 'sm',
-    imageUrls: ['1', '2', '3', '4', '5', '6'],
+    imageUrls: [
+      'https://i.pravatar.cc/150?img=1',
+      'https://i.pravatar.cc/150?img=2',
+      'https://i.pravatar.cc/150?img=3',
+      'https://i.pravatar.cc/150?img=4',
+      'https://i.pravatar.cc/150?img=5',
+      'https://i.pravatar.cc/150?img=6',
+    ],
   },
 };
+
+export const Large: Story = {
+  args: {
+    size: 'lg',
+    imageUrls: [
+      'https://i.pravatar.cc/150?img=1',
+      'https://i.pravatar.cc/150?img=2',
+    ],
+  },
+};
frontend/src/components/Avatar/AvatarCount.tsx (1)

24-25: Consider memoizing getTypo function.

Since getTypo is a pure function, it could be memoized for performance optimization.

-const getTypo = (size: Size): Typo => 
-  size === 'sm' ? 'caption' : 't3';
+const getTypo = React.useMemo(
+  (size: Size): Typo => size === 'sm' ? 'caption' : 't3',
+  []
+);
frontend/src/components/Avatar/index.tsx (1)

14-19: Optimize performance with useMemo for limitedUrls.

The URL limitation logic could be memoized to prevent unnecessary recalculations.

 const Avatar = ({ size, imageUrls: prevUrls }: AvatarProps) => {
-  let limitedUrls = prevUrls;
-  if (prevUrls.length > MAX_IMAGE_COUNT) {
-    limitedUrls = prevUrls.slice(0, MAX_IMAGE_COUNT);
-  }
+  const limitedUrls = React.useMemo(
+    () => prevUrls.length > MAX_IMAGE_COUNT 
+      ? prevUrls.slice(0, MAX_IMAGE_COUNT)
+      : prevUrls,
+    [prevUrls]
+  );
frontend/src/components/Avatar/index.css.ts (2)

6-9: Consider adding gap control for better spacing flexibility.

While the current flex setup works, consider adding a gap property or allowing for customizable spacing between items when they're not overlapping.

 export const avatarContainerStyle = style({
   display: 'flex',
   alignItems: 'center',
+  gap: vars.space[2], // Add default gap
 });

11-44: Consider enhancing the avatar style recipe.

A few suggestions to improve the recipe:

  1. Move hard-coded values to theme variables for better consistency
  2. Add a medium size variant for more flexibility
  3. Use theme variables for border width
 export const avatarItemStyle = recipe({
   base: {
     backgroundColor: vars.color.Ref.Neutral['White'],
     borderRadius: vars.radius['Max'],
-    border: `2px solid ${vars.color.Ref.Netural['White']}`,
+    border: `${vars.borderWidth.md} solid ${vars.color.Ref.Neutral['White']}`,
     selectors: {
       '&:last-child': {
-        borderColor: vars.color.Ref.Netural[100],
+        borderColor: vars.color.Ref.Neutral[100],
       },
     },
   },
   variants: {
     size: {
       sm: { 
-        width: '28px',
-        height: '28px',
+        width: vars.space[7],  // Assuming theme has appropriate spacing scale
+        height: vars.space[7],
         selectors: {
           '&:not(:first-child)': {
-            marginLeft: '-12px',
+            marginLeft: `calc(-1 * ${vars.space[3]})`,
           },
         },
       },
+      md: {
+        width: vars.space[8],
+        height: vars.space[8],
+        selectors: {
+          '&:not(:first-child)': {
+            marginLeft: `calc(-1 * ${vars.space[4]})`,
+          },
+        },
+      },
       lg: { 
-        width: '42px', 
-        height: '42px',
+        width: vars.space[10],
+        height: vars.space[10],
         selectors: {
           '&:not(:first-child)': {
-            marginLeft: '-14px',
+            marginLeft: `calc(-1 * ${vars.space[4]})`,
           },
         },
       },
     },
   },
 });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6d682 and 2d977d3.

📒 Files selected for processing (5)
  • frontend/src/components/Avatar/Avatar.stories.tsx (1 hunks)
  • frontend/src/components/Avatar/AvatarCount.tsx (1 hunks)
  • frontend/src/components/Avatar/AvatarItem.tsx (1 hunks)
  • frontend/src/components/Avatar/index.css.ts (1 hunks)
  • frontend/src/components/Avatar/index.tsx (1 hunks)
🔇 Additional comments (1)
frontend/src/components/Avatar/index.css.ts (1)

1-4: Well-structured imports using vanilla-extract!

Good choice using vanilla-extract for type-safe CSS and importing theme variables for consistent styling.

Comment on lines 10 to 16
const AvatarItem = ({ src, size, alt }: AvatarItemProps) => (
<img
alt={alt}
className={avatarItemStyle({ size })}
src={src}
/>
);
Copy link

@coderabbitai coderabbitai bot Feb 6, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add image loading optimization and error handling.

Consider the following improvements:

  1. Add loading="lazy" for performance
  2. Add onError handler for fallback behavior
  3. Ensure meaningful alt text for accessibility
 const AvatarItem = ({ src, size, alt }: AvatarItemProps) => (
   <img
-    alt={alt}
+    alt={alt || `Avatar image ${src}`}
     className={avatarItemStyle({ size })}
+    loading="lazy"
+    onError={onError}
     src={src}
   />
 );

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

Choose a reason for hiding this comment

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

요거 조은 것 같습니닷

Copy link

Choose a reason for hiding this comment

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

@hamo-o Thanks for confirming! I'm glad you find the suggested improvements helpful. Let me know if you need any assistance implementing these changes.

(\ (
(^.^)
c(")(")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

적용 완료했습니다 df9083f

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 20 to 37
return (
<div className={avatarContainerStyle}>
{limitedUrls.map((url, index) => (
<AvatarItem
alt=''
key={`${index}-${url}`}
size={size}
src={url}
/>
))}
{prevUrls.length > MAX_IMAGE_COUNT && (
<AvatarCount
count={prevUrls.length}
size={size}
/>
)}
</div>
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve accessibility and key prop.

Add aria-label to the container and use more reliable key prop.

   return (
-    <div className={avatarContainerStyle}>
+    <div 
+      aria-label={`Avatar group with ${prevUrls.length} images`}
+      className={avatarContainerStyle}
+      role="group"
+    >
       {limitedUrls.map((url, index) => (
         <AvatarItem
           alt=''
-          key={`${index}-${url}`}
+          key={url}
           size={size}
           src={url}
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div className={avatarContainerStyle}>
{limitedUrls.map((url, index) => (
<AvatarItem
alt=''
key={`${index}-${url}`}
size={size}
src={url}
/>
))}
{prevUrls.length > MAX_IMAGE_COUNT && (
<AvatarCount
count={prevUrls.length}
size={size}
/>
)}
</div>
);
return (
<div
aria-label={`Avatar group with ${prevUrls.length} images`}
className={avatarContainerStyle}
role="group"
>
{limitedUrls.map((url, index) => (
<AvatarItem
alt=''
key={url}
size={size}
src={url}
/>
))}
{prevUrls.length > MAX_IMAGE_COUNT && (
<AvatarCount
count={prevUrls.length}
size={size}
/>
)}
</div>
);

Comment on lines +7 to +10
interface AvatarProps {
size: Size;
imageUrls: string[];
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding prop validation for imageUrls.

Add validation to ensure imageUrls is not empty and contains valid URLs.

 interface AvatarProps {
   size: Size;
-  imageUrls: string[];
+  imageUrls: string[] & { length: number };
 }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@hamo-o hamo-o left a comment

Choose a reason for hiding this comment

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

수고하셨습니당

export const Default: Story = {
args: {
size: 'sm',
imageUrls: ['1', '2', '3', '4', '5', '6'],
Copy link
Contributor

Choose a reason for hiding this comment

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

p3;
https://picsum.photos/
요런 mock url이라도 넣어주시면 확인이 편할 것 같습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 적용해보겠습니다~

Copy link
Contributor Author

Choose a reason for hiding this comment

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

적용 완료했습니다 df9083f

Comment on lines 15 to 18
let limitedUrls = prevUrls;
if (prevUrls.length > MAX_IMAGE_COUNT) {
limitedUrls = prevUrls.slice(0, MAX_IMAGE_COUNT);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let limitedUrls = prevUrls;
if (prevUrls.length > MAX_IMAGE_COUNT) {
limitedUrls = prevUrls.slice(0, MAX_IMAGE_COUNT);
}
const ENTIRE_LENGTH = imageUrls.length;
const limitedUrls = imageUrls.slice(0, MAX_IMAGE_COUNT);

p2;
불필요한 let 사용인 것 같습니다. 위와 같이 수정하면 어떨까요? 배열보다 큰 범위여도 원본 배열의 내용이 리턴될 것 같아요.
이후에 리턴문의 조건문에서는 ENTIRE_LENGTH > MAX_IMAGE_COUNT으로 쓸 수도 있고요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

더 깔끔하네요 적용해보겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 완료했습니다 8ccc2ed

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/src/components/Avatar/AvatarItem.tsx (1)

15-15: Consider making the fallback image URL configurable.

The hardcoded fallback URL could be:

  1. Moved to a constants file or configuration
  2. Made more reliable by using a local asset or a stable external URL
  3. Passed as a prop to allow customization

Example implementation:

-  const fallbackSrc = 'https://picsum.photos/id/200/200/200';
+  const fallbackSrc = props.fallbackSrc || DEFAULT_AVATAR_FALLBACK;

And in a constants file:

export const DEFAULT_AVATAR_FALLBACK = '/assets/default-avatar.png';
frontend/src/components/Avatar/Avatar.stories.tsx (2)

5-21: Consider enhancing the component documentation and size options.

The meta configuration is well-structured, but could benefit from:

  1. Adding a description field to document the component's purpose and usage
  2. Consider adding a 'md' (medium) size option for more flexibility
 const meta: Meta = {
   title: 'Components/Avatar',
   component: Avatar,
+  parameters: {
+    docs: {
+      description: {
+        component: 'Avatar component displays user profile images with overflow handling.',
+      },
+    },
+    backgrounds: {
       default: 'dark',
     },
   },
   argTypes: {
     size: {
-      control: { type: 'radio', options: ['sm', 'lg'] },
+      control: { type: 'radio', options: ['sm', 'md', 'lg'] },
     },
     imageUrls: {
       control: false,
     },
   },
 } satisfies Meta<typeof Avatar>;

41-51: Improve error state testing.

The current approach using hi.com is not ideal for testing fetch errors. Consider using:

  1. A real but non-existent image URL
  2. Different error scenarios (404, 500, etc.)
 export const FetchError: Story = {
   args: {
     size: 'sm',
     imageUrls: [
-      'https://hi.com/',
-      'https://hi.com/',
-      'https://hi.com/',
-      'https://hi.com/',
+      'https://picsum.photos/404', // 404 Not Found
+      'https://picsum.photos/500', // 500 Server Error
+      'https://picsum.photos/invalid.jpg', // Invalid image
+      'https://picsum.photos/broken.jpg', // Another invalid image
     ],
   },
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d977d3 and df9083f.

📒 Files selected for processing (2)
  • frontend/src/components/Avatar/Avatar.stories.tsx (1 hunks)
  • frontend/src/components/Avatar/AvatarItem.tsx (1 hunks)
🔇 Additional comments (5)
frontend/src/components/Avatar/AvatarItem.tsx (3)

1-4: LGTM! Imports are well-organized.

The imports are correctly structured with React hooks and local imports properly separated.


6-10: LGTM! Props interface is well-defined.

The interface is clean, properly typed, and follows TypeScript best practices.


24-30: Great job implementing the previous review suggestions!

The component now includes all the recommended optimizations:

  • Lazy loading for performance
  • Error handling with fallback
  • Meaningful alt text
frontend/src/components/Avatar/Avatar.stories.tsx (2)

25-25: LGTM!

The Story type definition is correct and follows Storybook best practices.


27-39: Well implemented with mock image URLs!

Good job implementing the suggestion to use picsum.photos with different IDs for unique mock images. This helps in testing both the regular display and overflow scenarios.

Copy link
Contributor

@hamo-o hamo-o left a comment

Choose a reason for hiding this comment

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

LGTM 🚀 conflict만 해결해주세요 ~~_

@dioo1461 dioo1461 merged commit 2d09b1d into dev Feb 7, 2025
1 check was pending
@dioo1461 dioo1461 deleted the feature/fe/avatar-component branch February 7, 2025 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🖥️ FE Frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants