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
27 changes: 27 additions & 0 deletions src/app/group/[groupId]/pending/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { use } from 'react';

import {
GroupPendingHeader,
GroupPendingMembers,
GroupPendingSummary,
} from '@/components/pages/group';

interface Props {
params: Promise<{ groupId: string }>;
}

const GroupPendingMembersPage = ({ params }: Props) => {
const { groupId } = use(params);

return (
<>
<GroupPendingHeader />
<GroupPendingSummary />
<GroupPendingMembers groupId={groupId} />
</>
);
};

export default GroupPendingMembersPage;
26 changes: 26 additions & 0 deletions src/components/pages/group/group-pending-header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';

export const GroupPendingHeader = () => {
const router = useRouter();

const handleBackClick = () => {
router.back();
};

return (
<div className='sticky top-14 z-100 flex h-12 items-center justify-center border-b border-gray-200 bg-white'>
<button
className='absolute left-5 flex items-center justify-center'
aria-label='뒤로 가기'
onClick={handleBackClick}
>
<Icon id='chevron-left-2' className='text-gray-500' />
</button>
<h2 className='text-text-md-bold text-gray-800'>참여 신청</h2>
</div>
);
};
83 changes: 83 additions & 0 deletions src/components/pages/group/group-pending-members/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import { useCallback } from 'react';

import { EmptyState } from '@/components/layout/empty-state';
import { GetPendingMembersResponse } from '@/types/service/group';

import { PendingMemberCard } from './pending-member-card';

interface Props {
groupId: string;
pendingMembers?: GetPendingMembersResponse['pendingMembers'];
}

const MOCK_PENDING_MEMBERS: GetPendingMembersResponse['pendingMembers'] = [
{
userId: 101,
groupUserId: 1,
nickName: 'Hope Lee',
profileImage: null,
profileMessage: 'Here We Go Again :)',
requestMessage: '안녕하세요 함께하고 싶습니다!',
requestedAt: '2025-12-24T13:20:10.123456',
},
{
userId: 102,
groupUserId: 2,
nickName: '바다소년',
profileImage: null,
profileMessage: '물고기를 좋아합니다.',
requestMessage: '보드게임 정말 좋아해서 신청합니다! 앞으로 잘 부탁드려요',
requestedAt: '2025-12-24T13:25:15.789012',
},
{
userId: 103,
groupUserId: 3,
nickName: '박디자인',
profileImage: null,
profileMessage: 'UI/UX 디자이너',
requestMessage: null,
requestedAt: '2025-12-24T13:30:20.456789',
},
];

export const GroupPendingMembers = ({ groupId, pendingMembers = MOCK_PENDING_MEMBERS }: Props) => {
const handleReject = useCallback(
(memberId: number) => {
console.log('거절:', groupId, memberId);
},
[groupId],
);

const handleApprove = useCallback(
(memberId: number) => {
console.log('승인:', groupId, memberId);
},
[groupId],
);

if (pendingMembers?.length === 0) {
return (
<section className='relative h-[calc(100vh-192px)]'>
<EmptyState>승인 대기 중인 멤버가 없습니다</EmptyState>
</section>
);
}

return (
<section className='mt-5 px-4 pb-5'>
<ul className='space-y-4'>
{pendingMembers.map((member) => (
<li key={member.groupUserId}>
<PendingMemberCard
member={member}
onApprove={() => handleApprove(member.groupUserId)}
onReject={() => handleReject(member.groupUserId)}
/>
</li>
))}
</ul>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';

import Link from 'next/link';

import { Button, ImageWithFallback } from '@/components/ui';
import { GetPendingMembersResponse } from '@/types/service/group';

type PendingMember = GetPendingMembersResponse['pendingMembers'][number];

interface Props {
member: PendingMember;
onReject: () => void;
onApprove: () => void;
}

export const PendingMemberCard = ({ member, onReject, onApprove }: Props) => {
const profileUrl = `/profile/${member.userId}`;

return (
<div className='bg-mono-white rounded-3xl px-5 py-[26px] shadow-sm'>
<Link href={profileUrl} className='flex gap-3'>
<ImageWithFallback
width={40}
className='object-fit h-10 w-10 shrink-0 rounded-full'
alt={`${member.nickName} 프로필`}
draggable={false}
height={40}
src={member.profileImage ?? ''}
/>

<div className='min-w-0 flex-1'>
<h4 className='text-text-md-semibold h-6 text-gray-800'>{member.nickName}</h4>
{member.profileMessage && (
<p className='text-text-xs-regular h-[18px] text-gray-600'>{member.profileMessage}</p>
)}
</div>
</Link>

{member.requestMessage && (
<p className='text-text-md-medium mt-4 line-clamp-2 max-h-12 min-h-6 text-gray-600'>
{member.requestMessage}
</p>
)}

<div className='mt-4 flex gap-2'>
<Button className='flex-1' size='sm' variant='tertiary' onClick={onReject}>
거절하기
</Button>
<Button
className='bg-mint-500 text-text-sm-bold text-mono-white hover:bg-mint-600 active:bg-mint-700 flex-1'
size='sm'
variant='primary'
onClick={onApprove}
>
수락하기
</Button>
</div>
</div>
);
};
49 changes: 49 additions & 0 deletions src/components/pages/group/group-pending-summary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import { DEFAULT_GROUP_IMAGE } from 'constants/default-images';

import { ImageWithFallback } from '@/components/ui';

interface Props {
thumbnail?: string | null;
title?: string;
pendingCount?: number;
}

const MOCK_DATA = {
thumbnail: null,
title: '매우 긴 제목을 가진 모임입니다 이 제목은 너무 길어서 잘려야 합니다',
pendingCount: 5,
};

export const GroupPendingSummary = ({
thumbnail = MOCK_DATA.thumbnail,
title = MOCK_DATA.title,
pendingCount = MOCK_DATA.pendingCount,
}: Props) => {
return (
<div className='flex h-[88px] items-center gap-3 border-b border-gray-200 bg-white p-4'>
<div className='relative h-14 w-14 shrink-0 overflow-hidden rounded-[10px] bg-gray-200'>
<ImageWithFallback
width={56}
className='h-full w-full object-cover'
alt={title || '모임 썸네일'}
fallbackSrc={DEFAULT_GROUP_IMAGE}
height={56}
src={thumbnail ?? ''}
unoptimized
/>
</div>

<div className='flex min-w-0 flex-1 flex-col justify-between'>
<h3 className='text-text-md-semibold mt-[5px] h-6 truncate text-gray-800'>{title}</h3>

<div className='text-text-sm-medium mb-[5px] flex h-5 items-center'>
<span className='text-gray-600'>신청한 유저</span>
<span className='text-mint-600 ml-1'>{pendingCount}</span>
<span className='text-gray-600'>명</span>
</div>
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions src/components/pages/group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export { GroupBannerImages } from './group-banner-images';
export { GroupButtons } from './group-buttons';
export { GroupDescriptions } from './group-descriptions';
export { GroupMembers } from './group-members';
export { GroupPendingHeader } from './group-pending-header';
export { GroupPendingMembers } from './group-pending-members';
export { GroupPendingSummary } from './group-pending-summary';
28 changes: 0 additions & 28 deletions src/mock/service/group/group-mock.ts

This file was deleted.

46 changes: 17 additions & 29 deletions src/types/service/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,35 +84,6 @@ export interface GetMyGroupsResponse {
nextCursor: number | null;
}

/**

* 밑에 타입들은 다른 분들이 아직 다른 파일에서 사용 중이므로 그냥 제거해버리시면 안됩니다(다른 분들도 다 수정 후에 제거 예정)
* 아직 사용 중인 파일:
* - src/types/service/notification.ts (Notification.group)
* - src/mock/service/group/group-mock.ts (groupMockItem)
*/
export interface Group {
id: number;
title: string;
location: string;
locationDetail: string;
startTime: string;
endTime: string;
images: string[];
tags: string[];
description: string;
participantCount: number;
maxParticipants: number;
createdBy: {
userId: number;
nickName: string;
profileImage: null | string;
};
createdAt: string;
updatedAt: string;
joinedCount: number;
}

export interface PreUploadGroupImagePayload {
images: File[];
}
Expand Down Expand Up @@ -185,6 +156,7 @@ export interface CreateGroupResponse {
export interface GetGroupDetailsResponse {
id: number;
title: string;
joinPolicy: GroupV2JoinPolicy;
status: GroupV2Status;
address: {
location: string;
Expand Down Expand Up @@ -233,3 +205,19 @@ export interface GetGroupDetailsResponse {
export interface GroupIdParams {
groupId: string;
}

// 승인 대기자 목록 조회 응답 (GET /api/v2/groups/{groupId}/attendance/pending)
export interface GetPendingMembersResponse {
groupId: number;
joinPolicy: GroupV2JoinPolicy;
pendingMembers: {
userId: number;
groupUserId: number;
nickName: string;
profileImage: string | null;
profileMessage: string | null;
requestMessage: string | null;
requestedAt: string;
}[];
serverTime: string;
}