Skip to content

Conversation

@areumH
Copy link
Collaborator

@areumH areumH commented Feb 24, 2025

💡 작업 내용

  • Button 컴포넌트에 outlineHighlight 스타일 추가
  • SummaryItem, SummaryBox 모바일 컴포넌트 구현
  • VoteToggle 컴포넌트 구현
  • ProfileIcon 컴포넌트에 extraSmall 사이즈 추가
  • TalkPickSection 모바일 컴포넌트 구현 및 데이터 연결
  • ReportModal 모바일 컴포넌트 구현

💡 자세한 설명

✅ ProfileIcon

  • 톡픽 상세 조회 모바일 컴포넌트에 작성자 프로필 아이콘이 추가되었기 때문에 기존의 ProfileIcon 컴포넌트에 extraSmall 사이즈를 추가했습니다!
  • 모바일의 ProfileIcon 컴포넌트는 원형이 아닌 사각형 모양이기 때문에 웹 컴포넌트에 사이즈를 추가했습니다!

✅ SummaryBox

  • 세줄 요약 컴포넌트에서의 svg 아이콘의 경우 웹에서와 디자인이 동일하기 때문에 새로 선언하지 않고 웹에서 사용된 아이콘에 크기 스타일을 넣어 구현했습니다.

✅ TalkPickSection

  • TalkPickDetail 타입에 writerProfileImgUrl 값을 추가해두었습니다.
  • 신고 버튼 클릭 시 신고 모달이 뜨도록 연결해두었습니다. 스토리북으로 확인 부탁드립니다!!

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

🚩 후속 작업 (선택)

✅ 셀프 체크리스트

  • PR 제목을 형식에 맞게 작성했나요?
  • 브랜치 전략에 맞는 브랜치에 PR을 올리고 있나요? (master/main이 아닙니다.)
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 작업 도중 문서 수정이 필요한 경우 잘 수정했나요?
  • 테스트는 잘 통과했나요?
  • 불필요한 코드는 제거했나요?

closes #301

Summary by CodeRabbit

  • New Features

    • 두 개의 새로운 아이콘 추가로 다양한 그래픽 자산을 제공합니다.
    • 프로필 아이콘에 extraSmall 사이즈 옵션이 도입되어 다양한 크기 표현이 가능해졌습니다.
    • 버튼에 두 가지 새로운 아웃라인 강조 스타일(빨강/파랑)을 추가하여 선택 옵션을 확장했습니다.
    • 요약 항목, 리포트 모달, 투표 토글, 대화픽 섹션 등의 신규 UI 컴포넌트가 도입되었습니다.
    • TalkPickDetail 타입에 작성자 프로필 이미지 URL 속성이 추가되었습니다.
    • 새로운 사용자 프롬프트 상수(PROMPT)가 추가되어 사용자 상호작용을 개선했습니다.
  • Style Improvements

    • 모바일 타이포그래피와 컬러 팔레트 업데이트로 일관되고 세련된 UI 경험을 제공합니다.
    • 버튼, 모달, 요약 영역 등 전반적인 스타일이 개선되었습니다.

@areumH areumH added ✔︎pull requests pull requests 코드 체크 요청 👩🏻‍💻 frontend 프론트엔드 작업 🎨 markup ✅feature labels Feb 24, 2025
@areumH areumH requested review from WonJuneKim and alwubin February 24, 2025 17:22
@areumH areumH self-assigned this Feb 24, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2025

Walkthrough

이번 PR은 다양한 컴포넌트와 스타일, Storybook 구성을 업데이트합니다. 새로운 SVG 아이콘, ProfileIcon의 리팩토링, Button의 새로운 variant, ReportModal, SummaryBox, VoteToggle, TalkPickSection 등 여러 컴포넌트가 추가되거나 수정되었습니다. 또한, 색상 및 타이포그래피 스타일 변경, TalkPickDetail 타입 확장이 포함되어 있으며, Storybook 문서도 함께 보강되었습니다.

Changes

파일(들) 변경 요약
src/assets/index.ts 새로운 SVG 아이콘(MobileReport, PickIcon)을 export 추가
src/components/atoms/ProfileIcon/ProfileIcon.* profileWrapper 리팩토링 및 getProfileSize 함수 추가, size prop에 extraSmall 옵션 적용
src/components/mobile/atoms/Button/Button.* 새로운 버튼 variant (outlineHighlightR, outlineHighlightB) 추가 및 스타일 수정 (whiteSpace 변경 등)
src/components/mobile/atoms/SummaryItem/SummaryItem.* 새 SummaryItem 컴포넌트 및 스타일 정의 추가
src/components/mobile/molecules/ReportModal/ReportModal.* ReportModal 컴포넌트 및 관련 스타일 추가
src/components/mobile/molecules/SummaryBox/SummaryBox.* SummaryBox 컴포넌트 및 스타일 정의 추가
src/components/mobile/molecules/VoteToggle/VoteToggle.* VoteToggle 컴포넌트 및 스타일, 투표 로직(로그인/비로그인 처리) 추가
src/components/mobile/organisms/TalkPickSection/TalkPickSection.*
src/components/organisms/TalkPickSection/TalkPickSection.tsx
TalkPickSection 컴포넌트 추가 및 스타일 정의, 기본 talkPickId 값 수정
src/stories/** Storybook 구성 업데이트: ProfileIcon extraSmall 옵션, Button 신규 variant, SummaryItem, ReportModal, SummaryBox, VoteToggle, TalkPickSection 스토리 추가/수정 및 SummaryBox, TalkPickSection의 상태 프로퍼티 보강
src/styles/color.ts
src/styles/typo.ts
색상 팔레트에 새로운 색상 추가 및 Mobile 타이포그래피 스타일(Medium_14, Regular_12, Regular_10) 추가
src/types/talk-pick.ts TalkPickDetail 타입에 writerProfileImgUrl 속성 추가

Suggested reviewers

  • WonJuneKim

Poem

나는 깡총깡총 뛰노는 토끼,
코드 숲 속을 유유히 달리네,
새 아이콘과 버튼들이 반짝이면,
리포트와 투표 모달도 춤추고,
스타일과 타입이 한 데 어우러져,
나는 기쁜 마음으로 깡총!
변경의 축복을 노래하노라 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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.
  • @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
Contributor

@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: 11

🔭 Outside diff range comments (3)
src/components/organisms/TalkPickSection/TalkPickSection.tsx (3)

58-63: 🛠️ Refactor suggestion

console.log 대신 적절한 에러 처리 필요

개발용 console.log 문을 제거하고 적절한 에러 처리를 추가하는 것이 좋겠습니다.

-      .then(() => {
-        console.log('톡픽 링크 복사 완료!');
-      })
-      .catch((err) => {
-        console.log(err);
-      });
+      .then(() => {
+        showToastModal(SUCCESS.COPY.LINK);
+      })
+      .catch(() => {
+        showToastModal(ERROR.COPY.LINK);
+      });

159-163: ⚠️ Potential issue

ReportModal의 onConfirm 핸들러 구현 필요

ReportModal의 onConfirm 핸들러가 비어있습니다. 신고 기능 구현이 필요합니다.

        <ReportModal
          isOpen={activeModal === 'reportTalkPick'}
-         onConfirm={() => {}}
+         onConfirm={() => {
+           // TODO: Implement report functionality
+           handleReport(talkPick.id);
+           onCloseModal();
+           showToastModal(SUCCESS.REPORT);
+         }}
          onClose={onCloseModal}
        />

203-207: 🛠️ Refactor suggestion

이미지 최적화 필요

이미지 요소에 width, height, loading 속성을 추가하여 성능과 사용자 경험을 개선하는 것이 좋겠습니다.

                <div css={S.talkPickImageWrapper}>
                  {talkPick?.imgUrls.map((url, idx) => (
-                   <img src={url} alt={`image ${idx + 1}`} />
+                   <img
+                     src={url}
+                     alt={`image ${idx + 1}`}
+                     width="100%"
+                     height="auto"
+                     loading="lazy"
+                   />
                  ))}
                </div>
🧹 Nitpick comments (18)
src/stories/organisms/TalkPickSection.stories.tsx (1)

26-26: 다양한 상태에 대한 스토리 추가 필요

summaryStatus가 'SUCCESS'로 하드코딩되어 있습니다. 'PENDING', 'FAIL', 'NOT_REQUIRED' 상태에 대한 스토리도 추가하여 모든 상태를 테스트할 수 있도록 하는 것이 좋겠습니다.

export const Default: Story = {
  play: () => {
    store.dispatch(setToken('accessToken'));
  },
};

+export const PendingSummary: Story = {
+  args: {
+    talkPick: {
+      ...defaultTodayTalkPick,
+      summaryStatus: 'PENDING',
+    },
+  },
+};
+
+export const FailedSummary: Story = {
+  args: {
+    talkPick: {
+      ...defaultTodayTalkPick,
+      summaryStatus: 'FAIL',
+    },
+  },
+};
+
+export const NotRequiredSummary: Story = {
+  args: {
+    talkPick: {
+      ...defaultTodayTalkPick,
+      summaryStatus: 'NOT_REQUIRED',
+    },
+  },
+};
src/components/mobile/atoms/SummaryItem/SummaryItem.tsx (1)

9-14: 컴포넌트 구현이 간단명료합니다만, 접근성 개선이 필요합니다.

시맨틱한 HTML 요소 사용과 ARIA 속성 추가를 고려해보세요.

-  <div css={summaryItemStyling}>
-    <div css={numberItemStyling}>{itemNumber}</div>
+  <article css={summaryItemStyling} role="listitem">
+    <span css={numberItemStyling} aria-label={`항목 ${itemNumber}`}>{itemNumber}</span>
     {children}
-  </div>
+  </article>
src/stories/mobile/molecules/ReportModal.stories.tsx (1)

4-17: Storybook 설정이 기본적으로 잘 되어있습니다.

하지만 다음과 같은 추가 스토리들을 고려해보세요:

  • 닫힌 상태 (isOpen: false)
  • 다양한 신고 사유가 선택된 상태
  • 에러 상태
src/components/mobile/atoms/Button/Button.tsx (1)

7-12: 버튼 variant 이름이 좀 더 의미있게 개선되면 좋겠습니다.

'outlineHighlightR'와 'outlineHighlightB'보다는 의미를 더 잘 전달하는 이름을 제안드립니다:

  • 'outlineHighlightRed' 또는 'outlineError'
  • 'outlineHighlightBlue' 또는 'outlineInfo'

또한 각 variant의 용도에 대한 JSDoc 문서화를 추가하면 좋겠습니다:

 export interface ButtonProps extends ComponentPropsWithRef<'button'> {
   size?: 'large' | 'medium';
+  /** 버튼의 시각적 스타일을 정의합니다.
+   * @property primary - 기본 스타일
+   * @property roundPrimary - 둥근 모서리의 기본 스타일
+   * @property outlineShadow - 그림자가 있는 외곽선 스타일
+   * @property outlineHighlightR - 빨간색 강조 외곽선 스타일 (경고/에러용)
+   * @property outlineHighlightB - 파란색 강조 외곽선 스타일 (정보용)
+   */
   variant?:
     | 'primary'
     | 'roundPrimary'
     | 'outlineShadow'
     | 'outlineHighlightR'
     | 'outlineHighlightB';
src/stories/mobile/atoms/SummaryItem.stories.tsx (1)

31-48: 스토리 예시에 접근성 개선이 필요합니다.

리스트 아이템에 의미있는 텍스트 콘텐츠를 사용하고 있지만, 스크린 리더 사용자를 위한 추가적인 접근성 개선이 필요합니다.

다음과 같이 aria-label을 추가하는 것을 제안합니다:

-    <ul css={storyContainer}>
+    <ul css={storyContainer} aria-label="요약 아이템 예시 목록">
src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (1)

17-60: contentMap 구현에 대한 개선 제안

현재 구현은 잘 작동하지만, 다음과 같은 개선사항을 고려해보세요:

  1. 각 상태별 컴포넌트를 별도 함수로 분리하여 가독성 향상
  2. summary prop이 undefined일 때의 처리 추가

다음과 같은 리팩토링을 제안합니다:

+ const renderPendingContent = () => (
+   <div css={S.summarySpinnerWrapper}>
+     <div css={S.summaryTextStyling}>{SUMMARY.TITLE}</div>
+     <div css={S.summarySpinnerStyling}>
+       <Spinner css={S.spinnerStyling} />
+     </div>
+     <p css={S.summarySpinnerText}>{SUMMARY.PENDING}</p>
+   </div>
+ );

+ const renderSuccessContent = (summary?: TalkPickSummary) => (
+   <div css={S.summaryWrapper}>
+     <SummaryItem itemNumber="1">{summary?.summaryFirstLine || ''}</SummaryItem>
+     <SummaryItem itemNumber="2">{summary?.summarySecondLine || ''}</SummaryItem>
+     <SummaryItem itemNumber="3">{summary?.summaryThirdLine || ''}</SummaryItem>
+   </div>
+ );

  const contentMap = {
-   PENDING: (
-     <div css={S.summarySpinnerWrapper}>
-       ...
-     </div>
-   ),
+   PENDING: renderPendingContent,
-   SUCCESS: (
-     <div css={S.summaryWrapper}>
-       ...
-     </div>
-   ),
+   SUCCESS: () => renderSuccessContent(summary),
    ...
  };
src/stories/molecules/SummaryBox.stories.tsx (1)

37-58: 스토리 구성에 대한 제안

각 상태별 스토리가 잘 구성되어 있지만, 문서화 측면에서 개선이 필요합니다.

각 상태별로 parameters에 docs 설명을 추가하는 것을 제안합니다:

export const All: Story = {
+ parameters: {
+   docs: {
+     description: {
+       story: '`SummaryBox` 컴포넌트의 모든 상태를 보여주는 예시입니다.'
+     }
+   }
+ },
  render: () => (
    ...
  ),
};
src/stories/mobile/molecules/SummaryBox.stories.tsx (1)

19-30: 모바일 특화 문서화 필요

모바일 컴포넌트임을 명확히 하는 문서화가 필요합니다.

다음과 같이 모바일 관련 문서를 추가하는 것을 제안합니다:

const meta = {
  title: 'mobile/molecules/SummaryBox',
  component: SummaryBox,
  parameters: {
    layout: 'centered',
+   docs: {
+     description: {
+       component: '모바일 환경에 최적화된 SummaryBox 컴포넌트입니다. 모바일에서의 터치 영역과 가독성을 고려하여 설계되었습니다.'
+     }
+   }
  },
  ...
};
src/components/mobile/atoms/Button/Button.style.ts (1)

73-88: 코드 중복을 제거하여 유지보수성을 향상시키세요.

outlineHighlightR과 outlineHighlightB 변형에 대한 사이즈 스타일링이 동일합니다. 중복을 제거하기 위해 공통 스타일을 추출하는 것이 좋습니다.

다음과 같이 리팩토링하는 것을 제안합니다:

+const highlightSizeStyling = {
+  large: css({}),
+  medium: css(typo.Comment.SemiBold, {
+    width: '134px',
+    height: '72px',
+    padding: '0 21px',
+  }),
+};

 const style = {
   // ... other variants
   outlineHighlightR: {
-    large: css({}),
-    medium: css(typo.Comment.SemiBold, {
-      width: '134px',
-      height: '72px',
-      padding: '0 21px',
-    }),
+    ...highlightSizeStyling,
   },
   outlineHighlightB: {
-    large: css({}),
-    medium: css(typo.Comment.SemiBold, {
-      width: '134px',
-      height: '72px',
-      padding: '0 21px',
-    }),
+    ...highlightSizeStyling,
   },
 };
src/stories/mobile/molecules/VoteToggle.stories.tsx (1)

58-58: 오타를 수정하세요.

"Defult"를 "Default"로 수정해야 합니다.

-        <h3>Defult</h3>
+        <h3>Default</h3>
src/components/atoms/ProfileIcon/ProfileIcon.style.ts (1)

16-33: 크기 상수를 객체로 분리하는 것을 고려해보세요.

크기 값을 상수 객체로 분리하면 유지보수성이 향상될 것 같습니다.

+const PROFILE_SIZES = {
+  large: '142px',
+  small: '40px',
+  extraSmall: '24px',
+} as const;

 export const getProfileSize = (size: 'extraSmall' | 'small' | 'large') => {
   const style = {
     large: css({
-      width: '142px',
-      height: '142px',
+      width: PROFILE_SIZES.large,
+      height: PROFILE_SIZES.large,
     }),
     small: css({
-      width: '40px',
-      height: '40px',
+      width: PROFILE_SIZES.small,
+      height: PROFILE_SIZES.small,
     }),
     extraSmall: css({
-      width: '24px',
-      height: '24px',
+      width: PROFILE_SIZES.extraSmall,
+      height: PROFILE_SIZES.extraSmall,
     }),
   };

   return style[size];
 };
src/components/mobile/molecules/VoteToggle/VoteToggle.tsx (2)

34-36: 오타를 수정해주세요.

currnetOptioncurrentOption으로 수정해주세요.


58-70: 로컬 스토리지 키를 상수로 분리하는 것이 좋겠습니다.

로컬 스토리지 키가 여러 곳에서 반복되어 사용되고 있습니다. 상수로 분리하면 유지보수가 더 쉬워질 것 같습니다.

+const STORAGE_KEY_PREFIX = 'talkpick_';
+const getStorageKey = (id: number) => `${STORAGE_KEY_PREFIX}${id}`;

 const handleLoggedOutTalkPickVote = (voteOption: VoteOption) => {
   if (loggedOutVoteOption === voteOption) {
     setLoggedOutVoteOption(null);
-    localStorage.removeItem(`talkpick_${talkPickId}`);
+    localStorage.removeItem(getStorageKey(talkPickId));
   } else {
     setLoggedOutVoteOption(voteOption);
-    localStorage.setItem(`talkpick_${talkPickId}`, voteOption);
+    localStorage.setItem(getStorageKey(talkPickId), voteOption);
src/components/mobile/molecules/SummaryBox/SummaryBox.style.ts (2)

29-31: 애니메이션 지속 시간을 상수로 분리하는 것이 좋겠습니다.

애니메이션 지속 시간을 상수로 분리하면 일관성 있는 관리가 가능할 것 같습니다.

+const ANIMATION_DURATION = '2s';

 export const summarySpinnerStyling = css({
-  animation: `${rotate} 2s infinite linear`,
+  animation: `${rotate} ${ANIMATION_DURATION} infinite linear`,
 });

56-63: transform과 margin 값을 상수로 분리하는 것이 좋겠습니다.

반복되는 스타일 값들을 상수로 분리하면 유지보수가 더 쉬워질 것 같습니다.

+const ICON_SCALE = 0.75;
+const SPINNER_MARGIN = '10px';

 export const iconStyling = css({
-  transform: 'scale(0.75)',
+  transform: `scale(${ICON_SCALE})`,
 });

 export const spinnerStyling = css({
-  transform: 'scale(0.75)',
-  margin: '10px',
+  transform: `scale(${ICON_SCALE})`,
+  margin: SPINNER_MARGIN,
 });
src/components/mobile/molecules/ReportModal/ReportModal.style.ts (2)

37-38: 버튼 크기가 모바일 화면에 비해 너무 큽니다.

width: '143px'height: '50px'는 모바일 화면에서 상당히 큰 크기입니다. 모바일 환경에서의 사용성을 고려하여 버튼 크기를 조정하는 것이 좋습니다.

export const buttonStyling = css(typo.Mobile.Text.Medium_14, {
  display: 'flex',
-  width: '143px',
-  height: '50px',
+  width: '120px',
+  height: '44px',
  alignItems: 'center',
  padding: '0 16px',
  borderRadius: '8px',

69-71: 자동 완성 스타일이 불완전합니다.

자동 완성 시 배경색만 설정되어 있고, 텍스트 색상이 설정되어 있지 않아 가독성 문제가 발생할 수 있습니다.

  ':-webkit-autofill': {
    boxShadow: '0 0 0px 1000px white inset',
+    '-webkit-text-fill-color': color.BK,
  },
src/components/mobile/organisms/TalkPickSection/TalkPickSection.style.ts (1)

126-131: 이미지 최적화가 필요합니다.

이미지 스타일링에 최적화 속성이 누락되어 있습니다. 모바일 환경에서의 성능 향상을 위해 이미지 최적화 속성을 추가하는 것이 좋습니다.

  '& > img': {
    maxWidth: '100%',
    width: 'auto',
    height: 'auto',
    borderRadius: '5px',
+    objectFit: 'cover',
+    loading: 'lazy',
  },
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f17e265 and 00705ac.

⛔ Files ignored due to path filters (4)
  • src/assets/svg/angle-small-down.svg is excluded by !**/*.svg
  • src/assets/svg/angle-small-up.svg is excluded by !**/*.svg
  • src/assets/svg/mobile-report.svg is excluded by !**/*.svg
  • src/assets/svg/pick-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (28)
  • src/assets/index.ts (1 hunks)
  • src/components/atoms/ProfileIcon/ProfileIcon.style.ts (1 hunks)
  • src/components/atoms/ProfileIcon/ProfileIcon.tsx (2 hunks)
  • src/components/mobile/atoms/Button/Button.style.ts (3 hunks)
  • src/components/mobile/atoms/Button/Button.tsx (1 hunks)
  • src/components/mobile/atoms/SummaryItem/SummaryItem.style.ts (1 hunks)
  • src/components/mobile/atoms/SummaryItem/SummaryItem.tsx (1 hunks)
  • src/components/mobile/molecules/ReportModal/ReportModal.style.ts (1 hunks)
  • src/components/mobile/molecules/ReportModal/ReportModal.tsx (1 hunks)
  • src/components/mobile/molecules/SummaryBox/SummaryBox.style.ts (1 hunks)
  • src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (1 hunks)
  • src/components/mobile/molecules/VoteToggle/VoteToggle.style.ts (1 hunks)
  • src/components/mobile/molecules/VoteToggle/VoteToggle.tsx (1 hunks)
  • src/components/mobile/organisms/TalkPickSection/TalkPickSection.style.ts (1 hunks)
  • src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1 hunks)
  • src/components/organisms/TalkPickSection/TalkPickSection.tsx (1 hunks)
  • src/stories/atoms/ProfileIcon.stories.tsx (2 hunks)
  • src/stories/mobile/atoms/Button.stories.tsx (2 hunks)
  • src/stories/mobile/atoms/SummaryItem.stories.tsx (1 hunks)
  • src/stories/mobile/molecules/ReportModal.stories.tsx (1 hunks)
  • src/stories/mobile/molecules/SummaryBox.stories.tsx (1 hunks)
  • src/stories/mobile/molecules/VoteToggle.stories.tsx (1 hunks)
  • src/stories/mobile/organisms/TalkPickSection.stories.tsx (1 hunks)
  • src/stories/molecules/SummaryBox.stories.tsx (2 hunks)
  • src/stories/organisms/TalkPickSection.stories.tsx (1 hunks)
  • src/styles/color.ts (1 hunks)
  • src/styles/typo.ts (2 hunks)
  • src/types/talk-pick.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/components/mobile/atoms/SummaryItem/SummaryItem.style.ts
  • src/components/mobile/molecules/VoteToggle/VoteToggle.style.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/mobile/molecules/ReportModal/ReportModal.tsx

[error] 39-50: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx

[error] 212-215: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: storybook
  • GitHub Check: build
🔇 Additional comments (15)
src/types/talk-pick.ts (1)

25-25: LGTM!

writerProfileImgUrl 타입이 적절하게 정의되었습니다.

src/styles/color.ts (1)

10-10: LGTM!

기존 회색 계열 색상 팔레트에 잘 어울리는 색상이 추가되었습니다.

src/components/organisms/TalkPickSection/TalkPickSection.tsx (1)

223-223: LGTM!

talkPickId의 기본값이 0으로 변경된 것은 적절해 보입니다.

src/assets/index.ts (1)

154-155: 새로운 아이콘 추가가 잘 되었습니다!

새로운 모바일 리포트 아이콘과 픽 아이콘이 기존 패턴을 잘 따르고 있습니다.

src/components/mobile/atoms/SummaryItem/SummaryItem.tsx (1)

4-7: 타입 정의가 명확합니다.

itemNumber의 타입을 리터럴 유니온으로 제한한 것이 좋습니다.

src/styles/typo.ts (1)

238-244: 새로운 모바일 타이포그래피 스타일이 잘 구현되었습니다!

새로 추가된 Medium_14, Regular_12, Regular_10 스타일이 기존 타이포그래피 시스템의 일관성을 잘 유지하고 있습니다.

Also applies to: 266-279

src/stories/mobile/atoms/Button.stories.tsx (1)

18-24: 버튼 컴포넌트의 새로운 variant가 잘 구현되었습니다!

outlineHighlightR과 outlineHighlightB variant가 스토리북에 적절히 추가되어 있으며, 각각의 사용 예시가 명확하게 표현되어 있습니다.

Also applies to: 66-73

src/stories/atoms/ProfileIcon.stories.tsx (1)

21-21: ProfileIcon 컴포넌트의 extraSmall 사이즈가 잘 구현되었습니다!

새로운 extraSmall 사이즈 옵션이 기본 케이스와 커스텀 이미지 케이스 모두에 대해 적절히 구현되어 있습니다.

Also applies to: 40-42, 47-52

src/components/atoms/ProfileIcon/ProfileIcon.tsx (1)

12-12: ProfileIcon 컴포넌트의 타입과 스타일 적용 방식이 개선되었습니다!

다음과 같은 개선사항이 잘 구현되어 있습니다:

  • size 타입에 extraSmall 옵션 추가
  • CSS 스타일 적용 방식을 배열 구문으로 개선
  • 명시적인 button type 지정

Also applies to: 18-18, 32-37, 42-47

src/stories/mobile/atoms/SummaryItem.stories.tsx (1)

13-24: argTypes와 args 설정이 잘 구성되어 있습니다!

컴포넌트의 props가 명확하게 정의되어 있고, 적절한 컨트롤 타입이 설정되어 있습니다.

src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (1)

8-11: Props 타입 정의가 명확합니다.

인터페이스가 잘 정의되어 있으며, 적절한 옵셔널 프로퍼티를 사용하고 있습니다.

src/stories/molecules/SummaryBox.stories.tsx (1)

7-17: 예시 데이터가 잘 구성되어 있습니다.

테스트용 데이터가 실제 사용 사례를 잘 반영하고 있습니다.

src/stories/mobile/molecules/SummaryBox.stories.tsx (1)

7-17: 모바일용 예시 데이터가 적절합니다.

실제 모바일 환경에서 발생할 수 있는 데이터를 잘 반영하고 있습니다.

src/components/atoms/ProfileIcon/ProfileIcon.style.ts (1)

3-14: 스타일 분리가 잘 되어있습니다!

기본 스타일과 크기 관련 스타일을 분리한 것이 좋은 접근입니다.

src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1)

143-147: ReportModal의 onConfirm 핸들러가 비어있습니다.

신고 모달의 확인 버튼 핸들러가 구현되지 않았습니다. 신고 처리 로직을 추가해주세요.


export const Default: Story = {
play: () => {
store.dispatch(setToken('accessToken'));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

민감한 정보 노출에 주의하세요.

스토리북 파일에 하드코딩된 액세스 토큰은 보안상 위험할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

-    store.dispatch(setToken('accessToken'));
+    store.dispatch(setToken('dummy_token_for_storybook'));
📝 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
store.dispatch(setToken('accessToken'));
store.dispatch(setToken('dummy_token_for_storybook'));

Comment on lines +54 to +60
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 필드에 대한 유효성 검사를 추가하세요.

기타 사유 입력 시 최소/최대 길이 제한과 같은 유효성 검사가 필요합니다.

 <input
   css={S.reportInputStyling}
   placeholder="신고사유를 작성해주세요."
+  minLength={10}
+  maxLength={200}
   onChange={handleOtherReportReason}
+  required
 />
📝 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
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
)}
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
minLength={10}
maxLength={200}
onChange={handleOtherReportReason}
required
/>
)}

Comment on lines +55 to +59
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성을 개선하세요.

입력 필드에 적절한 레이블과 ARIA 속성이 필요합니다.

+<label htmlFor="otherReason" css={S.srOnly}>
+  기타 신고 사유
+</label>
 <input
+  id="otherReason"
   css={S.reportInputStyling}
   placeholder="신고사유를 작성해주세요."
+  aria-label="기타 신고 사유"
   onChange={handleOtherReportReason}
 />
📝 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
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
<label htmlFor="otherReason" css={S.srOnly}>
기타 신고 사유
</label>
<input
id="otherReason"
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
aria-label="기타 신고 사유"
onChange={handleOtherReportReason}
/>

Comment on lines 37 to 52
{reportOptions.map((option) => (
<button
type="button"
value={option.value}
onClick={() => {
setReportReason(option.value);
setOtherReason('');
}}
css={[
S.buttonStyling,
option.value === reportReason && S.selectedButtonStyling,
]}
>
{option.label}
</button>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

map 함수 사용 시 key prop을 추가하세요.

리스트 렌더링 시 각 요소에 고유한 key prop이 필요합니다.

 {reportOptions.map((option) => (
   <button
+    key={option.value}
     type="button"
     value={option.value}
📝 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
{reportOptions.map((option) => (
<button
type="button"
value={option.value}
onClick={() => {
setReportReason(option.value);
setOtherReason('');
}}
css={[
S.buttonStyling,
option.value === reportReason && S.selectedButtonStyling,
]}
>
{option.label}
</button>
))}
{reportOptions.map((option) => (
<button
key={option.value}
type="button"
value={option.value}
onClick={() => {
setReportReason(option.value);
setOtherReason('');
}}
css={[
S.buttonStyling,
option.value === reportReason && S.selectedButtonStyling,
]}
>
{option.label}
</button>
))}
🧰 Tools
🪛 Biome (1.9.4)

[error] 39-50: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +72 to +83
const handleTalkPickVote = (
selectedOption: MyVoteOption,
voteOption: VoteOption,
) => {
if (selectedOption === null) {
createTalkPickVote(voteOption);
} else if (selectedOption === voteOption) {
deleteTalkPickVote();
} else {
editTalkPickVote(voteOption);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리를 추가하는 것이 좋겠습니다.

mutation 함수 호출 시 에러 처리가 없습니다. try-catch 블록을 추가하여 에러 상황을 처리하는 것이 좋겠습니다.

 const handleTalkPickVote = (
   selectedOption: MyVoteOption,
   voteOption: VoteOption,
 ) => {
+  try {
     if (selectedOption === null) {
       createTalkPickVote(voteOption);
     } else if (selectedOption === voteOption) {
       deleteTalkPickVote();
     } else {
       editTalkPickVote(voteOption);
     }
+  } catch (error) {
+    showToastModal('투표 처리 중 오류가 발생했습니다.');
+  }
 };
📝 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
const handleTalkPickVote = (
selectedOption: MyVoteOption,
voteOption: VoteOption,
) => {
if (selectedOption === null) {
createTalkPickVote(voteOption);
} else if (selectedOption === voteOption) {
deleteTalkPickVote();
} else {
editTalkPickVote(voteOption);
}
};
const handleTalkPickVote = (
selectedOption: MyVoteOption,
voteOption: VoteOption,
) => {
try {
if (selectedOption === null) {
createTalkPickVote(voteOption);
} else if (selectedOption === voteOption) {
deleteTalkPickVote();
} else {
editTalkPickVote(voteOption);
}
} catch (error) {
showToastModal('투표 처리 중 오류가 발생했습니다.');
}
};

Comment on lines +107 to +114
const { mutate: deleteTalkPick } = useDeleteTalkPickMutation(
talkPick?.id ?? 0,
);

const handleDeleteButton = () => {
deleteTalkPick();
onCloseModal();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 작업 후 피드백이 필요합니다.

삭제 작업 완료 후 사용자에게 피드백을 제공하고 적절한 페이지로 리다이렉션하는 것이 좋겠습니다.

 const handleDeleteButton = () => {
-  deleteTalkPick();
+  deleteTalkPick({
+    onSuccess: () => {
+      showToastModal('톡픽이 삭제되었습니다.');
+      navigate('/');
+    },
+    onError: () => {
+      showToastModal('삭제 중 오류가 발생했습니다.');
+    },
+  });
   onCloseModal();
 };
📝 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
const { mutate: deleteTalkPick } = useDeleteTalkPickMutation(
talkPick?.id ?? 0,
);
const handleDeleteButton = () => {
deleteTalkPick();
onCloseModal();
};
const { mutate: deleteTalkPick } = useDeleteTalkPickMutation(
talkPick?.id ?? 0,
);
const handleDeleteButton = () => {
deleteTalkPick({
onSuccess: () => {
showToastModal('톡픽이 삭제되었습니다.');
navigate('/');
},
onError: () => {
showToastModal('삭제 중 오류가 발생했습니다.');
},
});
onCloseModal();
};

Comment on lines +53 to +59
export const getButtonStyling = (selected: boolean) =>
css({
width: '293px',
height: '44px',
borderRadius: '12px',
backgroundColor: selected ? color.MAIN : color.GY[2],
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

선택된 버튼의 너비가 일관성이 없습니다.

getButtonStyling 함수에서 설정된 버튼 너비(293px)가 buttonStyling의 너비(143px)와 크게 다릅니다. 일관된 사용자 경험을 위해 버튼 크기를 통일하는 것이 좋습니다.

export const getButtonStyling = (selected: boolean) =>
  css({
-    width: '293px',
+    width: '120px',
    height: '44px',
    borderRadius: '12px',
    backgroundColor: selected ? color.MAIN : color.GY[2],
  });

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

Comment on lines +32 to +38
export const talkPickWrapper = css({
display: 'flex',
flexDirection: 'column',
width: '330px',
padding: '10px 0',
borderTop: `1px solid${color.GY[4]}`,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

고정된 너비 값이 모바일 반응형에 적합하지 않습니다.

width: '330px'와 같은 고정된 너비는 다양한 모바일 화면 크기에 대응하기 어렵습니다. 상대적인 단위를 사용하는 것이 좋습니다.

export const talkPickWrapper = css({
  display: 'flex',
  flexDirection: 'column',
-  width: '330px',
+  width: '100%',
+  maxWidth: '330px',
  padding: '10px 0',
-  borderTop: `1px solid${color.GY[4]}`,
+  borderTop: `1px solid ${color.GY[4]}`,
});
📝 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
export const talkPickWrapper = css({
display: 'flex',
flexDirection: 'column',
width: '330px',
padding: '10px 0',
borderTop: `1px solid${color.GY[4]}`,
});
export const talkPickWrapper = css({
display: 'flex',
flexDirection: 'column',
width: '100%',
maxWidth: '330px',
padding: '10px 0',
borderTop: `1px solid ${color.GY[4]}`,
});

Comment on lines +84 to +89
export const talkPickTitleStyling = css(typo.Main.SemiBold, {
width: '300px',
wordBreak: 'break-all',
whiteSpace: 'normal',
color: color.BK,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

제목 스타일에 접근성 고려가 필요합니다.

제목의 너비가 고정되어 있어 화면 크기에 따른 유연한 대응이 어렵습니다. 또한, 접근성을 위해 최소 글자 크기를 보장해야 합니다.

export const talkPickTitleStyling = css(typo.Main.SemiBold, {
-  width: '300px',
+  width: '100%',
+  maxWidth: '300px',
+  minWidth: '200px',
  wordBreak: 'break-all',
  whiteSpace: 'normal',
  color: color.BK,
+  fontSize: 'clamp(16px, 4vw, 20px)',
});
📝 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
export const talkPickTitleStyling = css(typo.Main.SemiBold, {
width: '300px',
wordBreak: 'break-all',
whiteSpace: 'normal',
color: color.BK,
});
export const talkPickTitleStyling = css(typo.Main.SemiBold, {
width: '100%',
maxWidth: '300px',
minWidth: '200px',
wordBreak: 'break-all',
whiteSpace: 'normal',
color: color.BK,
fontSize: 'clamp(16px, 4vw, 20px)',
});

Comment on lines +144 to +150
export const toastModalStyling = css({
position: 'fixed',
top: '65px',
left: '50%',
transform: 'translate(-50%)',
zIndex: '1000',
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토스트 모달의 접근성 개선이 필요합니다.

토스트 모달의 위치가 고정되어 있어 다양한 모바일 기기에서 일관된 사용자 경험을 제공하기 어렵습니다. 또한, 접근성을 위한 속성이 누락되어 있습니다.

export const toastModalStyling = css({
  position: 'fixed',
-  top: '65px',
+  top: 'min(65px, 10vh)',
  left: '50%',
  transform: 'translate(-50%)',
  zIndex: '1000',
+  maxWidth: '90vw',
+  role: 'alert',
+  ariaLive: 'polite',
});

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

Copy link
Contributor

@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: 4

♻️ Duplicate comments (2)
src/components/mobile/molecules/ReportModal/ReportModal.tsx (1)

55-61: ⚠️ Potential issue

입력 필드의 유효성 검사와 접근성이 누락되었습니다.

이전 리뷰에서 지적된 유효성 검사와 접근성 문제가 아직 해결되지 않았습니다.

+<label htmlFor="reportReason" css={S.srOnly}>
+  기타 신고 사유
+</label>
 <input
+  id="reportReason"
   css={S.reportInputStyling}
   placeholder="신고사유를 작성해주세요."
+  minLength={10}
+  maxLength={200}
+  aria-label="기타 신고 사유"
+  required
   onChange={handleOtherReportReason}
 />
src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1)

111-114: ⚠️ Potential issue

삭제 작업 후 피드백과 에러 처리가 누락되었습니다.

이전 리뷰에서 지적된 삭제 작업 후 피드백과 에러 처리가 아직 구현되지 않았습니다.

 const handleDeleteButton = () => {
-  deleteTalkPick();
+  deleteTalkPick({
+    onSuccess: () => {
+      showToastModal('톡픽이 삭제되었습니다.');
+      navigate('/');
+    },
+    onError: (error) => {
+      showToastModal('삭제 중 오류가 발생했습니다.');
+      console.error('Delete error:', error);
+    },
+  });
   onCloseModal();
 };
🧹 Nitpick comments (2)
src/components/mobile/molecules/ReportModal/ReportModal.tsx (1)

62-69: 버튼의 비활성화 상태 처리가 필요합니다.

현재는 스타일링으로만 처리되어 있으며, 실제 버튼의 비활성화 상태가 적용되어 있지 않습니다.

 <Button
   size="large"
   variant="primary"
   onClick={handleConfirm}
+  disabled={!finalReportReason.trim()}
   css={S.getButtonStyling(!!finalReportReason)}
 >
   설정 완료
 </Button>
src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1)

177-183: ProfileIcon의 접근성 개선이 필요합니다.

프로필 이미지에 대한 적절한 대체 텍스트가 누락되어 있습니다.

 <ProfileIcon
   interaction={
     talkPick?.writerProfileImgUrl ? 'custom' : 'default'
   }
   imgUrl={talkPick?.writerProfileImgUrl ?? ''}
   size="extraSmall"
+  alt={`${talkPick?.writer}님의 프로필 이미지`}
 />
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 00705ac and b64bfb2.

📒 Files selected for processing (2)
  • src/components/mobile/molecules/ReportModal/ReportModal.tsx (1 hunks)
  • src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build
  • GitHub Check: storybook

Comment on lines +8 to +12
export interface ReportModalProps {
isOpen?: boolean;
onConfirm?: (reason: string) => void;
onClose?: () => void;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Props 타입에 필수값 표시가 필요합니다.

Props 타입에서 선택적 필드(?)를 사용하고 있지만, 모달의 정상적인 작동을 위해서는 이러한 props가 필수적입니다.

 export interface ReportModalProps {
-  isOpen?: boolean;
-  onConfirm?: (reason: string) => void;
-  onClose?: () => void;
+  isOpen: boolean;
+  onConfirm: (reason: string) => void;
+  onClose: () => void;
 }
📝 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
export interface ReportModalProps {
isOpen?: boolean;
onConfirm?: (reason: string) => void;
onClose?: () => void;
}
export interface ReportModalProps {
isOpen: boolean;
onConfirm: (reason: string) => void;
onClose: () => void;
}

Comment on lines +24 to +27
const handleConfirm = () => {
if (!finalReportReason.trim()) return;
onConfirm?.(finalReportReason);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

handleConfirm 함수의 오류 처리가 필요합니다.

빈 문자열 체크만 하고 있으며, 최소/최대 길이 검증이나 사용자 피드백이 없습니다.

 const handleConfirm = () => {
-  if (!finalReportReason.trim()) return;
+  if (!finalReportReason.trim()) {
+    showToastModal('신고 사유를 입력해주세요.');
+    return;
+  }
+  if (reportReason === '기타' && otherReason.length < 10) {
+    showToastModal('신고 사유는 최소 10자 이상 입력해주세요.');
+    return;
+  }
   onConfirm?.(finalReportReason);
 };
📝 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
const handleConfirm = () => {
if (!finalReportReason.trim()) return;
onConfirm?.(finalReportReason);
};
const handleConfirm = () => {
if (!finalReportReason.trim()) {
showToastModal('신고 사유를 입력해주세요.');
return;
}
if (reportReason === '기타' && otherReason.length < 10) {
showToastModal('신고 사유는 최소 10자 이상 입력해주세요.');
return;
}
onConfirm?.(finalReportReason);
};

Comment on lines +64 to +77
const handleBookmarkClick = () => {
if (!talkPick) return;

if (myTalkPick) {
showToastModal(ERROR.BOOKMARK.MY_TALKPICK);
return;
}

if (talkPick.myBookmark) {
deleteBookmark();
} else {
createBookmark();
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

북마크 작업의 에러 처리와 사용자 피드백이 필요합니다.

북마크 생성/삭제 작업에 대한 성공/실패 피드백이 누락되어 있습니다.

 const handleBookmarkClick = () => {
   if (!talkPick) return;

   if (myTalkPick) {
     showToastModal(ERROR.BOOKMARK.MY_TALKPICK);
     return;
   }

   if (talkPick.myBookmark) {
-    deleteBookmark();
+    deleteBookmark({
+      onSuccess: () => showToastModal('북마크가 삭제되었습니다.'),
+      onError: () => showToastModal('북마크 삭제 중 오류가 발생했습니다.'),
+    });
   } else {
-    createBookmark();
+    createBookmark({
+      onSuccess: () => showToastModal('북마크가 추가되었습니다.'),
+      onError: () => showToastModal('북마크 추가 중 오류가 발생했습니다.'),
+    });
   }
 };
📝 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
const handleBookmarkClick = () => {
if (!talkPick) return;
if (myTalkPick) {
showToastModal(ERROR.BOOKMARK.MY_TALKPICK);
return;
}
if (talkPick.myBookmark) {
deleteBookmark();
} else {
createBookmark();
}
};
const handleBookmarkClick = () => {
if (!talkPick) return;
if (myTalkPick) {
showToastModal(ERROR.BOOKMARK.MY_TALKPICK);
return;
}
if (talkPick.myBookmark) {
deleteBookmark({
onSuccess: () => showToastModal('북마크가 삭제되었습니다.'),
onError: () => showToastModal('북마크 삭제 중 오류가 발생했습니다.'),
});
} else {
createBookmark({
onSuccess: () => showToastModal('북마크가 추가되었습니다.'),
onError: () => showToastModal('북마크 추가 중 오류가 발생했습니다.'),
});
}
};

Comment on lines +143 to +147
<ReportModal
isOpen={activeModal === 'reportTalkPick'}
onConfirm={() => {}}
onClose={onCloseModal}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

ReportModal의 onConfirm 핸들러가 비어있습니다.

신고 기능이 실제로 구현되어 있지 않습니다.

 <ReportModal
   isOpen={activeModal === 'reportTalkPick'}
-  onConfirm={() => {}}
+  onConfirm={(reason) => {
+    // TODO: Implement report API call
+    showToastModal('신고가 접수되었습니다.');
+    onCloseModal();
+  }}
   onClose={onCloseModal}
 />
📝 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
<ReportModal
isOpen={activeModal === 'reportTalkPick'}
onConfirm={() => {}}
onClose={onCloseModal}
/>
<ReportModal
isOpen={activeModal === 'reportTalkPick'}
onConfirm={(reason) => {
// TODO: Implement report API call
showToastModal('신고가 접수되었습니다.');
onCloseModal();
}}
onClose={onCloseModal}
/>

Copy link
Collaborator

@WonJuneKim WonJuneKim left a comment

Choose a reason for hiding this comment

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

전반적으로 깔끔하네요! 상세조회 구현 고생많으셨습니다~ 코멘트는 취향에 따라 갈리는 부분들이 있어서 참고만해도 좋아보입니다!

@@ -1,3 +1,3 @@
<svg width="current" height="current" viewBox="0 0 24 25" fill="current" xmlns="http://www.w3.org/2000/svg">
<svg width="100%" height="100%" viewBox="0 0 24 25" fill="current" xmlns="http://www.w3.org/2000/svg">
Copy link
Collaborator

Choose a reason for hiding this comment

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

width 와 height를 100%로 처리하고 해당 svg를 래핑하는 부분에서 크기를 제어하는 군요! 좋습니다~

import { numberItemStyling, summaryItemStyling } from './SummaryItem.style';

export interface SummaryItemProps {
itemNumber?: '1' | '2' | '3';
Copy link
Collaborator

Choose a reason for hiding this comment

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

children을 ReactNode로 열린 구조로 선언했다면, itemNumber의 경우에도 열린 구조로 처리를 하셔도 좋을 거 같아요.

1,2,3 에 한정된 컴포넌트를 구현한다면 지금 방식도 괜찮아보입니다!

}: TalkPickProps) => {
const navigate = useNavigate();

const [isExpanded, setIsExpanded] = useState<boolean>(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [isExpanded, setIsExpanded] = useState(false);

정도로 표기되어도 될 거 같아요~

Copy link
Collaborator

@alwubin alwubin left a comment

Choose a reason for hiding this comment

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

전반적으로 모듈화된 구조로 잘 짜여진 것 같아요👍 제가 남긴 코멘트도 한 번 확인 부탁드립니다. 구현하시느라 수고 많으셨습니다🙇‍♀️👏

const [loggedOutVoteOption, setLoggedOutVoteOption] =
useState<MyVoteOption>(null);

const currnetOption: MyVoteOption = accessToken
Copy link
Collaborator

Choose a reason for hiding this comment

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

변수 오타 수정되면 좋을거 같아요~ currnetOption -> currentOption

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

세상에.. 617d4e8 에서 수정했습니닷
웹 컴포넌트에도 동일한 오타가 있어 같이 수정했습니다!!! 🙌

onClose={onCloseModal}
/>
<TextModal
text="해당 게시글을 삭제하시겠습니까?"
Copy link
Collaborator

Choose a reason for hiding this comment

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

TextModal text들도 상수로 관리하면 좋을거 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

6cddd7b 에서 상수 메세지 추가하여 수정했습니다!!
해당 pr 먼저 머지 후 밸런스 게임 관련 브랜치에도 반영하겠습니당 👍👍

Copy link
Contributor

@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: 2

🔭 Outside diff range comments (1)
src/components/molecules/SummaryBox/SummaryBox.tsx (1)

30-34: 🛠️ Refactor suggestion

summary가 undefined일 때 예외 처리 필요

summary prop이 옵션이므로(8-11줄에서 정의됨) SUCCESS 상태일 때 summary가 undefined일 경우 런타임 에러가 발생할 수 있습니다. 빈 문자열이나 기본값을 표시하는 예외 처리를 추가하는 것이 좋겠습니다.

<div css={S.summaryWrapper}>
-  <SummaryItem itemNumber="1">{summary?.summaryFirstLine}</SummaryItem>
-  <SummaryItem itemNumber="2">{summary?.summarySecondLine}</SummaryItem>
-  <SummaryItem itemNumber="3">{summary?.summaryThirdLine}</SummaryItem>
+  <SummaryItem itemNumber="1">{summary?.summaryFirstLine || ''}</SummaryItem>
+  <SummaryItem itemNumber="2">{summary?.summarySecondLine || ''}</SummaryItem>
+  <SummaryItem itemNumber="3">{summary?.summaryThirdLine || ''}</SummaryItem>
</div>
🧹 Nitpick comments (5)
src/components/molecules/SummaryBox/SummaryBox.tsx (2)

8-11: Props 인터페이스 개선 제안

summaryStatus는 선택적이지만 기본값이 'PENDING'으로 설정되어 있어서 항상 값이 존재합니다. 인터페이스에서는 필수 속성으로 정의하는 것이 더 명확할 수 있습니다.

export interface SummaryBoxProps {
  summary?: TalkPickSummary;
-  summaryStatus?: 'PENDING' | 'SUCCESS' | 'FAIL' | 'NOT_REQUIRED';
+  summaryStatus: 'PENDING' | 'SUCCESS' | 'FAIL' | 'NOT_REQUIRED';
}

17-56: 상수 분리 고려

contentMap은 컴포넌트 내부에 정의되어 있지만, 컴포넌트가 렌더링될 때마다 다시 생성됩니다. 이를 컴포넌트 외부로 분리하고 필요한 데이터만 props로 전달받는 방식으로 리팩토링하면 성능을 개선할 수 있습니다.

src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (2)

1-1: React의 타입 가져오기 스타일이 일관되지 않습니다.

이전 코드에서는 주로 React.ReactNode와 같은 형태로 타입을 사용했는데, 여기서는 ReactNode를 직접 import하고 있습니다. 코드베이스 전체의 일관성을 위해 표기법을 통일하는 것이 좋습니다.

-import React, { ReactNode } from 'react';
+import React from 'react';

그리고 코드 내에서:

-  const contentMap: Record<
-    'PENDING' | 'SUCCESS' | 'FAIL' | 'NOT_REQUIRED',
-    ReactNode
-  > = {
+  const contentMap: Record<
+    'PENDING' | 'SUCCESS' | 'FAIL' | 'NOT_REQUIRED',
+    React.ReactNode
+  > = {

8-11: 타입 정의가 명확하고 잘 구성되어 있습니다.

인터페이스 정의가 간결하고 명확하게 되어 있습니다. 다만, 상태 값들은 enum으로 정의하면 더 안전하게 사용할 수 있습니다.

+export enum SummaryStatus {
+  PENDING = 'PENDING',
+  SUCCESS = 'SUCCESS',
+  FAIL = 'FAIL',
+  NOT_REQUIRED = 'NOT_REQUIRED'
+}

 export interface SummaryBoxProps {
   summary?: TalkPickSummary;
-  summaryStatus?: 'PENDING' | 'SUCCESS' | 'FAIL' | 'NOT_REQUIRED';
+  summaryStatus?: SummaryStatus;
 }
src/components/mobile/molecules/VoteToggle/VoteToggle.tsx (1)

93-121: 투표 버튼 UI 구현이 깔끔합니다.

사용자의 현재 선택 상태에 따라 버튼 스타일을 달리하는 구현이 잘 되어 있습니다.

로딩 상태 표시를 추가하면 사용자 경험이 더 좋아질 것 같습니다:

+  const [isLoading, setIsLoading] = useState(false);

   const { mutate: createTalkPickVote } =
-    useCreateTalkPickVoteMutation(talkPickId);
+    useCreateTalkPickVoteMutation(talkPickId, {
+      onMutate: () => setIsLoading(true),
+      onSettled: () => setIsLoading(false),
+    });

   const { mutate: editTalkPickVote } = useEditTalkPickVoteMutation(talkPickId, {
+    onMutate: () => setIsLoading(true),
+    onSettled: () => setIsLoading(false),
   });

   const { mutate: deleteTalkPickVote } =
-    useDeleteTalkPickVoteMutation(talkPickId);
+    useDeleteTalkPickVoteMutation(talkPickId, {
+      onMutate: () => setIsLoading(true),
+      onSettled: () => setIsLoading(false),
+    });

   // 컴포넌트 반환부에 로딩 상태 추가
   // 버튼 속성에 추가
   disabled={isLoading}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b64bfb2 and 5227975.

📒 Files selected for processing (7)
  • src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (1 hunks)
  • src/components/mobile/molecules/VoteToggle/VoteToggle.tsx (1 hunks)
  • src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1 hunks)
  • src/components/molecules/SummaryBox/SummaryBox.tsx (2 hunks)
  • src/components/molecules/VotePrototype/VotePrototype.tsx (3 hunks)
  • src/components/organisms/TalkPickSection/TalkPickSection.tsx (3 hunks)
  • src/constants/message.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/components/molecules/VotePrototype/VotePrototype.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/organisms/TalkPickSection/TalkPickSection.tsx
🔇 Additional comments (24)
src/constants/message.ts (1)

221-230: 메시지 상수 추가가 적절하게 구현되었습니다.

새로 추가된 PROMPT 상수는 게시글과 게임에 대한 삭제 및 신고 확인 메시지를 포함하고 있으며, 기존의 상수 패턴과 일관성을 유지하고 있습니다. 이를 통해 하드코딩된 문자열을 제거하고 메시지를 중앙화하여 유지보수성이 향상되었습니다.

src/components/molecules/SummaryBox/SummaryBox.tsx (3)

1-1: import 문에 ReactNode를 직접 가져와서 코드를 단순화했네요!

React에서 직접 ReactNode를 import 하는 방식으로 변경하여 코드를 더 깔끔하게 만들었습니다. 이는 좋은 방식입니다.


19-19: 타입 선언 간소화 👍

React.ReactNode 대신 직접 import한 ReactNode를 사용함으로써 타입 선언이 더 간결해졌습니다.


60-65: 전체적으로 잘 구현된 컴포넌트

요약 상자 컴포넌트가 상태에 따라 적절한 내용을 표시하도록 잘 구현되어 있습니다. 레이아웃과 구조가 깔끔하게 정의되어 있습니다.

src/components/mobile/molecules/SummaryBox/SummaryBox.tsx (3)

13-16: 기본값 설정이 적절합니다.

summaryStatus의 기본값을 'PENDING'으로 설정한 것은 좋은 선택입니다. 로딩 상태로 시작하는 것이 사용자 경험 측면에서 자연스럽습니다.


17-60: content 렌더링 방식이 체계적으로 잘 구성되어 있습니다.

객체 맵을 사용하여 각 상태별 렌더링 로직을 분리한 것은 가독성과 유지보수성을 높이는 좋은 방법입니다. 다만, SUCCESS 상태에서 summary가 없는 경우에 대한 방어 코드가 필요합니다.

SUCCESS 상태일 때 summary 속성이 undefined인 경우 어떻게 처리되는지 확인해보세요. 다음과 같이 방어 코드를 추가하는 것이 좋습니다:

 SUCCESS: (
   <div css={S.summaryWrapper}>
-    <SummaryItem itemNumber="1">{summary?.summaryFirstLine}</SummaryItem>
-    <SummaryItem itemNumber="2">{summary?.summarySecondLine}</SummaryItem>
-    <SummaryItem itemNumber="3">{summary?.summaryThirdLine}</SummaryItem>
+    <SummaryItem itemNumber="1">{summary?.summaryFirstLine || '-'}</SummaryItem>
+    <SummaryItem itemNumber="2">{summary?.summarySecondLine || '-'}</SummaryItem>
+    <SummaryItem itemNumber="3">{summary?.summaryThirdLine || '-'}</SummaryItem>
   </div>
 ),

62-64: 심플하고 효율적인 렌더링 방식을 사용했습니다.

미리 계산된 컨텐츠를 변수에 저장하고 이를 렌더링하는 방식은 효율적이고 가독성이 좋습니다.

src/components/mobile/molecules/VoteToggle/VoteToggle.tsx (5)

16-21: Props 인터페이스가 명확하게 정의되어 있습니다.

필요한 props가 타입과 함께 명확히 정의되어 있습니다.


34-36: 변수명 오타를 수정해주세요.

변수명에 오타가 있습니다. currnetOption이 아닌 currentOption으로 수정해야 합니다.


58-70: 비로그인 사용자 투표 처리가 잘 구현되어 있습니다.

사용자 경험을 고려하여 비로그인 상태에서도 투표 기능을 제공하고, 로그인 안내 메시지를 표시하는 방식이 좋습니다.

로컬 스토리지 접근 시에도 예외 처리를 추가하는 것이 좋습니다:

 const handleLoggedOutTalkPickVote = (voteOption: VoteOption) => {
   if (loggedOutVoteOption === voteOption) {
     setLoggedOutVoteOption(null);
-    localStorage.removeItem(`talkpick_${talkPickId}`);
+    try {
+      localStorage.removeItem(`talkpick_${talkPickId}`);
+    } catch (error) {
+      console.error('로컬 스토리지 접근 중 오류 발생:', error);
+    }
   } else {
     setLoggedOutVoteOption(voteOption);
-    localStorage.setItem(`talkpick_${talkPickId}`, voteOption);
+    try {
+      localStorage.setItem(`talkpick_${talkPickId}`, voteOption);
+    } catch (error) {
+      console.error('로컬 스토리지 접근 중 오류 발생:', error);
+    }

     showToastModal(NOTICE.REQUIRED.LOGIN, () => {
       navigate(`/${PATH.LOGIN}`, { state: { talkPickId } });
     });
   }
 };

72-83: 에러 처리를 추가해주세요.

투표 관련 API 호출 시 에러 처리가 누락되어 있습니다. try-catch 블록을 추가하여 오류 상황을 처리해야 합니다.


85-91: 투표 버튼 클릭 핸들러가 잘 구현되어 있습니다.

사용자의 로그인 상태에 따라 적절한 처리 함수를 호출하는 구조가 명확합니다.

src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (12)

32-36: Props 인터페이스가 명확하게 정의되어 있습니다.

필요한 props가 타입과 함께 명확히 정의되어 있습니다.


45-45: boolean 타입 state 선언을 간결하게 작성해주세요.

boolean 타입은 타입 명시를 생략해도 됩니다.


64-77: 북마크 작업의 에러 처리와 사용자 피드백이 필요합니다.

북마크 생성/삭제 작업에 대한 성공/실패 피드백이 누락되어 있습니다.


83-105: 메뉴 아이템 정의가 잘 되어 있습니다.

내 톡픽과 다른 사람의 톡픽에 따라 다른 메뉴 옵션을 제공하는 구현이 좋습니다. 각 메뉴 항목에 대한 동작도 명확히 정의되어 있습니다.


107-114: 삭제 작업 후 피드백이 필요합니다.

삭제 작업 완료 후 사용자에게 피드백을 제공하고 적절한 페이지로 리다이렉션하는 것이 좋겠습니다.


143-147: ReportModal의 onConfirm 핸들러가 비어있습니다.

신고 기능이 실제로 구현되어 있지 않습니다. 신고 처리를 위한 API 호출 또는 임시 처리 로직을 추가해주세요.


167-196: 톡픽 정보 표시가 체계적으로 구성되어 있습니다.

제목, 작성자 정보, 조회수 등이 잘 구조화되어 있으며, 조건부 렌더링을 통해 적절히 처리되고 있습니다.


209-211: 이미지 목록에 key prop이 추가되었습니다.

이전 리뷰에서 지적된 key prop 누락 문제가 해결되었습니다. 좋은 수정입니다.


198-201: SummaryBox 컴포넌트 통합이 잘 되어 있습니다.

새로 구현된 SummaryBox 컴포넌트를 적절히 활용하고 있습니다. 필요한 props가 잘 전달되고 있습니다.


226-233: VoteToggle 컴포넌트 통합이 잘 되어 있습니다.

새로 구현된 VoteToggle 컴포넌트를 적절히 활용하고 있습니다. 필요한 props가 잘 전달되고 있습니다.


116-122: 토스트 모달 위치가 적절합니다.

토스트 메시지가 컴포넌트 최상단에 렌더링되어 사용자에게 잘 보이도록 구현되어 있습니다.


129-147: TextModal 텍스트들은 상수로 관리하면 좋겠습니다.

TextModal에 표시되는 텍스트들도 메시지 상수로 관리하면 더 일관성 있게 관리할 수 있습니다.

Comment on lines +48 to +56
useEffect(() => {
if (!accessToken) {
const savedVote = localStorage.getItem(`talkpick_${talkPickId}`);

if (savedVote === 'A' || savedVote === 'B') {
setLoggedOutVoteOption(savedVote);
}
}
}, [accessToken, talkPickId]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

로컬 스토리지 사용 시 예외 처리가 필요합니다.

로컬 스토리지 접근 시 try-catch 블록으로 예외 처리를 추가하는 것이 좋습니다. 사용자가 개인정보 보호 모드를 사용하거나 로컬 스토리지 접근이 차단된 경우 예외가 발생할 수 있습니다.

 useEffect(() => {
   if (!accessToken) {
-    const savedVote = localStorage.getItem(`talkpick_${talkPickId}`);
-
-    if (savedVote === 'A' || savedVote === 'B') {
-      setLoggedOutVoteOption(savedVote);
+    try {
+      const savedVote = localStorage.getItem(`talkpick_${talkPickId}`);
+
+      if (savedVote === 'A' || savedVote === 'B') {
+        setLoggedOutVoteOption(savedVote);
+      }
+    } catch (error) {
+      console.error('로컬 스토리지 접근 중 오류 발생:', error);
     }
   }
 }, [accessToken, talkPickId]);
📝 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
useEffect(() => {
if (!accessToken) {
const savedVote = localStorage.getItem(`talkpick_${talkPickId}`);
if (savedVote === 'A' || savedVote === 'B') {
setLoggedOutVoteOption(savedVote);
}
}
}, [accessToken, talkPickId]);
useEffect(() => {
if (!accessToken) {
try {
const savedVote = localStorage.getItem(`talkpick_${talkPickId}`);
if (savedVote === 'A' || savedVote === 'B') {
setLoggedOutVoteOption(savedVote);
}
} catch (error) {
console.error('로컬 스토리지 접근 중 오류 발생:', error);
}
}
}, [accessToken, talkPickId]);

@@ -0,0 +1,239 @@
/* eslint-disable jsx-a11y/img-redundant-alt */
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ESLint 비활성화는 지양해주세요.

eslint-disable jsx-a11y/img-redundant-alt 주석이 추가되어 있습니다. 접근성을 고려해 올바른 alt 속성을 제공하는 것이 좋습니다. ESLint 규칙을 비활성화하기보다는 규칙을 준수하는 방향으로 코드를 수정하는 것이 좋습니다.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 4, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
24.7% Duplication on New Code (required ≤ 10%)

See analysis details on SonarQube Cloud

Copy link
Contributor

@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: 2

🧹 Nitpick comments (1)
src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1)

44-44: 상태 선언을 간소화할 수 있습니다.

명시적 타입 지정이 필요하지 않은 간단한 boolean 상태 선언입니다.

-const [isExpanded, setIsExpanded] = useState<boolean>(false);
+const [isExpanded, setIsExpanded] = useState(false);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5227975 and da4b270.

📒 Files selected for processing (2)
  • src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (1 hunks)
  • src/components/organisms/TalkPickSection/TalkPickSection.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/organisms/TalkPickSection/TalkPickSection.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (8)
src/components/mobile/organisms/TalkPickSection/TalkPickSection.tsx (8)

31-35: Props가 명확하게 정의되어 있습니다.

인터페이스가 명확하게 정의되어 있어 이 컴포넌트의 사용 방법이 잘 문서화되어 있습니다.


55-61: API 호출의 오류 처리가 필요합니다.

북마크 생성 및 삭제 기능에 대한 오류 처리 및 성공 피드백이 누락되어 있습니다.

북마크 생성/삭제 작업에 대한 성공/실패 피드백이 누락되어 있습니다. 이전 리뷰에서도 지적된 사항입니다.

 const { mutate: createBookmark } = useCreateTalkPickBookmarkMutation(
   talkPick?.id ?? 0,
 );

 const { mutate: deleteBookmark } = useDeleteTalkPickBookmarkMutation(
   talkPick?.id ?? 0,
 );

63-76: 북마크 작업의 에러 처리와 사용자 피드백이 필요합니다.

북마크 관련 기능이 성공하거나 실패했을 때 사용자에게 적절한 피드백을 제공해야 합니다.

이전 리뷰에서도 지적된 사항입니다.

 const handleBookmarkClick = () => {
   if (!talkPick) return;

   if (myTalkPick) {
     showToastModal(ERROR.BOOKMARK.MY_TALKPICK);
     return;
   }

   if (talkPick.myBookmark) {
-    deleteBookmark();
+    deleteBookmark({
+      onSuccess: () => showToastModal('북마크가 삭제되었습니다.'),
+      onError: () => showToastModal('북마크 삭제 중 오류가 발생했습니다.'),
+    });
   } else {
-    createBookmark();
+    createBookmark({
+      onSuccess: () => showToastModal('북마크가 추가되었습니다.'),
+      onError: () => showToastModal('북마크 추가 중 오류가 발생했습니다.'),
+    });
   }
 };

106-113: 삭제 작업 후 피드백이 필요합니다.

톡픽 삭제 후 사용자에게 피드백을 제공하고 적절한 페이지로 리다이렉션해야 합니다.

이전 리뷰에서도 지적된 사항입니다.

 const { mutate: deleteTalkPick } = useDeleteTalkPickMutation(
   talkPick?.id ?? 0,
 );

 const handleDeleteButton = () => {
-  deleteTalkPick();
+  deleteTalkPick({
+    onSuccess: () => {
+      showToastModal('톡픽이 삭제되었습니다.');
+      navigate('/');
+    },
+    onError: () => {
+      showToastModal('삭제 중 오류가 발생했습니다.');
+    },
+  });
   onCloseModal();
 };

142-146: ReportModal의 onConfirm 핸들러가 비어있습니다.

신고 기능이 실제로 구현되어 있지 않습니다.

이전 리뷰에서도 지적된 사항입니다.

 <ReportModal
   isOpen={activeModal === 'reportTalkPick'}
-  onConfirm={() => {}}
+  onConfirm={(reason) => {
+    // TODO: 신고 API 호출 구현
+    showToastModal('신고가 접수되었습니다.');
+    onCloseModal();
+  }}
   onClose={onCloseModal}
 />

226-231: 투표 컴포넌트가 잘 구현되었습니다.

VoteToggle 컴포넌트에 필요한 속성들이 적절하게 전달되고 있습니다.


197-200: 요약 정보 표시 컴포넌트가 잘 구현되었습니다.

SummaryBox 컴포넌트를 활용하여 톡픽의 요약 정보를 표시하고 있습니다.


1-29: 필요한 모든 임포트가 잘 구성되어 있습니다.

컴포넌트에 필요한 모든 외부 의존성과 내부 컴포넌트, 유틸리티 함수들이 적절하게 임포트되어 있습니다.

Comment on lines +123 to +127
<ShareModal
isOpen={activeModal === 'share'}
onConfirm={() => {}}
onClose={onCloseModal}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

공유 모달의 onConfirm 핸들러가 비어있습니다.

공유 모달의 확인 버튼을 누를 때 실행되는 기능이 구현되어 있지 않습니다.

 <ShareModal
   isOpen={activeModal === 'share'}
-  onConfirm={() => {}}
+  onConfirm={() => {
+    // 공유 기능 구현
+    showToastModal('링크가 복사되었습니다.');
+    onCloseModal();
+  }}
   onClose={onCloseModal}
 />
📝 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
<ShareModal
isOpen={activeModal === 'share'}
onConfirm={() => {}}
onClose={onCloseModal}
/>
<ShareModal
isOpen={activeModal === 'share'}
onConfirm={() => {
// 공유 기능 구현
showToastModal('링크가 복사되었습니다.');
onCloseModal();
}}
onClose={onCloseModal}
/>

Comment on lines +208 to +210
{talkPick?.imgUrls.map((url) => (
<img src={url} key={url} alt={url} />
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지의 alt 속성이 적절하지 않습니다.

이미지의 대체 텍스트로 URL을 사용하는 것은 접근성 측면에서 좋지 않습니다. 이미지의 내용을 설명하는 의미 있는 텍스트를 제공해야 합니다.

-{talkPick?.imgUrls.map((url) => (
-  <img src={url} key={url} alt={url} />
-))}
+{talkPick?.imgUrls.map((url, idx) => (
+  <img src={url} key={url} alt={`톡픽 이미지 ${idx + 1}`} />
+))}
📝 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
{talkPick?.imgUrls.map((url) => (
<img src={url} key={url} alt={url} />
))}
{talkPick?.imgUrls.map((url, idx) => (
<img src={url} key={url} alt={`톡픽 이미지 ${idx + 1}`} />
))}

@alwubin alwubin self-requested a review March 5, 2025 04:43
Copy link
Collaborator

@alwubin alwubin left a comment

Choose a reason for hiding this comment

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

수정 사항 반영된거 모두 확인했습니다~! 수고하셨습니당👏

@areumH areumH merged commit 4329a04 into dev Mar 5, 2025
5 of 7 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in PICK-O Mar 5, 2025
@areumH areumH deleted the feat/301-talkpick-mobile branch March 5, 2025 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✅feature 👩🏻‍💻 frontend 프론트엔드 작업 🎨 markup ✔︎pull requests pull requests 코드 체크 요청

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

톡픽 상세 조회 모바일 컴포넌트 구현

4 participants