Skip to content

Conversation

@maylh
Copy link
Collaborator

@maylh maylh commented Feb 11, 2026

🛰️ 관련 이슈


✨ 주요 변경 사항

1️⃣ 팔로잉/팔로워 목록 페이지 퍼블리싱
2️⃣ feed 관련 페이지 폴더구조 세팅 및 유저아이디 기반 라우팅 처리


🔍 테스트 방법 / 체크리스트

  • 없음

🗯️ PR 포인트

1️⃣ 팔로잉/팔로워 목록 페이지 퍼블리싱 관련

  • ContentHeader 컴포넌트에서 기존에 내부에 고정되어 있던 정렬 옵션을 props로 분리하여 화면별 요구사항에 따라 동적으로 정렬 옵션을 구성할 수 있도록 리팩토링 했습니다

2️⃣ feed 관련 페이지 폴더구조 세팅 및 유저아이디 기반 라우팅 처리

feed
 ┣ followers
 ┃ ┗ index.tsx
 ┣ following
 ┃ ┗ index.tsx
 ┣ ui
 ┣ FeedLayout.tsx
 ┗ FollowLayout.tsx
  • FollowLayout, FollowTab이 팔로우/팔로잉 페이지에서 모두 쓰여서 따로 묶어야 하나 고민하다가 우선 루트에 뒀습니다. 서브 레이아웃이 더 생기면 ui내에 layout 하나 두고 폴더링 해주면 좋을 것 같아요 !
  • 기존 mypage 의 경우는 mypage/ui/(중첩경로들) 이런 식으로 경로들이 ui로 한번 더 묶여있던데 혹시 이거 따로 이유가 있나요 ? 경로가 너무 많아서 묶어주는 용도인건가요 ?

🚀 알게된 점


📖 참고 자료 (선택)

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 팔로우 버튼이 개선되어 다양한 스타일 옵션이 추가되었습니다.
    • 팔로워 및 팔로잉 페이지를 추가했습니다.
    • 콘텐츠 정렬 옵션에 새로운 옵션이 추가되었습니다.
    • 사용자 프로필 피드 라우팅이 추가되었습니다.
  • 리팩토링

    • 콘텐츠 헤더 컴포넌트가 확장되어 동적 정렬 옵션을 지원합니다.

@maylh maylh added this to the 2차 스프린트 개발 milestone Feb 11, 2026
@maylh maylh self-assigned this Feb 11, 2026
@maylh maylh added the MEDIUM 일반적인 중간 우선순위 label Feb 11, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

워크스루

팔로잉/팔로워 목록 페이지 퍼블리싱을 위한 새로운 피드 레이아웃 및 페이지 컴포넌트를 추가하고, FollowButton을 단순화하며, ContentHeader를 확장하여 정렬 옵션과 카운트 타입을 동적으로 지원하고, 새로운 사용자 기반 라우팅을 구성했습니다.

변경 사항

집단 / 파일(s) 요약
아이콘 인덱스
src/assets/icons/index.ts
Add와 Tick 아이콘의 두 개 새로운 명명된 내보내기를 추가했습니다.
FollowButton 리팩토링
src/features/follow/ui/FollowButton.tsx
Props를 단순화하고(playlistId, userName, profile 제거, variant 추가), 바텀시트 플로우를 제거하고 인라인 Button으로 대체했습니다. 로컬 상태 관리 및 Add/Tick 아이콘으로 팔로우 상태를 표시합니다.
피드 레이아웃 컴포넌트
src/pages/feed/FeedLayout.tsx, src/pages/feed/FollowLayout.tsx, src/pages/feed/ui/FollowTab.tsx, src/pages/feed/ui/index.ts
새로운 피드 레이아웃 구조를 구성하는 컴포넌트들을 추가했습니다. FollowLayout은 헤더, 탭, 정렬 제어를 포함하고, FollowTab은 다이나믹 사용자 ID 기반 네비게이션을 제공합니다.
팔로워/팔로잉 페이지
src/pages/feed/followers/index.tsx, src/pages/feed/following/index.tsx
사용자 목록을 표시하는 새로운 페이지 컴포넌트들을 추가했습니다. SearchResultItem과 FollowButton을 사용하여 각 사용자를 렌더링합니다.
ContentHeader 확장
src/shared/ui/ContentHeader.tsx, src/stories/ContentHeader.stories.tsx
SortType에 OLDEST를 추가하고, CountType('NUMBER'
라우팅 구성
src/shared/config/routesConfig.ts
새로운 /:userId 기반 라우트와 하위 /followers, /following 라우트를 추가했습니다. FeedLayout과 FollowLayout을 활용한 중첩 라우팅 구조를 구성했습니다.
ContentHeader 사용처 업데이트
src/pages/mypage/ui/main/components/MyCdList.tsx, src/pages/mypage/ui/main/components/MyLikedCdList.tsx, src/pages/search/SearchResultPage.tsx
ContentHeader에 options prop을 추가하여 정렬 옵션을 명시적으로 전달했습니다.

예상 코드 리뷰 노력

🎯 3 (보통) | ⏱️ ~25분

🐰 팔로우 버튼을 다듬고 새 페이지들을 짓고
레이아웃과 탭으로 구조를 깔고
ContentHeader 옵션을 여러 곳에 펼쳐 놓았네
라우팅 설정도 모두 제대로 연결하여
팔로잉/팔로워 목록이 아름답게 피어났다! 🌸

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 팔로잉/팔로워 목록 페이지 퍼블리싱이라는 주요 변경사항을 명확하게 반영하며, 변경 사항과 직접 관련이 있습니다.
Description check ✅ Passed PR 설명은 관련 이슈, 주요 변경사항, PR 포인트를 포함하지만 테스트 방법은 '없음'으로 표기되어 있고 일부 섹션이 미완성 상태입니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 이슈 #178의 팔로잉/팔로워 목록 페이지 퍼블리싱 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed ContentHeader 옵션 리팩토링, 아이콘 추가, 라우팅 설정은 모두 팔로잉/팔로워 목록 페이지 퍼블리싱을 지원하는 필수 변경사항입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#178/pub-follow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @maylh, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 팔로잉/팔로워 목록 페이지를 퍼블리싱하고, 관련 피드 페이지의 폴더 구조와 사용자 ID 기반 라우팅을 설정합니다. 또한, ContentHeader 컴포넌트를 리팩토링하여 정렬 옵션을 동적으로 구성할 수 있도록 유연성을 높였습니다.

Highlights

  • 팔로잉/팔로워 목록 페이지 퍼블리싱: 팔로잉 및 팔로워 목록을 표시하는 페이지가 구현되었습니다.
  • 피드 관련 페이지 폴더 구조 및 라우팅: 피드 관련 페이지의 폴더 구조가 설정되었고, 사용자 ID 기반의 라우팅이 처리되었습니다.
  • ContentHeader 컴포넌트 리팩토링: ContentHeader 컴포넌트의 정렬 옵션이 props로 분리되어, 화면별 요구사항에 따라 동적으로 정렬 옵션을 구성할 수 있도록 개선되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/assets/icons/icn_add.svg
    • 새 '추가' SVG 아이콘이 추가되었습니다.
  • src/assets/icons/icn_tick.svg
    • 새 '체크' SVG 아이콘이 추가되었습니다.
  • src/assets/icons/index.ts
    • AddTick 아이콘이 내보내기 목록에 추가되었습니다.
  • src/features/follow/ui/FollowButton.tsx
    • FollowButton 컴포넌트가 리팩토링되어 AddTick 아이콘을 사용하고, BottomSheet 로직이 제거되었으며, variant prop을 통해 다양한 스타일을 지원하도록 개선되었습니다.
  • src/pages/feed/FeedLayout.tsx
    • 피드 페이지의 기본 레이아웃을 제공하는 FeedLayout 컴포넌트가 추가되었습니다.
  • src/pages/feed/FollowLayout.tsx
    • 팔로우/팔로잉 페이지를 위한 FollowLayout 컴포넌트가 추가되었으며, 헤더와 FollowTab을 포함합니다.
  • src/pages/feed/followers/index.tsx
    • 팔로워 목록을 표시하는 Followers 페이지가 추가되었습니다.
  • src/pages/feed/following/index.tsx
    • 팔로잉 목록을 표시하는 Following 페이지가 추가되었습니다.
  • src/pages/feed/ui/FollowTab.tsx
    • 팔로우/팔로잉 페이지 간의 내비게이션을 위한 FollowTab 컴포넌트가 추가되었습니다.
  • src/pages/feed/ui/index.ts
    • FollowTab 컴포넌트가 내보내기 목록에 추가되었습니다.
  • src/pages/mypage/ui/main/components/MyCdList.tsx
    • ContentHeader 컴포넌트에 정렬 options prop이 전달되도록 업데이트되었습니다.
  • src/pages/mypage/ui/main/components/MyLikedCdList.tsx
    • ContentHeader 컴포넌트에 정렬 options prop이 전달되도록 업데이트되었습니다.
  • src/pages/search/SearchResultPage.tsx
    • ContentHeader 컴포넌트에 정렬 options prop이 전달되도록 업데이트되었습니다.
  • src/shared/config/routesConfig.ts
    • 피드, 팔로워, 팔로잉 페이지를 위한 새로운 라우트 설정이 추가되었습니다.
  • src/shared/ui/ContentHeader.tsx
    • ContentHeader 컴포넌트가 options prop을 받아 동적으로 정렬 옵션을 구성하고, CountType을 추가하며, 스타일이 업데이트되도록 리팩토링되었습니다.
  • src/stories/ContentHeader.stories.tsx
    • ContentHeader 스토리북이 새로운 props와 사용법을 반영하여 업데이트되었습니다.
Activity
  • 이 풀 리퀘스트는 아직 리뷰 활동이나 추가적인 코멘트가 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

🎵 Storybook Link 🎵
🔗 https://689dbb45f8d09aea7832eeb1-ejmiffmzot.chromatic.com/

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

🤖 Fix all issues with AI agents
In `@src/features/follow/ui/FollowButton.tsx`:
- Line 21: The login check in FollowButton is using useAuthStore() without a
selector so it always returns a truthy store object; update the call to
useAuthStore to select the actual auth flag or user (e.g., useAuthStore(state =>
state.isLoggedIn or state.user)) and rename the variable from isLogin to
something like isLoggedIn; then use that boolean in the FollowButton click
handler (and any checks around openLoginModal or toggleFollow) so
unauthenticated users trigger the login modal instead of running the follow
toggle.

In `@src/pages/feed/FeedLayout.tsx`:
- Around line 3-11: The component declared as UserLayout in FeedLayout.tsx is
misnamed and unnecessarily wrapped in a Fragment; either rename the component to
FeedLayout (change function UserLayout to function FeedLayout) and update the
default export to export default FeedLayout, or rename the file to match
UserLayout; also remove the unnecessary Fragment and return Outlet directly
(e.g., return <Outlet />) inside the FeedLayout/UserLayout function.

In `@src/pages/feed/FollowLayout.tsx`:
- Around line 24-29: The ContentHeader call currently passes a hardcoded
totalCount={2}; replace this placeholder with the real data source (e.g., a prop
or state value such as followersCount, followingCount, or totalCount derived in
FollowLayout) or add a clear TODO comment if left for publishing; update the JSX
where ContentHeader is used (the totalCount prop on ContentHeader) to accept the
dynamic value (or add a TODO above that line referencing the data to be wired)
and ensure the selected and onSelect props remain unchanged.
- Line 20: The center prop currently renders a hardcoded name ("홍길동"); replace
this by reading the route param and/or fetching the real user name: import and
call useParams() in the FollowLayout component to get userId, then either
display userId or call your existing user-fetching function/ hook (e.g.,
fetchUser, useUser or an API client) to resolve and render the real name in the
center prop; if this is intentionally a placeholder, add a TODO comment near
FollowLayout noting it should be replaced with real data before publishing.

In `@src/shared/config/routesConfig.ts`:
- Around line 131-143: 자식 라우트에서 절대 경로('/:userId/followers' 및
'/:userId/following')를 사용하고 있어 부모 경로와 중복되며 매칭 오류를 일으킬 수 있으니, FollowLayout의
children 항목에서 path를 부모에 상대적인 'followers' 및 'following'으로 변경하여 FollowLayout →
Followers/Following 중첩 라우팅이 정상 동작하도록 수정하세요; 대상 식별자: FollowLayout, Followers,
Following, 문제 있는 path 문자열('/:userId/followers', '/:userId/following').

In `@src/stories/ContentHeader.stories.tsx`:
- Line 48: The story example uses countType="number" but the component expects
the CountType union ('NUMBER' | 'PEOPLE') and the actual render uses
countType="NUMBER"; update the example prop to use the uppercase literal
"NUMBER" so the example matches the rendered usage and the CountType type
(change the countType prop in the example in ContentHeader.stories to "NUMBER").
🧹 Nitpick comments (7)
src/features/follow/ui/FollowButton.tsx (1)

13-19: 미사용 userId prop

userId가 props 인터페이스에 선언되어 있지만, 컴포넌트에서 destructure 및 사용되지 않습니다. 현재 불필요하다면 제거하거나, 향후 API 연동 시 필요하다면 TODO 주석을 추가해 주세요.

src/shared/ui/ContentHeader.tsx (1)

58-68: BottomSheet 높이가 200px로 고정

options 배열 길이에 따라 BottomSheet 내용 크기가 달라질 수 있습니다. 옵션이 3개(RECENT, POPULAR, OLDEST)면 180px(60px × 3)이 필요하고, 2개면 120px이면 충분합니다. 현재는 큰 문제가 아니지만, 옵션 수에 따라 높이를 동적으로 계산하는 것을 고려해 볼 수 있습니다.

src/pages/feed/following/index.tsx (2)

9-22: 변수명이 페이지 의미와 불일치

Following 페이지에서 데이터 변수명이 followerList로 되어 있습니다. 팔로잉 목록이므로 followingList가 더 적절합니다.


29-40: SearchResultItem에 중복 key prop

Line 30의 ItemWrapper에 이미 key={item.userId}가 있으므로, Line 32의 SearchResultItem에 있는 key는 불필요합니다.

♻️ 수정 제안
         <ItemWrapper key={item.userId}>
           <SearchResultItem
-            key={item.userId}
             type="USER"
             imageUrl={item.profileUrl}
             searchResult={item.userName}
             onClick={() => navigate(`/${item.userId}`)}
           />
src/pages/feed/followers/index.tsx (2)

29-40: SearchResultItem에 중복 key prop (following 페이지와 동일)

ItemWrapper에 이미 key가 있으므로 SearchResultItemkey는 제거해 주세요.

♻️ 수정 제안
         <ItemWrapper key={item.userId}>
           <SearchResultItem
-            key={item.userId}
             type="USER"

1-57: followersfollowing 페이지 간 코드 중복

followers/index.tsxfollowing/index.tsx의 렌더링 로직, ItemWrapper, ListWrapper 스타일드 컴포넌트가 거의 동일합니다. 퍼블리싱 단계에서는 허용 가능하지만, API 연동 시 공통 리스트 컴포넌트(예: FollowList)로 추출하는 것을 권장합니다.

src/shared/config/routesConfig.ts (1)

123-128: 와일드카드 * 라우트가 /:userId 보다 앞에 위치

react-router v6+는 경로 랭킹 기반 매칭을 사용하므로 순서상 문제가 발생하지 않을 수 있지만, 가독성과 명확성을 위해 와일드카드(*) 라우트를 설정 배열의 마지막에 배치하는 것이 관례입니다. 또한 /:userId는 모든 단일 세그먼트 경로와 매칭되므로, /error, /feedback 등 정적 경로와의 충돌 여부를 확인해 주세요.

#!/bin/bash
# routesConfig에서 라우트 소비 방식 확인 (createBrowserRouter 등)
rg -n "routesConfig" --type=ts --type=tsx -C5

const navigate = useNavigate()
const { isLogin } = useAuthStore()
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false)
const isLogin = useAuthStore()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

useAuthStore() 셀렉터 누락으로 로그인 체크가 항상 truthy

useAuthStore()는 셀렉터 없이 호출하면 전체 스토어 객체를 반환하므로 항상 truthy입니다. 따라서 비로그인 사용자도 로그인 모달이 표시되지 않고 팔로우 토글이 실행됩니다.

🐛 수정 제안
-  const isLogin = useAuthStore()
+  const isLogin = useAuthStore((state) => state.isLogin)
📝 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 isLogin = useAuthStore()
const isLogin = useAuthStore((state) => state.isLogin)
🤖 Prompt for AI Agents
In `@src/features/follow/ui/FollowButton.tsx` at line 21, The login check in
FollowButton is using useAuthStore() without a selector so it always returns a
truthy store object; update the call to useAuthStore to select the actual auth
flag or user (e.g., useAuthStore(state => state.isLoggedIn or state.user)) and
rename the variable from isLogin to something like isLoggedIn; then use that
boolean in the FollowButton click handler (and any checks around openLoginModal
or toggleFollow) so unauthenticated users trigger the login modal instead of
running the follow toggle.

Comment on lines +3 to +11
const UserLayout = () => {
return (
<>
<Outlet />
</>
)
}

export default UserLayout
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

파일명과 컴포넌트명 불일치

파일명은 FeedLayout.tsx이지만 컴포넌트명은 UserLayout입니다. 네이밍 일관성을 위해 둘 중 하나를 맞춰주세요. 또한 자식이 <Outlet />뿐이므로 Fragment(<>...</>)는 불필요합니다.

♻️ 제안
-const UserLayout = () => {
-  return (
-    <>
-      <Outlet />
-    </>
-  )
-}
-
-export default UserLayout
+const FeedLayout = () => {
+  return <Outlet />
+}
+
+export default FeedLayout
📝 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 UserLayout = () => {
return (
<>
<Outlet />
</>
)
}
export default UserLayout
const FeedLayout = () => {
return <Outlet />
}
export default FeedLayout
🤖 Prompt for AI Agents
In `@src/pages/feed/FeedLayout.tsx` around lines 3 - 11, The component declared as
UserLayout in FeedLayout.tsx is misnamed and unnecessarily wrapped in a
Fragment; either rename the component to FeedLayout (change function UserLayout
to function FeedLayout) and update the default export to export default
FeedLayout, or rename the file to match UserLayout; also remove the unnecessary
Fragment and return Outlet directly (e.g., return <Outlet />) inside the
FeedLayout/UserLayout function.

<HeaderSection>
<Header
left={<SvgButton icon={LeftArrow} onClick={() => navigate(-1)} />}
center={<span>홍길동</span>}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

하드코딩된 사용자 이름 "홍길동"을 라우트 파라미터 또는 API 데이터로 교체 필요

/:userId 라우트 하위에서 렌더링되는 컴포넌트이므로, useParamsuserId를 가져와 실제 사용자 이름을 표시해야 합니다. 퍼블리싱 단계에서 의도적인 플레이스홀더라면 TODO 주석을 남겨주세요.

💡 제안
-        center={<span>홍길동</span>}
+        center={<span>홍길동</span>} {/* TODO: userId 기반 사용자 이름 조회로 교체 - maylh */}
📝 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
center={<span>홍길동</span>}
center={<span>홍길동</span>} {/* TODO: userId 기반 사용자 이름 조회로 교체 - maylh */}
🤖 Prompt for AI Agents
In `@src/pages/feed/FollowLayout.tsx` at line 20, The center prop currently
renders a hardcoded name ("홍길동"); replace this by reading the route param and/or
fetching the real user name: import and call useParams() in the FollowLayout
component to get userId, then either display userId or call your existing
user-fetching function/ hook (e.g., fetchUser, useUser or an API client) to
resolve and render the real name in the center prop; if this is intentionally a
placeholder, add a TODO comment near FollowLayout noting it should be replaced
with real data before publishing.

Comment on lines +24 to +29
<ContentHeader
totalCount={2}
currentSort={selected}
onSortChange={onSelect}
options={['RECENT', 'OLDEST']}
countType="PEOPLE"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

하드코딩된 totalCount={2} — 실제 데이터 연동 시 교체 필요

팔로워/팔로잉 수도 플레이스홀더로 보입니다. 퍼블리싱 목적이라면 TODO 주석을 추가해 주세요.

🤖 Prompt for AI Agents
In `@src/pages/feed/FollowLayout.tsx` around lines 24 - 29, The ContentHeader call
currently passes a hardcoded totalCount={2}; replace this placeholder with the
real data source (e.g., a prop or state value such as followersCount,
followingCount, or totalCount derived in FollowLayout) or add a clear TODO
comment if left for publishing; update the JSX where ContentHeader is used (the
totalCount prop on ContentHeader) to accept the dynamic value (or add a TODO
above that line referencing the data to be wired) and ensure the selected and
onSelect props remain unchanged.

Comment on lines +131 to +143
children: [
{
path: '/:userId/followers',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Followers }],
},
{
path: '/:userId/following',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Following }],
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

중첩 라우트에서 절대 경로 사용 — 라우팅 오류 가능성

이 파일의 다른 중첩 라우트(예: Line 59의 '', Line 60의 ':id')는 모두 상대 경로를 사용하고 있습니다. 그런데 Line 133과 139에서는 /:userId/followers, /:userId/following처럼 절대 경로를 사용하고 있어 기존 패턴과 불일치합니다.

react-router-dom v7에서 중첩 라우트의 path는 부모 경로에 상대적으로 해석되므로, 부모가 이미 /:userId인 상태에서 자식에 다시 /:userId/...를 쓰면 의도와 다르게 동작하거나 매칭이 실패할 수 있습니다.

🐛 수정 제안: 상대 경로로 변경
     children: [
       {
-        path: '/:userId/followers',
+        path: 'followers',
         component: FollowLayout,
         isNotSuspense: true,
         children: [{ path: '', component: Followers }],
       },
       {
-        path: '/:userId/following',
+        path: 'following',
         component: FollowLayout,
         isNotSuspense: true,
         children: [{ path: '', component: Following }],
       },
     ],
#!/bin/bash
# 다른 중첩 라우트에서 절대 경로 vs 상대 경로 사용 패턴 확인
rg -n "path:" --type=ts -A0 src/shared/config/routesConfig.ts
🤖 Prompt for AI Agents
In `@src/shared/config/routesConfig.ts` around lines 131 - 143, 자식 라우트에서 절대
경로('/:userId/followers' 및 '/:userId/following')를 사용하고 있어 부모 경로와 중복되며 매칭 오류를 일으킬
수 있으니, FollowLayout의 children 항목에서 path를 부모에 상대적인 'followers' 및 'following'으로
변경하여 FollowLayout → Followers/Following 중첩 라우팅이 정상 동작하도록 수정하세요; 대상 식별자:
FollowLayout, Followers, Following, 문제 있는 path 문자열('/:userId/followers',
'/:userId/following').

currentSort={selected}
onSortChange={onSelect}
options={['RECENT', 'POPULAR']}
countType="number"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

스토리 문서의 예시 코드에서 countType 값이 불일치합니다.

Line 48의 예시에서 countType="number"(소문자)로 작성되어 있지만, Line 67의 실제 렌더 코드에서는 countType="NUMBER"(대문자)를 사용하고 있습니다. CountType'NUMBER' | 'PEOPLE'이므로 예시도 대문자로 수정해야 합니다.

📝 수정 제안
-  countType="number"
+  countType="NUMBER"
🤖 Prompt for AI Agents
In `@src/stories/ContentHeader.stories.tsx` at line 48, The story example uses
countType="number" but the component expects the CountType union ('NUMBER' |
'PEOPLE') and the actual render uses countType="NUMBER"; update the example prop
to use the uppercase literal "NUMBER" so the example matches the rendered usage
and the CountType type (change the countType prop in the example in
ContentHeader.stories to "NUMBER").

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request successfully establishes the publishing and routing structure for the following/follower list pages, and commendably refactors the ContentHeader component for enhanced flexibility. However, a significant security vulnerability has been identified: a medium-severity Open Redirect in src/pages/feed/ui/FollowTab.tsx. This issue stems from using unsanitized user input from the URL to construct redirect paths, which could enable attackers to craft malicious links. It is crucial to address this vulnerability promptly. Additionally, the review highlighted several areas for improvement: an error in the nested route path configuration within routesConfig.ts, a bug in the FollowButton component concerning login status checks and missing follow functionality, and adherence to style guidelines, including naming conventions and the use of magic numbers, across the new pages. Regarding architectural considerations, moving FollowLayout and FollowTab to a shared src/pages/feed/ui/layouts folder is a good practice for future scalability, and the mypage/ui structure aligns well with FSD principles. Please refer to the specific file comments for detailed feedback and the recommended fix for the Open Redirect vulnerability.

const navigate = useNavigate()
const { isLogin } = useAuthStore()
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false)
const isLogin = useAuthStore()

Choose a reason for hiding this comment

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

critical

useAuthStore는 전체 상태 객체를 반환하므로, isLogin 변수에는 boolean 값이 아닌 전체 스토어 객체가 할당됩니다. 이로 인해 if (isLogin) 조건문이 항상 참으로 평가되는 버그가 발생합니다.

객체 구조 분해 할당을 사용하여 isLogin 상태만 가져오도록 수정해야 합니다.

  const { isLogin } = useAuthStore()

Comment on lines +131 to +144
children: [
{
path: '/:userId/followers',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Followers }],
},
{
path: '/:userId/following',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Following }],
},
],

Choose a reason for hiding this comment

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

critical

react-router-dom v6에서 중첩 라우트(children)의 경로는 부모 경로에 상대적으로 설정되어야 합니다. 현재 followersfollowing 경로가 /:userId/followers와 같이 절대 경로로 작성되어 있어 라우팅이 정상적으로 동작하지 않습니다. 부모 경로인 /:userId를 기준으로 상대 경로인 'followers''following'으로 수정해야 합니다.

Suggested change
children: [
{
path: '/:userId/followers',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Followers }],
},
{
path: '/:userId/following',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Following }],
},
],
children: [
{
path: 'followers',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Followers }],
},
{
path: 'following',
component: FollowLayout,
isNotSuspense: true,
children: [{ path: '', component: Following }],
},
],

if (isLogin) {
toggleFollow()
setIsBottomSheetOpen(false)
setFollowing((prev) => !prev)

Choose a reason for hiding this comment

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

high

현재 handleFollowClick 함수가 setFollowing을 통해 컴포넌트의 로컬 상태만 변경하고 있어, 실제 팔로우/언팔로우 기능이 동작하지 않습니다. 이는 UI 퍼블리싱을 위한 임시 구현으로 보이지만, 핵심 기능이 누락된 상태이므로 빠른 시일 내에 API 연동 로직을 추가해야 합니다.

스타일 가이드(2.4)에 따라 Tanstack Query의 useMutation을 사용하여 비동기 상태를 관리하는 것을 권장합니다.

Comment on lines +12 to +14
<TabButton to={`/${userId}/following`}>팔로잉</TabButton>

<TabButton to={`/${userId}/followers`}>팔로워</TabButton>

Choose a reason for hiding this comment

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

security-medium medium

The userId parameter, obtained from the URL via useParams(), is used directly in the to prop of NavLink components. An attacker can craft a malicious URL with a path traversal payload (e.g., ../admin) in the userId segment. When a user clicks on the generated links, they could be redirected to an unintended page within the application, potentially leading to unauthorized access to other parts of the application. This is a form of Open Redirect.

Suggested change
<TabButton to={`/${userId}/following`}>팔로잉</TabButton>
<TabButton to={`/${userId}/followers`}>팔로워</TabButton>
<TabButton to={`/${encodeURIComponent(userId)}/following`}>팔로잉</TabButton>
<TabButton to={`/${encodeURIComponent(userId)}/followers`}>팔로워</TabButton>

playlistId: number
userName: string
profile?: string
userId?: string

Choose a reason for hiding this comment

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

medium

컴포넌트에 전달된 userId prop이 사용되지 않고 있습니다. 팔로우/언팔로우 API 연동 시 필요할 것으로 보이는데, 만약 현재 구현 범위에서 사용되지 않는다면 삭제하거나, 추후 사용될 예정이라면 // TODO 주석을 추가하여 명시해주는 것이 좋겠습니다.

Comment on lines +20 to +25
center={<span>홍길동</span>}
/>
<FollowTab />
<ContentHeaderWrapper>
<ContentHeader
totalCount={2}

Choose a reason for hiding this comment

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

medium

스타일 가이드 1.4에 따라, 의미를 알 수 없는 문자열이나 숫자인 '매직 넘버/스트링'의 사용을 지양해야 합니다. 현재 헤더에 사용자 이름이 '홍길동'으로, ContentHeadertotalCount가 2로 하드코딩되어 있습니다. 퍼블리싱을 위한 임시 데이터라도, 상수로 분리하거나 추후 실제 데이터를 받아올 부분임을 명시하는 것이 좋습니다.

References
  1. 의미를 알 수 없는 숫자나 문자열(매직 넘버/스트링) 대신 명명된 상수를 사용합니다. (link)

Comment on lines +9 to +22
const followerList = [
{
userId: '1',
profileUrl: '',
userName: '지구젤리',
isFollowing: true,
},
{
userId: '2',
profileUrl: '',
userName: '김들락',
isFollowing: false,
},
]

Choose a reason for hiding this comment

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

medium

스타일 가이드 1.2에 따라, 컴포넌트 외부의 상수 데이터는 UPPER_SNAKE_CASE로 명명해야 합니다. followerListFOLLOWER_LIST로 변경하는 것을 제안합니다.

Suggested change
const followerList = [
{
userId: '1',
profileUrl: '',
userName: '지구젤리',
isFollowing: true,
},
{
userId: '2',
profileUrl: '',
userName: '김들락',
isFollowing: false,
},
]
const FOLLOWER_LIST = [
References
  1. 상수는 대문자 스네이크 케이스(UPPER_SNAKE_CASE)를 사용합니다 (예: API_BASE_URL, MAX_ITEM_COUNT). (link)

{followerList?.map((item) => (
<ItemWrapper key={item.userId}>
<SearchResultItem
key={item.userId}

Choose a reason for hiding this comment

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

medium

map 함수 내에서 key prop은 최상위 엘리먼트에만 지정하면 됩니다. 현재 ItemWrapperkey가 올바르게 적용되어 있으므로, 자식 컴포넌트인 SearchResultItemkey prop은 불필요하며 제거해야 합니다.

Suggested change
key={item.userId}
type="USER"

Comment on lines +9 to +22
const followerList = [
{
userId: '1',
profileUrl: '',
userName: '지구젤리',
isFollowing: true,
},
{
userId: '2',
profileUrl: '',
userName: '김들락',
isFollowing: true,
},
]

Choose a reason for hiding this comment

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

medium

팔로잉 목록 페이지임에도 불구하고 변수명이 followerList로 되어 있어 혼동을 줄 수 있습니다. followingList와 같이 페이지의 역할에 맞는 명확한 이름으로 변경하는 것이 좋습니다. 또한, 스타일 가이드 1.2에 따라 상수는 UPPER_SNAKE_CASE로 명명해야 하므로 FOLLOWING_LIST로 변경하는 것을 제안합니다.

Suggested change
const followerList = [
{
userId: '1',
profileUrl: '',
userName: '지구젤리',
isFollowing: true,
},
{
userId: '2',
profileUrl: '',
userName: '김들락',
isFollowing: true,
},
]
const FOLLOWING_LIST = [
References
  1. 변수, 함수, 상수는 명확하고 의미론적이며 일관성을 유지하도록 명명해야 합니다. 상수는 UPPER_SNAKE_CASE를 사용합니다. (link)

{followerList?.map((item) => (
<ItemWrapper key={item.userId}>
<SearchResultItem
key={item.userId}

Choose a reason for hiding this comment

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

medium

map 함수 내에서 key prop은 최상위 엘리먼트에만 지정하면 됩니다. 현재 ItemWrapperkey가 올바르게 적용되어 있으므로, 자식 컴포넌트인 SearchResultItemkey prop은 불필요하며 제거해야 합니다.

Suggested change
key={item.userId}
type="USER"

Copy link
Member

@hansololiviakim hansololiviakim left a comment

Choose a reason for hiding this comment

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

구조 정리 감사합니다 👏
말씀주신대로 마이페이지는 중첩 경로가 너무 많아서 UI에 해당 경로 묶어두고, 각 페이지들은 Next.js app route처럼 최대한 UI만 구현하는 로직이 들어가도록 짜뒀어요! (내부에서 step 돌아가는 customize 제외..)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

MEDIUM 일반적인 중간 우선순위

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 팔로잉/팔로워 목록 페이지 퍼블리싱

2 participants