Skip to content

feat: 사용자 추천 프로필 클릭 시 상대방 프로필로 이동 구현 - #261

Merged
shinwokkang merged 2 commits into
devfrom
feat/260-user-recommend-clickToMove
Apr 18, 2026
Merged

shinwokkang merged 2 commits into
devfrom
feat/260-user-recommend-clickToMove

Conversation

@shinwokkang

@shinwokkang shinwokkang commented Apr 18, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)
홈 화면 및 스토리 페이지의 사용자 추천 목록에서 카드 클릭 시 해당 사용자의 프로필 페이지(/profile/[nickname])로 이동하는 기능을 구현했습니다.
관련 이슈: #260

🛠️ 변경 사항 (Changes)
새로운 기능 추가
사용자 추천 카드(컨테이너) 전체를 클릭 가능 영역으로 확대
클릭 시 해당 유저의 프로필로 이동하는 라우팅 로직 연결 (Home, Stories 페이지 공통 적용)
코드 리팩토링
ListSubscribeElement, ListSubscribeLarge 컴포넌트의 인터페이스에 onProfileClick 추가하여 단일 구조 원칙 준수
구독 버튼 클릭 시 프로필 이동이 중복 발생하지 않도록 이벤트 버블링 방지(e.stopPropagation()) 처리
기타 (설명: UX 개선)
마우스 오버 시 시각적 피드백을 위한 hover:bg-stone-100 및 cursor-pointer 스타일링 적용

📸 스크린샷 (Screenshots)

✅ 체크리스트 (Checklist)
상세 구현 노트
이벤트 분리: 카드의 어느 곳을 눌러도 프로필로 이동하지만, '구독' 버튼을 누를 때만은 프로필로 이동하지 않고 순수하게 구독 기능만 동작하도록 설계했습니다.
재사용성: 컴포넌트 내부에서 직접 useRouter를 사용하지 않고 부모로부터 핸들러를 주입받는 방식을 사용하여, 다른 페이지에서 추천 컴포넌트를 재사용할 때도 유연하게 대응할 수 있습니다.

Summary by CodeRabbit

  • New Features
    • Users can now click on user cards in recommendations and subscription lists to navigate directly to their profiles. This interactive feature is available across the app, including home recommendations, story pages, and other user list sections, enabling seamless profile exploration.

@shinwokkang shinwokkang self-assigned this Apr 18, 2026
@shinwokkang shinwokkang added the ✨ feat 새로운 기능 추가 label Apr 18, 2026
@vercel

vercel Bot commented Apr 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment Apr 18, 2026 7:16am
checkmo Ready Ready Preview, Comment Apr 18, 2026 7:16am

@coderabbitai

coderabbitai Bot commented Apr 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a8dda38-45ca-4b9b-8405-009f2d0efbce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/260-user-recommend-clickToMove

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 gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements profile navigation by adding onProfileClick handlers to recommendation and story list components. Key changes include updating the UI to support clickable profile elements with hover effects and preventing event bubbling on the subscribe buttons. Review feedback focuses on enhancing web accessibility by adding appropriate ARIA roles and tab indices to clickable containers, ensuring hover styles are only applied when the click handler is present, and maintaining visual consistency across different list element components.

Comment on lines +26 to +29
<div
className="flex w-full t:w-[296px] min-h-[60px] t:h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white cursor-pointer hover:bg-stone-100 transition-colors group"
onClick={onProfileClick}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

onProfileClick이 선택적(optional) 속성이므로, 클릭 가능한 스타일(cursor-pointer, hover 배경색)은 해당 함수가 존재할 때만 적용되는 것이 좋습니다. 또한 div를 클릭 가능하게 만들 때는 웹 접근성을 위해 role="button"과 tabIndex 설정을 권장합니다.

    <div 
      className={`flex w-full t:w-[296px] min-h-[60px] t:h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white transition-colors group ${onProfileClick ? "cursor-pointer hover:bg-stone-100" : ""}`}
      onClick={onProfileClick}
      role={onProfileClick ? "button" : undefined}
      tabIndex={onProfileClick ? 0 : -1}
    >

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"타당하고 세심한 리뷰 감사합니다! onProfileClick이 없을 때는 클릭 가능하지 않다는 것을 시각적으로 명확히 전달하도록 조건부 스타일을 적용했습니다. 또한, 웹 접근성 표준을 준수하기 위해 role="button"과 tabIndex 속성을 추가하여 키보드 사용자 및 스크린 리더 사용자도 원활하게 인지하고 이용할 수 있도록 개선하였습니다. 제안 주신 덕분에 컴포넌트의 완성도가 한층 높아졌습니다. 감사합니다!"

Comment on lines +28 to +31
<div
className="flex w-[296px] h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white cursor-pointer hover:bg-stone-100 transition-colors group"
onClick={onProfileClick}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

onProfileClick이 존재할 때만 클릭 가능한 스타일이 적용되도록 조건부 클래스를 사용하는 것이 좋습니다. 또한 웹 접근성을 위해 적절한 ARIA 역할(role)과 탭 인덱스(tabIndex)를 추가하는 것을 권장합니다.

    <div
      className={`flex w-[296px] h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white transition-colors group ${onProfileClick ? "cursor-pointer hover:bg-stone-100" : ""}`}
      onClick={onProfileClick}
      role={onProfileClick ? "button" : undefined}
      tabIndex={onProfileClick ? 0 : -1}
    >

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

좋은 지적 감사합니다! list_subscribe_large 컴포넌트 역시 onProfileClick이 옵셔널이므로, 동작이 있을 때만 인터랙션(커서, 호버 배경색)이 발생하도록 조건부 클래스를 적용했습니다. 또한, 웹 접근성 향상을 위해 클릭이 가능한 상태일 때 role="button"과 tabIndex={0}이 활성화되도록 수정하여 일관성 있는 환경을 구축했습니다. 덕분에 더욱 완성도 높은 컴포넌트가 되었습니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (5)
src/app/(main)/page.tsx (1)

55-55: LGTM — consider URL-encoding the nickname.

Wiring is correct. Minor: router.push(/profile/${encodeURIComponent(nickname)}) would be safer against nicknames containing reserved URL characters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(main)/page.tsx at line 55, The onProfileClick handler currently
pushes raw nicknames into the URL via router.push(`/profile/${nickname}`);
update the handler used in the component (the onProfileClick prop / arrow
function) to URL-encode the nickname before navigation (use encodeURIComponent
on the nickname argument) so reserved characters are escaped and routes remain
valid.
src/components/base-ui/home/Recommendation/list_subscribe_element.tsx (1)

55-55: Duplicate subscribe handler — extract.

The exact same onClick={(e) => { e.stopPropagation(); onSubscribeClick?.(); }} appears on both the tablet and mobile buttons. Extract once:

♻️ Proposed refactor
+  const handleSubscribe = (e: React.MouseEvent) => {
+    e.stopPropagation();
+    onSubscribeClick?.();
+  };
...
-      onClick={(e) => { e.stopPropagation(); onSubscribeClick?.(); }}
+      onClick={handleSubscribe}
...
-      onClick={(e) => { e.stopPropagation(); onSubscribeClick?.(); }}
+      onClick={handleSubscribe}

Also applies to: 66-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/home/Recommendation/list_subscribe_element.tsx` at
line 55, The duplicate inline handler on the tablet and mobile subscribe buttons
should be extracted into a single named handler to avoid repetition: create a
function (e.g. handleSubscribeClick) inside the component that takes the event,
calls e.stopPropagation(), and then invokes onSubscribeClick?.(), then replace
both occurrences of onClick={(e) => { e.stopPropagation(); onSubscribeClick?.();
}} with onClick={handleSubscribeClick}; ensure the function is defined in the
same scope as the buttons in list_subscribe_element.tsx so it can access
onSubscribeClick.
src/components/base-ui/home/Story/HomeStoryList.tsx (1)

63-63: Same duplication/encoding note as stories page.

(nickname) => router.push(/profile/${nickname}) appears twice here and in several other files in this PR. Consider extracting a shared helper (or a custom hook like useProfileNavigation()) and URL-encoding the nickname to defend against special characters.

Also applies to: 81-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/home/Story/HomeStoryList.tsx` at line 63, The
onProfileClick handler (onProfileClick={(nickname) =>
router.push(`/profile/${nickname}`)}) is duplicated across files; extract a
shared helper or custom hook (e.g., useProfileNavigation or navigateToProfile)
that takes a nickname, URL-encodes it via encodeURIComponent, and calls
router.push(`/profile/${encodedNickname}`), then replace all inline arrow
handlers (including the one in HomeStoryList and the duplicate at lines noted)
with the new helper to centralize logic and guard against special characters.
src/components/base-ui/home/Recommendation/HomeRecommendationSection.tsx (1)

10-10: Prop threading looks good; note type mismatch with ListSubscribeElement.

HomeRecommendationSectionProps.onProfileClick is correctly typed as (nickname: string) => void and invoked with u.nickname on line 50. However, ListSubscribeElement's prop type is onProfileClick?: () => void (no args) — fine here since the wrapper injects the nickname via closure, but the naming is asymmetric vs. ListSubscribeLarge which takes a nickname. Worth aligning both child components to the same signature for consistency and to reduce cognitive load.

Also applies to: 50-50, 64-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/home/Recommendation/HomeRecommendationSection.tsx` at
line 10, HomeRecommendationSectionProps defines onProfileClick as (nickname:
string) => void but ListSubscribeElement currently types onProfileClick as () =>
void; make the signatures consistent by changing ListSubscribeElement’s prop
type to onProfileClick?: (nickname: string) => void and update its internal
invocation (and any wrapper closures) to accept and forward the nickname (the
HomeRecommendationSection currently calls onProfileClick(u.nickname) and
ListSubscribeLarge already accepts a nickname) so both ListSubscribeElement and
ListSubscribeLarge use the same (nickname: string) => void signature.
src/app/(main)/stories/page.tsx (1)

172-172: Consider URL-encoding the nickname and deduplicating the handler.

router.push(/profile/${nickname}) is duplicated (lines 172 and 181), and raw nickname is interpolated without encoding. If a nickname contains characters that are not URL-safe (spaces, #, ?, /, non-ASCII edge cases), the resulting path can be misinterpreted. Wrap with encodeURIComponent, and extract a single handler to reuse.

♻️ Suggested refactor
+  const handleProfileClick = (nickname: string) =>
+    router.push(`/profile/${encodeURIComponent(nickname)}`);
...
-        onProfileClick={(nickname) => router.push(`/profile/${nickname}`)}
+        onProfileClick={handleProfileClick}
...
-              onProfileClick={(nickname) => router.push(`/profile/${nickname}`)}
+              onProfileClick={handleProfileClick}

Also applies to: 181-181

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(main)/stories/page.tsx at line 172, Extract a single reusable
handler (e.g., goToProfile or handleProfileClick) instead of duplicating the
inline arrow for onProfileClick, and call router.push from that handler with the
nickname URL-encoded using encodeURIComponent (i.e.,
router.push(`/profile/${encodeURIComponent(nickname)}`)) so nicknames with
spaces/special chars are safe; you can memoize it with useCallback if needed and
then pass the same handler to both onProfileClick usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/base-ui/home/Recommendation/list_subscribe_element.tsx`:
- Around line 26-29: The card div using onProfileClick (in
list_subscribe_element.tsx) is not keyboard or screen-reader accessible; update
the interactive surface by either wrapping the card with next/link for native
semantics or, if keeping the div, add role="button", tabIndex={0}, an onKeyDown
handler that triggers onProfileClick for Enter and Space, and an aria-label like
`${name} 프로필 보기`; also ensure a visible focus ring via a focus-visible class and
make subscribe button clicks call e.preventDefault() and e.stopPropagation() so
they don't trigger the card navigation.

In `@src/components/base-ui/home/Recommendation/list_subscribe_large.tsx`:
- Around line 28-31: The clickable card currently implemented as a <div> with
only onClick is not keyboard or screen-reader accessible; update the component
(the element using onProfileClick in list_subscribe_large.tsx) to use a semantic
link via next/link when navigating to a profile (e.g., href built from the
profile name) or, if you must keep a non-link container, add role="button",
tabIndex={0}, and an onKeyDown handler that calls onProfileClick when Enter or
Space is pressed; if you switch to Link and still have an inner subscribe button
(onSubscribeClick), ensure the button’s handler calls e.preventDefault() and
e.stopPropagation() to avoid triggering the link navigation.

---

Nitpick comments:
In `@src/app/`(main)/page.tsx:
- Line 55: The onProfileClick handler currently pushes raw nicknames into the
URL via router.push(`/profile/${nickname}`); update the handler used in the
component (the onProfileClick prop / arrow function) to URL-encode the nickname
before navigation (use encodeURIComponent on the nickname argument) so reserved
characters are escaped and routes remain valid.

In `@src/app/`(main)/stories/page.tsx:
- Line 172: Extract a single reusable handler (e.g., goToProfile or
handleProfileClick) instead of duplicating the inline arrow for onProfileClick,
and call router.push from that handler with the nickname URL-encoded using
encodeURIComponent (i.e.,
router.push(`/profile/${encodeURIComponent(nickname)}`)) so nicknames with
spaces/special chars are safe; you can memoize it with useCallback if needed and
then pass the same handler to both onProfileClick usages.

In `@src/components/base-ui/home/Recommendation/HomeRecommendationSection.tsx`:
- Line 10: HomeRecommendationSectionProps defines onProfileClick as (nickname:
string) => void but ListSubscribeElement currently types onProfileClick as () =>
void; make the signatures consistent by changing ListSubscribeElement’s prop
type to onProfileClick?: (nickname: string) => void and update its internal
invocation (and any wrapper closures) to accept and forward the nickname (the
HomeRecommendationSection currently calls onProfileClick(u.nickname) and
ListSubscribeLarge already accepts a nickname) so both ListSubscribeElement and
ListSubscribeLarge use the same (nickname: string) => void signature.

In `@src/components/base-ui/home/Recommendation/list_subscribe_element.tsx`:
- Line 55: The duplicate inline handler on the tablet and mobile subscribe
buttons should be extracted into a single named handler to avoid repetition:
create a function (e.g. handleSubscribeClick) inside the component that takes
the event, calls e.stopPropagation(), and then invokes onSubscribeClick?.(),
then replace both occurrences of onClick={(e) => { e.stopPropagation();
onSubscribeClick?.(); }} with onClick={handleSubscribeClick}; ensure the
function is defined in the same scope as the buttons in
list_subscribe_element.tsx so it can access onSubscribeClick.

In `@src/components/base-ui/home/Story/HomeStoryList.tsx`:
- Line 63: The onProfileClick handler (onProfileClick={(nickname) =>
router.push(`/profile/${nickname}`)}) is duplicated across files; extract a
shared helper or custom hook (e.g., useProfileNavigation or navigateToProfile)
that takes a nickname, URL-encodes it via encodeURIComponent, and calls
router.push(`/profile/${encodedNickname}`), then replace all inline arrow
handlers (including the one in HomeStoryList and the duplicate at lines noted)
with the new helper to centralize logic and guard against special characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd847d4e-7efe-4280-8827-2b2c623eec55

📥 Commits

Reviewing files that changed from the base of the PR and between 540cede and 806b191.

📒 Files selected for processing (6)
  • src/app/(main)/page.tsx
  • src/app/(main)/stories/page.tsx
  • src/components/base-ui/home/Recommendation/HomeRecommendationSection.tsx
  • src/components/base-ui/home/Recommendation/list_subscribe_element.tsx
  • src/components/base-ui/home/Recommendation/list_subscribe_large.tsx
  • src/components/base-ui/home/Story/HomeStoryList.tsx

Comment on lines +28 to +31
<div
className="flex w-[296px] h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white cursor-pointer hover:bg-stone-100 transition-colors group"
onClick={onProfileClick}
>

@coderabbitai coderabbitai Bot Apr 18, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Accessibility: clickable <div> is not keyboard/screen-reader navigable.

The card is now a primary interactive element but remains a <div> with only onClick. Keyboard users cannot focus or activate it, and assistive tech won't announce it as actionable. Prefer a semantic anchor (ideal for a profile link — enables prefetching, middle-click, "open in new tab", and SEO) or at minimum add role="button", tabIndex={0}, and an onKeyDown handler for Enter/Space.

♻️ Preferred: use `next/link` (requires lifting navigation into the component, or accepting an href prop)
import Link from "next/link";
// ...
<Link
  href={`/profile/${encodeURIComponent(name)}`}
  className="flex w-[296px] h-[66px] ... cursor-pointer hover:bg-stone-100 ... group"
>
  {/* ... */}
  <button
    type="button"
    onClick={(e) => { e.preventDefault(); e.stopPropagation(); onSubscribeClick?.(); }}
    ...
  >
    {buttonText}
  </button>
</Link>

Note: when nesting a <button> inside <a>, use e.preventDefault() in addition to stopPropagation() to suppress the link navigation.

Minimal fallback if you keep the <div>:

 <div
   className="... cursor-pointer hover:bg-stone-100 ..."
+  role="button"
+  tabIndex={0}
   onClick={onProfileClick}
+  onKeyDown={(e) => {
+    if (e.key === "Enter" || e.key === " ") {
+      e.preventDefault();
+      onProfileClick?.();
+    }
+  }}
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/home/Recommendation/list_subscribe_large.tsx` around
lines 28 - 31, The clickable card currently implemented as a <div> with only
onClick is not keyboard or screen-reader accessible; update the component (the
element using onProfileClick in list_subscribe_large.tsx) to use a semantic link
via next/link when navigating to a profile (e.g., href built from the profile
name) or, if you must keep a non-link container, add role="button",
tabIndex={0}, and an onKeyDown handler that calls onProfileClick when Enter or
Space is pressed; if you switch to Link and still have an inner subscribe button
(onSubscribeClick), ensure the button’s handler calls e.preventDefault() and
e.stopPropagation() to avoid triggering the link navigation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

좋은 지적 감사합니다! list_subscribe_large 컴포넌트 역시 onProfileClick이 옵셔널이므로, 동작이 있을 때만 인터랙션(커서, 호버 배경색)이 발생하도록 조건부 클래스를 적용했습니다. 또한, 웹 접근성 향상을 위해 클릭이 가능한 상태일 때 role="button"과 tabIndex={0}이 활성화되도록 수정하여 일관성 있는 환경을 구축했습니다. 덕분에 더욱 완성도 높은 컴포넌트가 되었습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@shinwokkang 개선 사항 감사합니다! onProfileClick 유무에 따른 조건부 스타일링과 role="button" / tabIndex 적용은 훌륭합니다 👍

한 가지 추가로 말씀드리면, 현재 구현에서는 키보드의 Enter / Space 키로 카드를 활성화할 수 없는 상태입니다. tabIndex={0}으로 포커스는 가능하지만, onKeyDown 핸들러가 없으면 스크린 리더 및 키보드 사용자가 실제로 클릭 동작을 수행할 수 없습니다. 아래와 같이 추가하면 접근성이 완성됩니다:

<div
  className={`flex w-[296px] h-[66px] px-[14px] py-[8px] gap-[8px] rounded-[8px] border border-Subbrown-4 bg-white transition-colors group ${onProfileClick ? "cursor-pointer hover:bg-stone-100" : ""}`}
  onClick={onProfileClick}
  role={onProfileClick ? "button" : undefined}
  tabIndex={onProfileClick ? 0 : -1}
  onKeyDown={onProfileClick ? (e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      onProfileClick();
    }
  } : undefined}
>

이 부분까지 반영해 주시면 키보드 전용 사용자와 보조 기술(AT) 사용자 모두에게 완전한 접근성을 제공할 수 있습니다! 😊

@shinwokkang
shinwokkang merged commit 81a6f46 into dev Apr 18, 2026
4 checks passed
@shinwokkang
shinwokkang deleted the feat/260-user-recommend-clickToMove branch April 18, 2026 07:16

This branch was successfully deployed

2 active deployments
Preview – checkmo — cff6b763 Deployed Apr 18, 2026 by vercel[bot]
Preview – check-mo — cff6b763 Deployed Apr 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant