-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 독서메이트 목록 페이지 구현 #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| `; | ||
|
|
||
| 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; | ||
| `; | ||
| 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; |
| 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, | ||
| }, | ||
| ]; |
| 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}; | ||
| `; |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자 페이지 경로 불일치 수정 필요 현재 옵션 B를 채택할 경우 다음과 같이 변경하세요. - navigate(`/user/${memberId}`);
+ navigate(`/otherfeed/${memberId}`);라우트 추가(옵션 A)를 선택한다면 본 변경은 불필요합니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <> | ||||||||||||||||||
| <TitleHeader | ||||||||||||||||||
| leftIcon={<img src={leftArrow} alt="뒤로가기" />} | ||||||||||||||||||
| title="독서메이트" | ||||||||||||||||||
| onLeftClick={handleBackClick} | ||||||||||||||||||
| /> | ||||||||||||||||||
| <Wrapper> | ||||||||||||||||||
| <MemberList members={mockMembers} onMemberClick={handleMemberClick} /> | ||||||||||||||||||
| </Wrapper> | ||||||||||||||||||
| </> | ||||||||||||||||||
| ); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export default GroupMembers; | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 내비게이션 경로 불일치: '/user/:id' 라우트가 없어 클릭 시 404 가능성 MemberList/GroupMembers에서 옵션 A) 사용자 라우트를 추가해 기존 내비게이션을 살립니다. <Route path="otherfeed/:userId" element={<OtherFeedPage />} />
+ <Route path="user/:userId" element={<OtherFeedPage />} />
<Route path="follow/:type/:userId" element={<FollowerListPage />} />옵션 B) 내비게이션 경로를 기존 사용자 상세 라우트로 통일합니다.
Also applies to: 57-57 🤖 Prompt for AI Agents |
||
|
|
||
| const Router = () => { | ||
| const router = createBrowserRouter( | ||
|
|
@@ -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 />} /> | ||
|
|
||
There was a problem hiding this comment.
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=""를 지정하세요.ProfileImage 컴포넌트가 실제로 img 태그로 교체해도 문제가 없는지, 사용 위치와 전달되는 props(src, alt 등)를 확인하기 위해 아래 스크립트를 실행하겠습니다.
🏁 Script executed:
Length of output: 2953
프로필 이미지가 실제로
src와alt같은 props를 받아 렌더링되는지 확인이 필요합니다. 아래 스크립트로MemberList.tsx사용 부분을 살펴보겠습니다.🏁 Script executed:
Length of output: 1382
프로필 URL이 실제로
Member타입에 포함되어 있는지 확인하기 위해src/mocks/members.mock.ts를 조회해 보겠습니다.🏁 Script executed:
Length of output: 1160
프로필 이미지를
<img>로 전환하고src/alt를 전달하세요현재
ProfileImage는 styled.div로 고정된 회색 원형만 렌더링되어 실제 유저 이미지와 대체 텍스트를 제공하지 않습니다. 접근성과 화면 비율 유지를 위해 다음을 적용해주세요:• src/components/members/MemberList.styled.ts
styled.div→styled.imgobject-fit: cover추가• 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