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
2 changes: 1 addition & 1 deletion src/app/message/chat/[roomId]/ChatRoomPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
}

return (
<div className='relative h-[calc(100vh-112px)] overflow-hidden'>
<div className='relative h-[calc(100dvh-112px)] overflow-hidden'>
{/* 채팅 화면 */}
<div
className={`absolute inset-0 flex flex-col transition-transform duration-300 ease-in-out ${
Expand Down
55 changes: 38 additions & 17 deletions src/app/message/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,54 @@ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query

import { API } from '@/api';
import { FollowingContent } from '@/components/pages/message/message-following-content';
import { followKeys } from '@/lib/query-key/query-key-follow';

const INITIAL_PAGE_SIZE = 10;

const getQueryClient = () => {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1분
},
},
});
};

export default async function MessagePage() {
const cookieStore = await cookies();
const accessToken = cookieStore.get('accessToken')?.value || null;
const queryClient = new QueryClient();
const queryClient = getQueryClient();

const me = await API.userService.getMeSkipRedirect();
const userId = me.userId;

// 첫 페이지 우선 prefetch
await queryClient.prefetchInfiniteQuery({
queryKey: ['followers', userId],
queryFn: async () => {
return await API.followerService.getFollowers({
userId,
cursor: undefined,
size: INITIAL_PAGE_SIZE,
});
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => {
return lastPage.nextCursor ?? undefined;
},
pages: 1,
});
await Promise.all([
// 팔로워 목록 prefetch
queryClient.prefetchInfiniteQuery({
queryKey: followKeys.followers(userId),
queryFn: async () => {
return await API.followerService.getFollowers({
userId,
cursor: undefined,
size: INITIAL_PAGE_SIZE,
});
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => {
return lastPage.nextCursor ?? undefined;
},
pages: 1,
}),

// 채팅 목록 prefetch
queryClient.prefetchQuery({
queryKey: ['chatList', userId],
queryFn: async () => {
return await API.chatService.getChatRooms();
},
}),
]);

// dehydrate로 직렬화
const dehydratedState = dehydrate(queryClient);
Expand Down
6 changes: 3 additions & 3 deletions src/components/pages/chat/chat-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export const ChatList = ({ userId, accessToken }: IProps) => {
className='flex cursor-pointer items-center gap-3 bg-white p-5 transition hover:bg-gray-50'
onClick={() => handleClick(chat.chatRoomId)}
>
{/* 프로필 이미지 - 이미지 수정 필요💥💥*/}
<ProfileImage size='md' src={chat.thumbnail} />
{/* 프로필 이미지 */}
<ProfileImage fetchPriority='high' size='md' src={chat.thumbnail} />

{/* 텍스트 영역 */}
<div className='flex flex-1 flex-col'>
Expand All @@ -57,7 +57,7 @@ export const ChatList = ({ userId, accessToken }: IProps) => {
'text-text-sm-medium line-clamp-1 overflow-hidden break-all text-gray-700',
)}
>
{chat.lastMessage ? chat.lastMessage.content : '아직 대화가 없습니다.'}
{chat.lastMessage.content}
</span>
</div>

Expand Down
95 changes: 58 additions & 37 deletions src/components/pages/chat/chat-user-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,66 @@ import { useGetParticipants } from '@/hooks/use-chat';

import { UserOutModal } from './UserOutModal';

interface UserProps {
user: {
userId: number;
nickName: string;
profileImage: string;
profileMessage?: string;
isOwner: boolean;
};
roomId: number;
roomType: 'DM' | 'GROUP';
isManaging: boolean;
index: number;
}

interface UserListProps {
onClose: () => void;
roomId: number;
roomType: 'DM' | 'GROUP';
userId: number;
}

const User = ({ user, roomId, roomType, isManaging, index }: UserProps) => {
const { open } = useModal();

return (
<div className='bg-mono-white flex h-22 items-center gap-4 p-5'>
<div className='h-12 w-12 overflow-hidden rounded-full'>
<ProfileImage size='md' src={user.profileImage} />
</div>

<div className='flex-1'>
<div className='text-text-md-bold text-gray-800'>{user.nickName}</div>
<div className='text-text-sm-medium line-clamp-1 text-gray-600'>
{user.profileMessage || '상태 메시지가 없습니다.'}
</div>
</div>

{roomType === 'GROUP' && user.isOwner && (
<span className='bg-mint-100 text-mint-700 text-text-xs-medium rounded-full px-2.5 py-1'>
방장
</span>
)}

{isManaging && index !== 0 && (
<button
className='bg-error-500 flex h-5 w-5 items-center justify-center rounded-full'
onClick={(e) => {
e.stopPropagation();
open(<UserOutModal nickName={user.nickName} roomId={roomId} userId={user.userId} />);
}}
>
<div className='bg-mono-white h-0.5 w-2.5' />
</button>
)}
</div>
);
};

export const UserList = ({ onClose, roomId, roomType, userId }: UserListProps) => {
const [isManaging, setIsManaging] = useState(false);
const { open } = useModal();
const { data } = useGetParticipants(roomId);

const isCurrentUserOwner = data?.participants.some(
Expand Down Expand Up @@ -60,42 +110,13 @@ export const UserList = ({ onClose, roomId, roomType, userId }: UserListProps) =
<div className='scrollbar-thin flex-1 overflow-y-auto'>
{sortedParticipants.map((user, index) => (
<div key={user.userId}>
<div className='bg-mono-white flex h-22 items-center gap-4 p-5'>
<div className='h-12 w-12 overflow-hidden rounded-full'>
<ProfileImage size='md' src={user.profileImage} />
</div>

<div className='flex-1'>
<div className='text-text-md-bold text-gray-800'>{user.nickName}</div>
<div className='text-text-sm-medium line-clamp-1 text-gray-600'>
{user.profileMessage || '상태 메시지가 없습니다.'}
</div>
</div>

{roomType === 'GROUP' && user.isOwner ? (
<span className='bg-mint-100 text-mint-700 text-text-xs-medium rounded-full px-2.5 py-1'>
방장
</span>
) : null}

{isManaging && index !== 0 && (
<button
className='bg-error-500 flex h-5 w-5 items-center justify-center rounded-full'
onClick={(e) => {
e.stopPropagation();
open(
<UserOutModal
nickName={user.nickName}
roomId={roomId}
userId={user.userId}
/>,
);
}}
>
<div className='bg-mono-white h-0.5 w-2.5' />
</button>
)}
</div>
<User
index={index}
isManaging={isManaging}
roomId={roomId}
roomType={roomType}
user={user}
/>
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const FollowingCardTable: Story = {
nickname: '',
profileImage: '',
profileMessage: '',
type: 'following',
},
render: () => (
<table className='w-full border-collapse'>
Expand All @@ -49,11 +48,7 @@ export const FollowingCardTable: Story = {
<td className='p-4 align-top text-sm font-medium text-gray-900'>Following</td>
<td className='p-4 align-top text-sm text-gray-600'>기본 팔로잉 카드</td>
<td className='p-4'>
<FollowingCard
{...baseArgs}
profileMessage='안녕하세요! 반갑습니다 😊'
type='following'
/>
<FollowingCard {...baseArgs} profileMessage='안녕하세요! 반갑습니다 😊' />
</td>
</tr>
<tr className='border-b'>
Expand All @@ -65,56 +60,35 @@ export const FollowingCardTable: Story = {
<FollowingCard
{...baseArgs}
profileMessage='안녕하세요 안녕하세요 안녕하세요 안녕하세요 안녕하세요 안녕하세요 안녕하세요 안녕하세요'
type='following'
/>
</td>
</tr>
<tr className='border-b'>
<td className='p-4 align-top text-sm font-medium text-gray-900'>Message</td>
<td className='p-4 align-top text-sm text-gray-600'>읽지 않은 메시지 없음</td>
<td className='p-4'>
<FollowingCard
{...baseArgs}
count={0}
profileMessage='알림 0개 테스트'
type='message'
/>
<FollowingCard {...baseArgs} profileMessage='알림 0개 테스트' />
</td>
</tr>
<tr className='border-b'>
<td className='p-4 align-top text-sm font-medium text-gray-900'>Message</td>
<td className='p-4 align-top text-sm text-gray-600'>읽지 않은 메시지 1개</td>
<td className='p-4'>
<FollowingCard
{...baseArgs}
count={1}
profileMessage='알림 1개 테스트'
type='message'
/>
<FollowingCard {...baseArgs} profileMessage='알림 1개 테스트' />
</td>
</tr>
<tr className='border-b'>
<td className='p-4 align-top text-sm font-medium text-gray-900'>Message</td>
<td className='p-4 align-top text-sm text-gray-600'>읽지 않은 메시지 10개</td>
<td className='p-4'>
<FollowingCard
{...baseArgs}
count={10}
profileMessage='알림 10개 테스트'
type='message'
/>
<FollowingCard {...baseArgs} profileMessage='알림 10개 테스트' />
</td>
</tr>
<tr className='border-b'>
<td className='p-4 align-top text-sm font-medium text-gray-900'>Message</td>
<td className='p-4 align-top text-sm text-gray-600'>읽지 않은 메시지 99개 이상</td>
<td className='p-4'>
<FollowingCard
{...baseArgs}
count={100}
profileMessage='알림 100개 테스트'
type='message'
/>
<FollowingCard {...baseArgs} profileMessage='알림 100개 테스트' />
</td>
</tr>
</tbody>
Expand Down
32 changes: 4 additions & 28 deletions src/components/pages/message/message-following-card/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,46 +37,22 @@ describe('FollowingCard 컴포넌트 테스트', () => {
});
});

test('type=following 일 때 테스트', () => {
render(<FollowingCard {...defaultProps} type='following' />);
test('렌더링 테스트', () => {
render(<FollowingCard {...defaultProps} />);

expect(screen.getByText('메세지')).toBeInTheDocument();
});

test('type=message & count > 0 일 때 테스트', () => {
render(<FollowingCard {...defaultProps} count={5} type='message' />);

const badge = screen.getByText('5');

expect(badge).toBeInTheDocument();
expect(badge).not.toHaveClass('opacity-0');
});

test('type=message & count = 0 일 때 테스트', () => {
render(<FollowingCard {...defaultProps} count={0} type='message' />);

const badge = screen.getByText('0');

expect(badge).toBeInTheDocument();
expect(badge).toHaveClass('opacity-0');
});

test('count > 99 인 경우 "99+" 를 보여주는지 테스트.', () => {
render(<FollowingCard {...defaultProps} count={100} type='message' />);

expect(screen.getByText('99+')).toBeInTheDocument();
});

test('팔로잉 카드 클릭 시 router.push() 호출되는지 테스트.', () => {
render(<FollowingCard {...defaultProps} type='following' />);
render(<FollowingCard {...defaultProps} />);

fireEvent.click(screen.getByTestId('following-card'));

expect(routerPush).toHaveBeenCalledWith('/profile/0');
});

test('메시지 버튼 클릭 시 DM 생성 후 채팅방으로 이동되는지 테스트.', async () => {
render(<FollowingCard {...defaultProps} type='following' />);
render(<FollowingCard {...defaultProps} />);

fireEvent.click(screen.getByText('메세지'));

Expand Down
Loading