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
15 changes: 11 additions & 4 deletions src/api/service/follower-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { api } from '@/api/core';
import { Follower } from '@/types/service/follow';
import { GetFollowerResponse } from '@/types/service/follow';
import { FollowPathParams } from '@/types/service/user';

export const followerServiceRemote = () => ({
// 팔로워 목록 조회
// 임시주소로 작성. 나중에 수정 필요.
getFollowers: async () => {
return api.get<Follower[]>('/followers');
getFollowers: async ({ userId }: { userId: number }) => {
return api.get<GetFollowerResponse>(`/users/${userId}/follow`);
},

// 팔로워 등록
addFollower: async (params: FollowPathParams) => {
return api.post<string>(`/users/follow`, null, {
params: { followNickname: params.followNickname },
});
},
});
16 changes: 16 additions & 0 deletions src/app/message/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { redirect } from 'next/navigation';

interface Props {
children: React.ReactNode;
}

const FollowPageLayout = async ({ children }: Props) => {
const { cookies } = await import('next/headers');
const cookieStore = await cookies();
const myId = Number(cookieStore.get('userId')?.value);

if (!myId) redirect('/login');
return <>{children}</>;
};

export default FollowPageLayout;
27 changes: 22 additions & 5 deletions src/app/message/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { useSearchParams } from 'next/navigation';

import { Chat, FollowingList, FollowingSearch } from '@/components/pages/message';
import { useEffect, useState } from 'react';

import Cookies from 'js-cookie';

import { Chat, FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message';
import { TabNavigation } from '@/components/shared';
import { useGetFollowers } from '@/hooks/use-follower/use-follower-get';

Expand All @@ -12,20 +16,33 @@ const SOCIAL_TABS = [
];

export default function FollowingPage() {
const { data: followers } = useGetFollowers();
const [userId, setUserId] = useState(0);
const { data: followers } = useGetFollowers({ userId }, { enabled: !!userId });

useEffect(() => {
const id = Cookies.get('userId');
// eslint-disable-next-line react-hooks/set-state-in-effect
setUserId(Number(id));
}, []);
console.log(followers);
const params = useSearchParams();
const tab = params.get('tab') || 'following';
if (!followers) return null;
return (
<div className='min-h-screen bg-[#F1F5F9]'>
<TabNavigation basePath='/message' tabs={SOCIAL_TABS} />

{tab === 'chat' && <Chat />}
{tab === 'following' && (
<>
<FollowingSearch />
<FollowingList items={followers} />
<FollowingSearch userId={userId} />

{followers && followers.items.length > 0 ? (
<FollowingList items={followers} />
) : (
<div className='flex flex-1 items-center justify-center'>
<FollowingNone />
</div>
)}
</>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/pages/message/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Chat } from './chat';
export { FollowingCard } from './message-following-card';
export { FollowingList } from './message-following-list';
export { FollowingNone } from './message-following-none';
export { FollowingSearch } from './message-following-search';
21 changes: 13 additions & 8 deletions src/components/pages/message/message-following-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Image from 'next/image';
import { useRouter } from 'next/navigation';

import { ImageWithFallback } from '@/components/ui';
import { cn } from '@/lib/utils';

interface FollowingCardProps {
Expand Down Expand Up @@ -32,13 +32,18 @@ export const FollowingCard = ({
className='flex cursor-pointer items-center gap-3 bg-white p-5'
onClick={handleClick}
>
<Image
width={48}
className='size-12 rounded-full object-cover'
alt={nickname}
height={48}
src={profileImage}
/>
{/* <div className='size-12 rounded-full'>
<ImageWithFallback className='object-cover' alt='프로필 이미지' fill src={profileImage} />
</div> */}
<div className='relative size-12 overflow-hidden rounded-full'>
<ImageWithFallback
className='object-cover'
alt='프로필 이미지'
fill
loading='eager'
src={profileImage}
/>
</div>
<div className='flex flex-1 flex-col'>
<span className='text-text-md-bold text-gray-800'>{nickname}</span>
<span
Expand Down
52 changes: 30 additions & 22 deletions src/components/pages/message/message-following-list/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,37 @@ import { useRouter } from 'next/navigation';

import { render, screen } from '@testing-library/react';

import { GetFollowerResponse } from '@/types/service/follow';

import { FollowingList } from '.';

const TEST_ITEMS = [
{
userId: 0,
nickname: '신짱구',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 신짱구입니다',
},
{
userId: 1,
nickname: '김맹구',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 김맹구입니다',
},

{
userId: 2,
nickname: '흰둥이',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 흰둥이입니다',
},
];
const TEST_ITEMS: GetFollowerResponse = {
items: [
{
followId: 0,
userId: 0,
nickname: '신짱구',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 신짱구입니다',
},
{
followId: 1,
userId: 1,
nickname: '김맹구',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 김맹구입니다',
},
{
followId: 2,
userId: 2,
nickname: '흰둥이',
profileImage: 'http://test.com',
profileMessage: '안녕하세요 흰둥이입니다',
},
],
nextCursor: null,
};

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}));
Expand All @@ -41,7 +49,7 @@ describe('Following List 컴포넌트 테스트', () => {
test('모든 아이템이 렌더링 되는지 테스트', () => {
render(<FollowingList items={TEST_ITEMS} />);

TEST_ITEMS.forEach((item) => {
TEST_ITEMS.items.forEach((item) => {
expect(screen.getByText(item.nickname)).toBeInTheDocument();
});
});
Expand Down
6 changes: 3 additions & 3 deletions src/components/pages/message/message-following-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
'use client';

import { Follower } from '@/types/service/follow';
import { GetFollowerResponse } from '@/types/service/follow';

import { FollowingCard } from '../message-following-card';

interface FollowingListProps {
items: Follower[];
items: GetFollowerResponse;
}

export const FollowingList = ({ items }: FollowingListProps) => {
return (
<div>
{items.map((item) => (
{items.items.map((item) => (
<FollowingCard
key={item.userId}
nickname={item.nickname}
Expand Down
3 changes: 3 additions & 0 deletions src/components/pages/message/message-following-none/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FollowingNone = () => {
return <div className='text-gray-600'>아직 팔로우 한 사람이 없어요.</div>;
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';

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

import { FollowingSearch } from '.';

const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});

describe('Following Search 테스트', () => {
const queryClient = createQueryClient();
test('Following Search 렌더링 테스트', () => {
render(
<ModalProvider>
<FollowingSearch />
</ModalProvider>,
<QueryClientProvider client={queryClient}>
<ModalProvider>
<FollowingSearch userId={0} />
</ModalProvider>
</QueryClientProvider>,
);

expect(screen.getByText('팔로우 추가')).toBeInTheDocument();
});

test('팔로우 추가 클릭 시 모달 생성', () => {
render(
<ModalProvider>
<FollowingSearch />
</ModalProvider>,
<QueryClientProvider client={queryClient}>
<ModalProvider>
<FollowingSearch userId={0} />
</ModalProvider>
</QueryClientProvider>,
);

expect(screen.queryByText('팔로우 할 닉네임을 입력하세요')).toBeNull();
Expand Down
20 changes: 15 additions & 5 deletions src/components/pages/message/message-following-search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
'use client';
import { useState } from 'react';

import { Icon } from '@/components/icon';
import { Button, Input, ModalContent, ModalTitle, useModal } from '@/components/ui';
import { useAddFollowers } from '@/hooks/use-follower';

const FollowerModal = () => {
const FollowerModal = ({ userId }: { userId: number }) => {
const { close } = useModal();
const [nickname, setNickname] = useState('');
const { mutate: addFollower } = useAddFollowers({ userId });

const handleConfirm = () => {
// 유저 팔로우 기능 ...
console.log('팔로우 성공');
addFollower({ followNickname: nickname });
close();
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNickname(value);
};

// 모달 모양 바뀌면 적용하기!
return (
<ModalContent>
Expand All @@ -19,6 +28,7 @@ const FollowerModal = () => {
className='text-text-sm-medium mb-3 w-full rounded-3xl bg-gray-100 px-4 py-2.5 text-gray-800'
iconButton={<Icon id='search' className='absolute top-2.5 right-3 size-5 text-gray-500' />}
placeholder='nickname'
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleConfirm();
Expand All @@ -38,12 +48,12 @@ const FollowerModal = () => {
);
};

export const FollowingSearch = () => {
export const FollowingSearch = ({ userId }: { userId: number }) => {
const { open } = useModal();
return (
<div
className='flex items-center gap-5 px-5 py-4 transition-all hover:cursor-pointer hover:opacity-80'
onClick={() => open(<FollowerModal />)}
onClick={() => open(<FollowerModal userId={userId} />)}
>
<div className='rounded-full border-2 border-dashed border-gray-400 bg-gray-100 p-2'>
<Icon id='plus' className='size-6 text-gray-700' />
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/use-follower/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useAddFollowers } from './use-follower-add/index';
export { useGetFollowers } from './use-follower-get/index';
23 changes: 23 additions & 0 deletions src/hooks/use-follower/use-follower-add/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { API } from '@/api';
import { AddFollowParams } from '@/types/service/follow';

export const useAddFollowers = (
{ userId }: { userId: number },
options?: { enabled?: boolean },
) => {
const queryClient = useQueryClient();
const query = useMutation({
mutationFn: (params: AddFollowParams) => API.followerService.addFollower(params),
...options,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['followers', userId] });
console.log('팔로워 추가 성공');
},
onError: () => {
console.log('팔로워 추가 실패');
},
});
return query;
};
10 changes: 7 additions & 3 deletions src/hooks/use-follower/use-follower-get/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { useQuery } from '@tanstack/react-query';

import { API } from '@/api';

export const useGetFollowers = () => {
export const useGetFollowers = (
{ userId }: { userId: number },
options?: { enabled?: boolean },
) => {
const query = useQuery({
queryKey: ['followers'],
queryFn: () => API.followerService.getFollowers(),
queryKey: ['followers', userId],
...options,
queryFn: () => API.followerService.getFollowers({ userId }),
});
return query;
};
2 changes: 1 addition & 1 deletion src/mock/service/followers/followers-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { http, HttpResponse } from 'msw';
import { createMockErrorResponse, createMockSuccessResponse } from '../common/common-mock';
import { mockFollowingItems } from './followers-mocks';

const getFollowersMock = http.get('*/followers', () => {
const getFollowersMock = http.get(`*/users/:userId/follow`, () => {
if (!mockFollowingItems) {
return HttpResponse.json(
createMockErrorResponse({
Expand Down
13 changes: 13 additions & 0 deletions src/types/service/follow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
// 기본 팔로우 타입
export interface Follower {
followId: number;
userId: number;
nickname: string;
profileImage: string;
profileMessage: string;
}

// 팔로우 목록 조회 응답
export interface GetFollowerResponse {
items: Follower[];
nextCursor: number | null;
}

// 팔로우 등록 Parameters
export interface AddFollowParams {
followNickname: string;
}