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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/member/right-chevron.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions src/components/members/MemberList.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import styled from '@emotion/styled';
import { colors, typography, semanticColors } from '../../styles/global/global';

export const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;

export const MemberItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
cursor: pointer;
transition: background-color 0.2s;
position: relative;

&:hover {
background-color: ${colors.darkgrey['50']};
}

&:focus-visible {
outline: 2px solid ${semanticColors.text.point.green};
outline-offset: -2px;
}

&::after {
content: '';
position: absolute;
bottom: 0;
left: 20px;
right: 20px;
height: 1px;
background-color: ${colors.darkgrey.dark};
}

&:last-child::after {
display: none;
}
`;

export const ProfileSection = styled.div`
display: flex;
align-items: center;
gap: 8px;
flex: 1;
`;

export const ProfileImage = styled.div`
width: 48px;
height: 48px;
border-radius: 50%;
background-color: ${colors.grey['400']};
flex-shrink: 0;
`;
Comment on lines +50 to +56
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

프로필 이미지는 div 대신 img 사용 권장(alt 제공 및 object-fit)

현재 div는 대체 텍스트를 제공할 수 없어 접근성이 떨어집니다. styled.img로 전환하고 object-fit: cover를 적용하면 비율 유지와 자르기가 자연스럽습니다. 컴포넌트에서 alt를 의미 있게 제공하거나 장식용이면 alt=""를 지정하세요.

-export const ProfileImage = styled.div`
+export const ProfileImage = styled.img`
   width: 48px;
   height: 48px;
   border-radius: 50%;
+  object-fit: cover;
   background-color: ${colors.grey['400']};
   flex-shrink: 0;
 `

ProfileImage 컴포넌트가 실제로 img 태그로 교체해도 문제가 없는지, 사용 위치와 전달되는 props(src, alt 등)를 확인하기 위해 아래 스크립트를 실행하겠습니다.


🏁 Script executed:

#!/bin/bash
grep -R "ProfileImage" -n src

Length of output: 2953


프로필 이미지가 실제로 srcalt 같은 props를 받아 렌더링되는지 확인이 필요합니다. 아래 스크립트로 MemberList.tsx 사용 부분을 살펴보겠습니다.


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/components/members/MemberList.tsx

Length of output: 1382


프로필 URL이 실제로 Member 타입에 포함되어 있는지 확인하기 위해 src/mocks/members.mock.ts를 조회해 보겠습니다.


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/mocks/members.mock.ts

Length of output: 1160


프로필 이미지를 <img>로 전환하고 src/alt를 전달하세요

현재 ProfileImage는 styled.div로 고정된 회색 원형만 렌더링되어 실제 유저 이미지와 대체 텍스트를 제공하지 않습니다. 접근성과 화면 비율 유지를 위해 다음을 적용해주세요:

• src/components/members/MemberList.styled.ts

  • styled.divstyled.img
  • object-fit: cover 추가
-export const ProfileImage = styled.div`
+export const ProfileImage = styled.img`
  width: 48px;
  height: 48px;
  border-radius: 50%;
+   object-fit: cover;
  background-color: ${colors.grey['400']};
  flex-shrink: 0;
`;

• src/components/members/MemberList.tsx

  • <ProfileImage /> 사용부에 src와 유의미한 alt 전달
{members.map(member => (
  <MemberItem key={member.id} onClick={() => handleMemberClick(member.id)}>
    <ProfileSection>
-       <ProfileImage />
+       <ProfileImage
+         src={member.profileImage || undefined}
+         alt={member.nickname ? `${member.nickname} 프로필` : ''}
+       />
      <MemberInfo>
        <MemberName>{member.nickname}</MemberName>
        <MemberRole>{member.role}</MemberRole>
      </MemberInfo>
    </ProfileSection>
    <MemberStatus>{member.followers}</MemberStatus>
    <ChevronIcon src={rightChevron} alt="이동" />
  </MemberItem>
))}
🤖 Prompt for AI Agents
In src/components/members/MemberList.styled.ts lines 45 to 51, change
ProfileImage from styled.div to styled.img, add object-fit: cover to maintain
image aspect ratio, and remove background-color since the image will be shown.
Then in src/components/members/MemberList.tsx, update all uses of <ProfileImage
/> to pass appropriate src and alt props for accessibility and correct image
rendering.


export const MemberInfo = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;

export const MemberName = styled.div`
color: ${semanticColors.text.primary};
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.medium};
`;

export const MemberRole = styled.div`
color: ${semanticColors.text.point.green};
font-size: ${typography.fontSize.xs};
font-weight: ${typography.fontWeight.regular};
`;

export const MemberStatus = styled.div`
color: white;
font-size: 11px;
font-weight: ${typography.fontWeight.regular};
`;

export const ChevronIcon = styled.img`
width: 24px;
height: 24px;
`;
66 changes: 66 additions & 0 deletions src/components/members/MemberList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useNavigate } from 'react-router-dom';
import type { KeyboardEvent } from 'react';
import rightChevron from '../../assets/member/right-chevron.svg';
import type { Member } from '../../mocks/members.mock';
import {
Container,
MemberItem,
ProfileSection,
ProfileImage,
MemberInfo,
MemberName,
MemberRole,
MemberStatus,
ChevronIcon,
} from './MemberList.styled';

interface MemberListProps {
members: Member[];
onMemberClick?: (memberId: string) => void;
}

const MemberList = ({ members, onMemberClick }: MemberListProps) => {
const navigate = useNavigate();

const handleMemberClick = (memberId: string) => {
if (onMemberClick) {
onMemberClick(memberId);
} else {
// 기본 동작: 개별 유저 페이지로 이동
navigate(`/otherfeed/${memberId}`);
}
};

return (
<Container>
{members.map(member => (
<MemberItem
key={member.id}
role="button"
tabIndex={0}
onClick={() => handleMemberClick(member.id)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleMemberClick(member.id);
}
}}
>
<ProfileSection>
<ProfileImage />
<MemberInfo>
<MemberName>{member.nickname}</MemberName>
<MemberRole>{member.role}</MemberRole>
</MemberInfo>
</ProfileSection>
<MemberStatus>
{`${(member.followersCount ?? 0).toLocaleString()}명이 띱 하는 중`}
</MemberStatus>
<ChevronIcon src={rightChevron} alt="이동" />
</MemberItem>
))}
</Container>
);
};

export default MemberList;
67 changes: 67 additions & 0 deletions src/mocks/members.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export interface Member {
id: string;
nickname: string;
role: string;
profileImage: string;
points: number;
followersCount: number;
}

export const mockMembers: Member[] = [
{
id: '1',
nickname: 'Thiper',
role: '칭호칭호',
profileImage: '', // 빈 문자열로 기본 이미지 처리
points: 0,
followersCount: 0,
},
{
id: '2',
nickname: 'thipthip',
role: '공식 인플루언서',
profileImage: '',
points: 0,
followersCount: 0,
},
{
id: '3',
nickname: 'Thiper',
role: '청춘청춘',
profileImage: '',
points: 0,
followersCount: 0,
},
{
id: '4',
nickname: 'thip01',
role: '작가',
profileImage: '',
points: 0,
followersCount: 0,
},
{
id: '5',
nickname: 'thip01',
role: '작가',
profileImage: '',
points: 0,
followersCount: 0,
},
{
id: '6',
nickname: 'thip01',
role: '작가',
profileImage: '',
points: 0,
followersCount: 0,
},
{
id: '7',
nickname: 'thip01',
role: '작가',
profileImage: '',
points: 0,
followersCount: 0,
},
];
13 changes: 13 additions & 0 deletions src/pages/groupMembers/GroupMembers.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from '@emotion/styled';
import { colors } from '../../styles/global/global';

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
padding-top: 56px;
min-width: 320px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
background-color: ${colors.black.main};
`;
34 changes: 34 additions & 0 deletions src/pages/groupMembers/GroupMembers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useNavigate } from 'react-router-dom';
import TitleHeader from '../../components/common/TitleHeader';
import MemberList from '../../components/members/MemberList';
import leftArrow from '../../assets/common/leftArrow.svg';
import { mockMembers } from '../../mocks/members.mock';
import { Wrapper } from './GroupMembers.styled';

const GroupMembers = () => {
const navigate = useNavigate();

const handleBackClick = () => {
navigate(-1);
};

const handleMemberClick = (memberId: string) => {
// 특정 사용자 페이지로 이동
navigate(`/user/${memberId}`);
};
Comment on lines +15 to +18
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사용자 페이지 경로 불일치 수정 필요

현재 /user/${memberId}로 이동하지만 라우터에는 해당 경로가 없습니다. 기존 사용자 상세 라우트(otherfeed/:userId)와 맞추시는 걸 권장합니다.

옵션 B를 채택할 경우 다음과 같이 변경하세요.

-  navigate(`/user/${memberId}`);
+  navigate(`/otherfeed/${memberId}`);

라우트 추가(옵션 A)를 선택한다면 본 변경은 불필요합니다.

📝 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 handleMemberClick = (memberId: string) => {
// 특정 사용자 페이지로 이동
navigate(`/user/${memberId}`);
};
const handleMemberClick = (memberId: string) => {
// 특정 사용자 페이지로 이동
navigate(`/otherfeed/${memberId}`);
};
🤖 Prompt for AI Agents
In src/pages/groupMembers/GroupMembers.tsx lines 15 to 18, the navigation path
for user pages is incorrect as it uses `/user/${memberId}`, which does not match
any existing route. To fix this, update the navigate call to use the existing
route pattern `/otherfeed/${memberId}` so it aligns with the defined router
paths.


return (
<>
<TitleHeader
leftIcon={<img src={leftArrow} alt="뒤로가기" />}
title="독서메이트"
onLeftClick={handleBackClick}
/>
<Wrapper>
<MemberList members={mockMembers} onMemberClick={handleMemberClick} />
</Wrapper>
</>
);
};

export default GroupMembers;
2 changes: 2 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import WithdrawDonePage from './mypage/WithdrawDonePage';
import EditPage from './mypage/EditPage';
import Notice from './notice/Notice';
import ParticipatedGroupDetail from './groupDetail/ParticipatedGroupDetail';
import GroupMembers from './groupMembers/GroupMembers';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

내비게이션 경로 불일치: '/user/:id' 라우트가 없어 클릭 시 404 가능성

MemberList/GroupMembers에서 /user/${memberId}로 이동하지만, 현재 라우터에는 해당 경로가 없습니다. 현 상태로는 클릭 시 404가 날 수 있습니다. 아래 두 가지 중 하나로 정합성을 맞춰주세요.

옵션 A) 사용자 라우트를 추가해 기존 내비게이션을 살립니다.

       <Route path="otherfeed/:userId" element={<OtherFeedPage />} />
+      <Route path="user/:userId" element={<OtherFeedPage />} />
       <Route path="follow/:type/:userId" element={<FollowerListPage />} />

옵션 B) 내비게이션 경로를 기존 사용자 상세 라우트로 통일합니다.

  • MemberList: /user/${memberId} -> /otherfeed/${memberId}
  • GroupMembers: 동일 변경

Also applies to: 57-57

🤖 Prompt for AI Agents
In src/pages/index.tsx at lines 39 and 57, the navigation path '/user/:id' used
in MemberList and GroupMembers does not exist in the router, causing potential
404 errors. To fix this, either add a new user route matching '/user/:id' to the
router (Option A) or update the navigation paths in MemberList and GroupMembers
from '/user/${memberId}' to the existing route '/otherfeed/${memberId}' to
maintain consistency (Option B). Choose one approach and apply it consistently
to both locations.


const Router = () => {
const router = createBrowserRouter(
Expand All @@ -53,6 +54,7 @@ const Router = () => {
<Route path="group/search" element={<GroupSearch />} />
<Route path="group/detail" element={<GroupDetail />} />
<Route path="group/detail/joined" element={<ParticipatedGroupDetail />} />
<Route path="group/members" element={<GroupMembers />} />
<Route path="memory" element={<Memory />} />
<Route path="memory/record/write" element={<RecordWrite />} />
<Route path="memory/poll/write" element={<PollWrite />} />
Expand Down