Skip to content
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

[feat] 오픈 프로필 목록 페이지 구현 및 테스트 코드 작성 #53

Merged
merged 4 commits into from
Aug 21, 2023
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
31 changes: 20 additions & 11 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { apiService } from '../../services/ApiService';

import useLoginUserStore from '../../hooks/useLoginUserStore';

import { STATIC_ROUTES } from '../../constants/routes';

import profileIcon from '../../assets/image/icon/profile-icon.png';
import profileListIcon from '../../assets/image/icon/profile-list-icon.png';
import chatListIcon from '../../assets/image/icon/chat-list-icon.png';
import accountIcon from '../../assets/image/icon/account-icon.png';
import logoutIcon from '../../assets/image/icon/logout-icon.png';

export default function Header() {
export default function Header({ userType } : {userType: string}) {
const [, store] = useLoginUserStore();

const handleClickLogout = async () => {
Expand All @@ -24,26 +26,28 @@ export default function Header() {
};

return (
<Container>
<Container userType={userType}>
<h2>CHATCHAT</h2>
<div>
<nav>
<NavLink to="/profile">
<NavLink to={STATIC_ROUTES.MY_PROFILE}>
<img src={profileIcon} alt="" />
<span>내 프로필</span>
</NavLink>
<NavLink to="/companies">
<img src={profileListIcon} alt="" />
<span>오픈 프로필 목록</span>
</NavLink>
<NavLink to="/chatrooms">
{userType === 'customer' && (
<NavLink to={STATIC_ROUTES.OPEN_PROFILES}>
<img src={profileListIcon} alt="" />
<span>오픈 프로필 목록</span>
</NavLink>
)}
<NavLink to={STATIC_ROUTES.CHATROOMS}>
<img src={chatListIcon} alt="" />
<span>채팅 목록</span>
</NavLink>
</nav>

<nav>
<NavLink to="/account">
<NavLink to={STATIC_ROUTES.ACCOUNT}>
<img src={accountIcon} alt="" />
<span>계정 관리</span>
</NavLink>
Expand All @@ -60,7 +64,7 @@ export default function Header() {
);
}

const Container = styled.div`
const Container = styled.div<{userType: string}>`
position: absolute;
left: 0;
z-index: 10;
Expand Down Expand Up @@ -145,8 +149,13 @@ const Container = styled.div`
}
}

nav:nth-child(1) { flex-grow : 3 };
${(props) => (props.userType === 'customer'
? 'nav:nth-child(1) { flex-grow : 3 };'
: 'nav:nth-child(1) { flex-grow : 2 };'
)}

nav:nth-child(2) { flex-grow : 2 };

}

a.active{
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/PostLoginLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function PostLoginLayout() {
return (
<Container>
<Outlet />
<Header />
<Header userType={userType} />
</Container>
);
}
Expand Down
58 changes: 58 additions & 0 deletions src/components/profile/OpenProfileList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { screen } from '@testing-library/react';

import fixtures from '../../../fixtures';

import { render } from '../../test-helper';

import OpenProfileList from './OpenProfileList';

const context = describe;

const mockQueryData = {
isLoading: false,
data: {
pages: [{
companySummaries: fixtures.companies,
page: fixtures.page,
}],
},
};

jest.mock('../../hooks/useOpenProfileInfiniteQuery', () => () => mockQueryData);

jest.mock('react-intersection-observer', () => ({
useInView: () => [],
}));

describe('<OpenProfileList />', () => {
it('render open profile list', () => {
render(<OpenProfileList />);

screen.getByText(/기업명1/);
screen.getByText(/기업 설명2/);
});

context('empty chat list', () => {
beforeEach(() => {
mockQueryData.data.pages[0].companySummaries = [];
});

it('render empty message', () => {
render(<OpenProfileList />);

screen.getByText(/등록된 오픈 프로필이 없습니다./);
});
});

context('when loading', () => {
beforeEach(() => {
mockQueryData.isLoading = true;
});

it('render loading message', () => {
render(<OpenProfileList />);

screen.getByText(/Loading/);
});
});
});
46 changes: 46 additions & 0 deletions src/components/profile/OpenProfileList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import useOpenProfileInfiniteQuery from '../../hooks/useOpenProfileInfiniteQuery';

import { Profile } from '../../types';

import OpenProfileListRow from './OpenProfileListRow';

export default function OpenProfileList() {
const {
ref, isLoading, data, error,
} = useOpenProfileInfiniteQuery();

// TODO : Error Page로 이동하기
if (error) {
return <p>데이터를 불러올 수 없습니다.</p>;
}

// TODO : 로딩화면 스켈레톤 적용
if (isLoading) {
return <p>Loading...</p>;
}

const { pages } = data;

if (pages[0].companySummaries.length === 0) {
return <p>등록된 오픈 프로필이 없습니다.</p>;
}

const handleClickProfile = () => {
// ...
};

return (
<div>
<ul>
{pages.map((page) => page.companySummaries.map((openProfile : Profile) => (
<OpenProfileListRow
key={openProfile.id}
openProfile={openProfile}
handleClickProfile={handleClickProfile}
/>
)))}
</ul>
<div ref={ref} />
</div>
);
}
22 changes: 22 additions & 0 deletions src/components/profile/OpenProfileListRow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { screen } from '@testing-library/react';

import fixtures from '../../../fixtures';

import { render } from '../../test-helper';

import OpenProfileListRow from './OpenProfileListRow';

const handleClickProfile = jest.fn();

describe('<OpenProfileListRow />', () => {
const openProfile = fixtures.companies[0];

it('render open profile', () => {
render(<OpenProfileListRow
openProfile={openProfile}
handleClickProfile={handleClickProfile}
/>);

screen.getByText(/기업명1/);
});
});
80 changes: 80 additions & 0 deletions src/components/profile/OpenProfileListRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import styled from 'styled-components';

import { Profile } from '../../types';

import ProfileImage from '../ui/ProfileImage';

interface OpenProfileListRowProps {
openProfile: Profile;
handleClickProfile : (id: number) => void;
}

export default function OpenProfileListRow({
openProfile, handleClickProfile,
}: OpenProfileListRowProps) {
return (
<Container onClick={() => handleClickProfile(openProfile.id)}>
<div>
<ProfileImage src={openProfile.imageUrl} />
</div>
<div>
<b>{openProfile.name}</b>
<p>{openProfile.description}</p>
</div>
</Container>
);
}

const Container = styled.div`
width: 100%;
display: flex;
align-items: center;
padding-block: 1.4rem;
cursor: pointer;

> div:nth-child(1){
min-width: 10rem;
max-width: 10rem;
height: 10rem;
margin-right: 2rem;
}

> div:nth-child(2){
flex-grow: 1;

b {
${(props) => props.theme.texts.bold.title}
}

p {
${(props) => props.theme.texts.regular.medium}
color: ${(props) => props.theme.colors.gray1.default};
margin-top: .4rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}

@media screen and (${(props) => props.theme.breakPoint.mobile}) {
padding-block: 1rem;

> div:nth-child(1) {
min-width: 6.4rem;
max-width: 6.4rem;
height: 6.4rem;
margin-right: 1.4rem;
}

> div:nth-child(2){
b {
${(props) => props.theme.texts.bold.boldText}
}

p {
${(props) => props.theme.texts.regular.small}
}
}
}
`;
3 changes: 2 additions & 1 deletion src/constants/reactQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-disable-next-line import/prefer-default-export
export const QUERY_KEY = {
CHAT_LIST: ['chatList'],
CHAT_LIST: ['chat-list'],
AUTO_REPLY_ADMIN_LIST: ['auto-reply-admin'],
OPEN_PROFILE_LIST: ['open-profile-list'],
};
45 changes: 45 additions & 0 deletions src/hooks/useOpenProfileInfiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from 'react';

import { useInfiniteQuery } from '@tanstack/react-query';

import { useInView } from 'react-intersection-observer';

import { QUERY_KEY } from '../constants/reactQuery';

import { apiService } from '../services/ApiService';

const useOpenProfileInfiniteQuery = () => {
const [ref, inView] = useInView();

const {
isLoading, data, isPreviousData, fetchNextPage, error,
} = useInfiniteQuery(
QUERY_KEY.OPEN_PROFILE_LIST,
({ pageParam = 1 }) => apiService.fetchOpenProfiles({ page: pageParam }),
{
getNextPageParam: (lastPage) => {
const cur = lastPage.page.current;
if (lastPage.page.total > cur) {
return cur + 1;
}
return undefined;
},
cacheTime: 0,
},
);

useEffect(() => {
if (!isPreviousData && inView) {
fetchNextPage();
}
}, [inView]);

return {
ref,
isLoading,
data: data ?? { pages: [] },
error,
};
};

export default useOpenProfileInfiniteQuery;
4 changes: 2 additions & 2 deletions src/mocks/handlers/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,13 @@ const customerHandlers = [

return res(
ctx.status(200),
ctx.json({ companies: filteredCompanies, page }),
ctx.json({ companySummaries: filteredCompanies, page }),
);
}

return res(
ctx.status(200),
ctx.json({ companies: sliceCompanies, page }),
ctx.json({ companySummaries: sliceCompanies, page }),
);
}),
rest.get(`${BASE_URL}/companies/:id`, (req, res, ctx) => {
Expand Down
25 changes: 25 additions & 0 deletions src/pages/OpenProfileListPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';

import useLoginUserStore from '../hooks/useLoginUserStore';

import ContentLayout from '../components/layout/ContentLayout';
import OpenProfileList from '../components/profile/OpenProfileList';

export default function OpenProfileListPage() {
const [{ userType }] = useLoginUserStore();

useEffect(() => {
if (userType !== 'customer') {
// TODO : Error Page로 이동하기
}
}, []);

return (
<ContentLayout
pageHeader="오픈 프로필 목록"
testId="open-profile-list"
>
<OpenProfileList />
</ContentLayout>
);
}
Loading
Loading