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

[SP2] 블로그 페이지 스켈레톤 UI, scrollToTop, react-query #251

Merged
merged 17 commits into from
Nov 6, 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
10 changes: 5 additions & 5 deletions src/lib/api/remote/review.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { BASE_URL, DEFAULT_TIMEOUT } from '@src/lib/constants/client';
import { PartCategoryType } from '@src/lib/types/blog';
import { BlogResponse, PartCategoryType } from '@src/lib/types/blog';
import axios from 'axios';
import qs from 'qs';
import { GetReviewsResponse, GetSampleReviewsResponse, ReviewAPI } from '../../types/review';
import { GetSampleReviewsResponse, ReviewAPI } from '../../types/review';

const client = axios.create({
baseURL: BASE_URL,
timeout: DEFAULT_TIMEOUT,
});

const getResponse = async (
export const getResponse = async (
majorTab: number,
subTab: PartCategoryType,
pageNo = 1,
): Promise<GetReviewsResponse> => {
): Promise<BlogResponse> => {
const generationParameter = majorTab === 0 ? {} : { generation: majorTab };
const partParameter = subTab === PartCategoryType.ALL ? {} : { part: subTab };
const pageParameter = { pageNo, limit: 6 };
const parameter = qs.stringify({ ...partParameter, ...pageParameter, ...generationParameter });

const { data } = await client.get(`/reviews?${parameter}`);

return { hasNextPage: data.hasNextPage, response: data.data };
return { hasNextPage: data.hasNextPage, response: data.data, currentPage: data.currentPage };
};

const getSampleReviews = async (): Promise<GetSampleReviewsResponse> => {
Expand Down
23 changes: 10 additions & 13 deletions src/lib/api/remote/sopticle.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { BASE_URL, DEFAULT_TIMEOUT } from '@src/lib/constants/client';
import { BlogPostType } from '@src/lib/types/blog';
import { PartCategoryType } from '@src/lib/types/blog';
import {
GetSopticlesResponse,
PostSopticleLikeResponse,
SopticleAPI,
} from '@src/lib/types/sopticle';
import { BlogPostType, BlogResponse, PartCategoryType } from '@src/lib/types/blog';
import { PostSopticleLikeResponse, SopticleAPI } from '@src/lib/types/sopticle';
import { getStorageHandler } from '@src/lib/utils/storageHandler';
import axios from 'axios';
import { nanoid } from 'nanoid';
Expand All @@ -16,26 +11,28 @@ const client = axios.create({
timeout: DEFAULT_TIMEOUT,
});

const getResponse = async (
export const getResponse = async (
majorTab: number,
subTab: PartCategoryType,
pageNo = 1,
): Promise<GetSopticlesResponse> => {
): Promise<BlogResponse> => {
const generationParameter = majorTab === 0 ? {} : { generation: majorTab };
const partParameter = subTab === PartCategoryType.ALL ? {} : { part: subTab };
const pageParameter = { pageNo, limit: 6 };
const sessionStorageHandler = getStorageHandler('sessionStorage');
const sessionId = sessionStorageHandler.getItemOrGenerate('session-id', nanoid);
const parameter = qs.stringify({ ...partParameter, ...pageParameter, ...generationParameter });

const { data } = await client.get<{ hasNextPage: boolean; data: BlogPostType[] }>(
`/sopticle?${parameter}`,
{ headers: { 'session-id': sessionId } },
);
const { data } = await client.get<{
hasNextPage: boolean;
data: BlogPostType[];
currentPage: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 있는거 타입으로 빼면 어떤가요??

BlogPostType을 제너릭 인자로 받아오는 .. 그런 베이스 타입을 선언해두면 나중에 두고두고 쓰기 좋을 것 같아요!!

}>(`/sopticle?${parameter}`, { headers: { 'session-id': sessionId } });

return {
hasNextPage: data.hasNextPage,
response: data.data,
currentPage: data.currentPage,
};
};

Expand Down
6 changes: 6 additions & 0 deletions src/lib/types/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ export enum PartCategoryType {
WEB = 'WEB',
SERVER = 'SERVER',
}

export type BlogResponse = {
hasNextPage: boolean;
response: BlogPostType[];
currentPage: number;
};
13 changes: 2 additions & 11 deletions src/lib/types/review.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { PartCategoryType } from '@src/lib/types/blog';
import { BlogPostType } from './blog';
import { BlogPostType, BlogResponse } from './blog';

export type GetSampleReviewsResponse = {
reviews: BlogPostType[];
};

export type GetReviewsResponse = {
response: BlogPostType[];
hasNextPage: boolean;
};

export interface ReviewAPI {
getResponse(
majorTab: number,
subTab: PartCategoryType,
page: number,
): Promise<GetReviewsResponse>;
getResponse(majorTab: number, subTab: PartCategoryType, page: number): Promise<BlogResponse>;
getSampleReviews(): Promise<GetSampleReviewsResponse>;
}
13 changes: 2 additions & 11 deletions src/lib/types/sopticle.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { PartCategoryType } from '@src/lib/types/blog';
import { BlogPostType } from './blog';

export type GetSopticlesResponse = {
hasNextPage: boolean;
response: BlogPostType[];
};
import { BlogResponse } from './blog';

export type PostSopticleLikeResponse = {
currentLike: boolean;
likeChanged: boolean;
};
export interface SopticleAPI {
getResponse(
majorTab: number,
subTab: PartCategoryType,
page: number,
): Promise<GetSopticlesResponse>;
getResponse(majorTab: number, subTab: PartCategoryType, page: number): Promise<BlogResponse>;
postSopticleLike(sopticleId: number, prevLike: boolean): Promise<PostSopticleLikeResponse>;
}
61 changes: 42 additions & 19 deletions src/views/BlogPage/BlogPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { css } from '@emotion/react';
import PageLayout from '@src/components/common/PageLayout';
import useStorage from '@src/hooks/useStorage';
import { activeGenerationCategoryList } from '@src/lib/constants/tabs';
import { PartCategoryType } from '@src/lib/types/blog';
import BlogPostSkeletonUI from '@src/views/BlogPage/components/BlogPostSkeletonUI';
import { OvalSpinner } from '@src/views/ProjectPage/components';
import BlogPostList from './components/BlogPostList';
import BlogTab from './components/BlogTab';
import { BlogTabType } from './components/BlogTab/types';
import useFetch from './hooks/useFetch';
import { useGetResponse } from './hooks/queries/useGetResponse';
import useInfiniteScroll from './hooks/useInfiniteScroll';
import * as S from './style';

export default function BlogPage() {
Expand All @@ -26,20 +29,26 @@ export default function BlogPage() {
PartCategoryType.ALL,
);

const {
state: response,
ref,
canGetMoreResponse,
} = useFetch({
const { response, hasNextPage, fetchNextPage, isFetching } = useGetResponse(
selectedTab,
selectedMajorCategory,
selectedSubCategory,
});
);
const { ref } = useInfiniteScroll(fetchNextPage);

if (!(response._TAG === 'OK' || response._TAG === 'LOADING')) return null;
const showTotalPostList = () => {
setMajorCategory(activeGenerationCategoryList[0]);
setSubCategory(PartCategoryType.ALL);
};

return (
<PageLayout>
<PageLayout
showScrollTopButton
moreStyle={css`
justify-content: space-between;
height: 100vh;
`}
>
<BlogTab
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
Expand All @@ -48,16 +57,30 @@ export default function BlogPage() {
selectedSubCategory={selectedSubCategory}
setSubCategory={setSubCategory}
/>
<BlogPostList
selectedTap={selectedTab}
setMajorCategory={setMajorCategory}
setSubCategory={setSubCategory}
blogPostList={response.data}
/>
{(canGetMoreResponse || response._TAG === 'LOADING') && (
<S.SpinnerWrapper ref={canGetMoreResponse ? ref : undefined}>
<OvalSpinner />
</S.SpinnerWrapper>
{!response ? (
<BlogPostSkeletonUI />
) : (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에 코멘트 드렸던 대로 이 부분은 항상 삼항 연산자 밖에 있어도 괜찮을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번에 작업하면서 다시 안으로 들어갔나봐요....😂 다시 조건문 고민해볼게요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SeojinSeojin 서진 ~ 이부분 코멘트 좀 더 자세히 해줄 수 있을까요?
위 코멘트도 다시 읽어봤는데...!

저의생각))
BlogPostList를 선택적으로 띄울 필요가 없다!!
왜냐면, 위에서 제가 제안한 대로 '아직 올라온 아티클이/활동후기가 없어요' 부분이 여기로 빠진다면,
-> BlogPostList는 response.data를 띄우는 역할만 할 것이고,
-> 그럼 자연스럽게 response.data.length가 0이면 아무것도 안 보일 것이기 때문에
굳이 패칭 시작해서 data.length가 0이 될 때마다 마운트/언마운트를 시키지 않아도 괜찮을 거라고 생각합니다!!!!!!!!!

여기서 BlogPostList를 선택적을 띄울 필요가 없는 것은 이해하겠는데, 아직 올라온 아티클이/활동후기가 없어요' 게 어디로 빠지면 좋겠다는지 이해가 잘 안돼서요..!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에는 문구가 blogPostList 컴포넌트 아래에 있었는데, 그것의 부모 컴포넌트인 이곳으로 오면 좋겠다는 뜻이었어요..!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아,..! 저렇게 두어도 좋겠다는 뜻이었구뇨! 이해했습니다 !!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이 부분 의도하신 흐름에 대해서 말로 풀어서 설명해주실 수 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      {!response ? (
        <BlogPostSkeletonUI />
      ) : (
        <>
          {response.length === 0 ? (
            <S.EmptyBlogPostListWrapper>
              <S.EmptyBlogPostList>
                {`아직 올라온 ${selectedTab === 'article' ? '아티클이' : '활동후기가'} 없어요`}
              </S.EmptyBlogPostList>
              <S.Total onClick={showTotalPostList}>{`${
                selectedTab === 'article' ? '아티클' : '활동후기'
              } 전체 보기`}</S.Total>
            </S.EmptyBlogPostListWrapper>
          ) : (
            <>
              <BlogPostList selectedTap={selectedTab} blogPostList={response} />
              {(hasNextPage || isFetching) && (
                <S.SpinnerWrapper ref={hasNextPage ? ref : undefined}>
                  <OvalSpinner />
                </S.SpinnerWrapper>
              )}
            </>
          )}
        </>
      )}

이렇게 수정하였습니다!
데이터가 ㅐ처음 패칭중일때 undefined를 반환해서 그때는 BlogPostSkeletonUI 를 보여주고
response.length === 0 으로 아티클이 없을때는 없다는 문구가, !== 0으로 있을때는 리스트가 보여지게 됩니다!

<>
{response.length === 0 ? (
<S.EmptyBlogPostListWrapper>
<S.EmptyBlogPostList>
{`아직 올라온 ${selectedTab === 'article' ? '아티클이' : '활동후기가'} 없어요`}
</S.EmptyBlogPostList>
<S.Total onClick={showTotalPostList}>{`${
selectedTab === 'article' ? '아티클' : '활동후기'
} 전체 보기`}</S.Total>
</S.EmptyBlogPostListWrapper>
) : (
<>
<BlogPostList selectedTap={selectedTab} blogPostList={response} />
{(hasNextPage || isFetching) && (
<S.SpinnerWrapper ref={hasNextPage ? ref : undefined}>
<OvalSpinner />
</S.SpinnerWrapper>
)}
</>
)}
</>
)}
</PageLayout>
);
Expand Down
4 changes: 3 additions & 1 deletion src/views/BlogPage/components/BlogPost/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import Image from 'next/image';
export const BlogPost = styled.section`
display: flex;
justify-content: space-between;
max-width: 900px;
width: 100%;
gap: 36px;

cursor: pointer;
transition: opacity 0.2s linear;
&:hover {
opacity: 0.8;
opacity: 0.7;
}

/* 모바일 뷰 */
@media (max-width: 767px) {
gap: 16px;
width: 100%;
}
`;

Expand Down
39 changes: 6 additions & 33 deletions src/views/BlogPage/components/BlogPostList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,18 @@
import { activeGenerationCategoryList } from '@src/lib/constants/tabs';
import type { BlogPostType } from '@src/lib/types/blog';
import { PartCategoryType } from '@src/lib/types/blog';
import BlogPost from '@src/views/BlogPage/components/BlogPost';
import * as S from './style';

interface BlogPostListProps {
selectedTap: string; // review || article

blogPostList: BlogPostType[];
setMajorCategory: (newValue: number) => void;
setSubCategory: (newValue: PartCategoryType) => void;
}

export default function BlogPostList({
selectedTap,
blogPostList,
setMajorCategory,
setSubCategory,
}: BlogPostListProps) {
const showTotalPostList = () => {
setMajorCategory(activeGenerationCategoryList[0]);
setSubCategory(PartCategoryType.ALL);
};
export default function BlogPostList({ selectedTap, blogPostList }: BlogPostListProps) {
return (
<S.BlogPostListWrapper>
{blogPostList.length === 0 ? (
<>
<S.EmptyBlogPostList>
{`아직 올라온 ${selectedTap === 'article' ? '아티클이' : '활동후기가'} 없어요`}
</S.EmptyBlogPostList>
<S.Total onClick={showTotalPostList}>{`${
selectedTap === 'article' ? '아티클' : '활동후기'
} 전체 보기`}</S.Total>
</>
) : (
<S.BlogPostList>
{blogPostList?.map((blogPost) => (
<BlogPost key={blogPost.id} blogPost={blogPost} selectedTap={selectedTap} />
))}
</S.BlogPostList>
)}
</S.BlogPostListWrapper>
<S.BlogPostList>
{blogPostList?.map((blogPost) => (
<BlogPost key={blogPost.id} blogPost={blogPost} selectedTap={selectedTap} />
))}
</S.BlogPostList>
);
}
60 changes: 2 additions & 58 deletions src/views/BlogPage/components/BlogPostList/style.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';

export const BlogPostListWrapper = styled.div`
export const BlogPostList = styled.div`
display: flex;
flex-direction: column;
align-items: center;
Expand All @@ -10,68 +9,13 @@ export const BlogPostListWrapper = styled.div`
padding-left: 20px;
padding-right: 20px;
margin-bottom: 108px;
`;
export const BlogPostList = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 80px;
max-width: 900px;
width: 100%;

margin-top: 30px;
gap: 80px;

/* 모바일 뷰 */
@media (max-width: 767px) {
width: 100%;
margin-top: 24px;
gap: 36px;
}
`;

export const EmptyBlogPostList = styled.section`
color: ${colors.white};
font-size: 32px;
font-weight: 700;
line-height: 48px; /* 150% */
letter-spacing: -0.64px;
margin-top: 108px;
margin-bottom: 24px;

/* 모바일 뷰 */
@media (max-width: 767px) {
font-size: 18px;
line-height: 28px; /* 155.556% */
letter-spacing: -0.36px;
margin-top: 90px;
margin-bottom: 16px;
}
`;

export const Total = styled.button`
background-color: ${colors.gray10};

height: 42px;
padding: 0px 24px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 8px;
color: ${colors.gray950};
font-family: SUIT;
font-size: 16px;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.32px;

cursor: pointer;

/* 모바일 뷰 */
@media (max-width: 767px) {
height: 36px;
padding: 8px 14px;
font-size: 14px;
line-height: 18px; /* 128.571% */
letter-spacing: -0.28px;
}
`;
Loading
Loading