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

홈 화면 서스펜스 범위 재지정 및 셀렉트 컴포넌트 리팩터링 #840

Merged
merged 6 commits into from
Nov 14, 2023
2 changes: 1 addition & 1 deletion frontend/src/api/userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const modifyNickname = async (nickname: string) => {
};

export const withdrawalMembership = async () => {
await deleteFetch(`${BASE_URL}/members/me/delete`);
await deleteFetch(`${BASE_URL}/auth/members/me/delete`);
};

export const updateUserInfo = async (userInfo: UpdateUserInfoRequest) => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/ReportModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export default function ReportModal({
}: UserReportModalProps) {
const { name, reportMessageList } = REPORT_TYPE[reportType];
const defaultReportMessage = Object.keys(reportMessageList)[0] as ReportMessage;
const { selectedOption, handleOptionChange } = useSelect<ReportMessage>(defaultReportMessage);
const { selectedOption, handleOptionChange, isSelectOpen, toggleSelect } =
useSelect<ReportMessage>(defaultReportMessage);

const handlePrimaryButtonClick = () => {
if (isReportLoading) return;
Expand All @@ -49,6 +50,8 @@ export default function ReportModal({
>
<S.ModalBody>
<Select
isOpen={isSelectOpen}
toggleSelect={toggleSelect}
aria-label={`${name} 방법 선택`}
optionList={reportMessageList}
handleOptionChange={handleOptionChange}
Expand Down
69 changes: 21 additions & 48 deletions frontend/src/components/common/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { Meta } from '@storybook/react';

import { useState } from 'react';
import { useSelect } from '@hooks';

import Select from '.';

Expand All @@ -10,73 +10,46 @@ const meta: Meta<typeof Select> = {
};

export default meta;
type Story = StoryObj<typeof Select>;

const postStatus = ['all', 'progress', 'closed'] as const;
const sortingOption = ['popular', 'latest', 'longLong'] as const;

type PostStatusType = (typeof postStatus)[number];
type SortingOptionType = (typeof sortingOption)[number];

const MOCK_STATUS_OPTION: Record<PostStatusType, string> = {
all: '전체',
progress: '진행중',
closed: '마감완료',
};

const MOCK_SORTING_OPTION: Record<SortingOptionType, string> = {
popular: '인기순',
latest: '최신순',
longLong: '엄청나게 긴 옵션',
};

export const PostStatus: Story = {
render: () => (
<Select<PostStatusType>
aria-label="게시글 진행 상태 선택"
selectedOption="진행중"
optionList={MOCK_STATUS_OPTION}
handleOptionChange={() => {}}
/>
),
};

export const Sorting: Story = {
render: () => (
<Select
aria-label="게시글 정렬 방법 선택"
selectedOption="최신순"
optionList={MOCK_SORTING_OPTION}
handleOptionChange={() => {}}
/>
),
};
export const SelectExample = () => {
const { handleOptionChange, isSelectOpen, selectedOption, toggleSelect } =
useSelect<SortingOptionType>('popular');

export const Disabled: Story = {
render: () => (
<Select
return (
<Select<SortingOptionType>
isOpen={isSelectOpen}
toggleSelect={toggleSelect}
aria-label="게시글 정렬 방법 선택"
isDisabled={true}
selectedOption="최신순"
selectedOption={MOCK_SORTING_OPTION[selectedOption]}
optionList={MOCK_SORTING_OPTION}
handleOptionChange={() => {}}
handleOptionChange={handleOptionChange}
/>
),
);
};

export const SelectExample = () => {
const [selectedOption, setSelectedOption] = useState<SortingOptionType>('popular');

const handelOptionChange = (option: SortingOptionType) => {
setSelectedOption(option);
};
export const Disabled = () => {
const { handleOptionChange, isSelectOpen, selectedOption, toggleSelect } =
useSelect<SortingOptionType>('popular');

return (
<Select<SortingOptionType>
<Select
isOpen={isSelectOpen}
toggleSelect={toggleSelect}
aria-label="게시글 정렬 방법 선택"
selectedOption={MOCK_SORTING_OPTION[selectedOption]}
isDisabled={true}
selectedOption={selectedOption}
optionList={MOCK_SORTING_OPTION}
handleOptionChange={handelOptionChange}
handleOptionChange={handleOptionChange}
/>
);
};
20 changes: 9 additions & 11 deletions frontend/src/components/common/Select/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';

import chevronDown from '@assets/chevron-down.svg';
import chevronUp from '@assets/chevron-up.svg';
Expand All @@ -10,6 +10,8 @@ export interface SelectProps<T extends string>
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
selectedOption: string;
optionList: Record<T, string>;
isOpen: boolean;
toggleSelect: () => void;
handleOptionChange: (option: T) => void;
isDisabled?: boolean;
}
Expand All @@ -18,20 +20,16 @@ export default function Select<T extends string>({
selectedOption,
optionList,
handleOptionChange,
isOpen,
toggleSelect,
isDisabled = false,
...rest
}: SelectProps<T>) {
const optionKeyList = Object.keys(optionList) as T[];
const [isOpen, setIsOpen] = useState(false);

const toggleOpen = () => {
const handleToggleOpen = () => {
if (isDisabled) return;
setIsOpen(prev => !prev);
};

const handleSelectClick = (option: T) => {
handleOptionChange(option);
setIsOpen(false);
toggleSelect();
};

const getSelectStatus = () => {
Expand All @@ -48,7 +46,7 @@ export default function Select<T extends string>({

return (
<S.Container>
<S.SelectedContainer onClick={toggleOpen} $status={getSelectStatus()} {...rest}>
<S.SelectedContainer onClick={handleToggleOpen} $status={getSelectStatus()} {...rest}>
<span>{selectedOption}</span>
<S.Image src={isOpen ? chevronUp : chevronDown} alt="" $isSelected={isOpen} />
</S.SelectedContainer>
Expand All @@ -64,7 +62,7 @@ export default function Select<T extends string>({
<S.OptionContainer
tabIndex={0}
key={optionKey}
onClick={() => handleSelectClick(optionKey)}
onClick={() => handleOptionChange(optionKey)}
>
{optionList[optionKey]}
</S.OptionContainer>
Expand Down
111 changes: 50 additions & 61 deletions frontend/src/components/post/PostList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,86 @@
import React, { useContext, useEffect, useRef } from 'react';
import React, { MouseEvent, Suspense, useContext, useRef } from 'react';

import { useSelect } from '@hooks';

import { AuthContext } from '@hooks/context/auth';
import { PostOptionContext } from '@hooks/context/postOption';
import { usePostList } from '@hooks/query/usePostList';
import { useIntersectionObserver } from '@hooks/useIntersectionObserver';
import { usePostRequestInfo } from '@hooks/usePostRequestInfo';

import ErrorBoundary from '@pages/ErrorBoundary';
import { SORTING_OPTION, STATUS_OPTION } from '@pages/HomePage/constants';
import { PostSorting, PostStatus } from '@pages/HomePage/types';

import Select from '@components/common/Select';
import Skeleton from '@components/common/Skeleton';
import Post from '@components/post/Post';

import { PATH } from '@constants/path';

import EmptyPostList from '../EmptyPostList';
import PostListFetcher from '../PostListFetcher';

import * as S from './style';

export default function PostList() {
const topButtonRef = useRef<HTMLButtonElement>(null);
const { postType, postOptionalOption } = usePostRequestInfo();
const { loggedInfo } = useContext(AuthContext);
const { targetRef, isIntersecting } = useIntersectionObserver({
root: null,
rootMargin: '',
thresholds: 0.1,
});

const { postOption, setPostOption } = useContext(PostOptionContext);

const { selectedOption: selectedStatusOption, handleOptionChange: handleStatusOptionChange } =
useSelect<PostStatus>(postOption.status);
const { selectedOption: selectedSortingOption, handleOptionChange: handleSortingOptionChange } =
useSelect<PostSorting>(postOption.sorting);

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPostListEmpty } = usePostList(
{
postType,
postSorting: selectedSortingOption,
postStatus: selectedStatusOption,
isLoggedIn: loggedInfo.isLoggedIn,
},
postOptionalOption
);

const focusTopContent = () => {
const {
selectedOption: selectedStatusOption,
handleOptionChange: handleStatusOptionChange,
isSelectOpen: isStatusSelectOpen,
toggleSelect: toggleStatusSelect,
selectRef: statusSelectRef,
handleCloseClick: handleStatusClose,
} = useSelect<PostStatus>(postOption.status);
const {
selectedOption: selectedSortingOption,
handleOptionChange: handleSortingOptionChange,
isSelectOpen: isSortingSelectOpen,
toggleSelect: toggleSortingSelect,
selectRef: sortingSelectRef,
handleCloseClick: handleSortingClose,
} = useSelect<PostSorting>(postOption.sorting);

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍👍

const handleFocusTopContent = () => {
if (!topButtonRef.current) return;

topButtonRef.current.focus();
};

useEffect(() => {
if (isIntersecting && hasNextPage) {
fetchNextPage();
}
}, [isIntersecting, fetchNextPage, hasNextPage]);

return (
<S.Container>
<S.Container
onClick={(event: MouseEvent<HTMLDivElement>) => {
handleStatusClose(event);
handleSortingClose(event);
}}
>
<button ref={topButtonRef} role="contentinfo" aria-label="최상단입니다" />
<S.SelectContainer>
<S.SelectWrapper>
<S.SelectWrapper ref={statusSelectRef}>
<Select<PostStatus>
isOpen={isStatusSelectOpen}
toggleSelect={toggleStatusSelect}
aria-label={`마감 여부로 게시글 정렬 선택, 현재 옵션은 ${STATUS_OPTION[selectedStatusOption]}`}
handleOptionChange={(value: PostStatus) => {
handleOptionChange={async (value: PostStatus) => {
if (value === selectedStatusOption) return;

setPostOption({
...postOption,
status: value,
});

handleStatusOptionChange(value);
}}
optionList={STATUS_OPTION}
selectedOption={STATUS_OPTION[selectedStatusOption]}
/>
</S.SelectWrapper>
<S.SelectWrapper>
<S.SelectWrapper ref={sortingSelectRef}>
<Select<PostSorting>
isOpen={isSortingSelectOpen}
toggleSelect={toggleSortingSelect}
aria-label={`인기순/최신순으로 게시글 정렬 선택, 현재 옵션은 ${SORTING_OPTION[selectedSortingOption]}`}
handleOptionChange={(value: PostSorting) => {
if (value === selectedSortingOption) return;

setPostOption({
...postOption,
sorting: value,
Expand All @@ -93,28 +92,18 @@ export default function PostList() {
/>
</S.SelectWrapper>
</S.SelectContainer>
<ErrorBoundary hasIcon={true} hasRetryInteraction={true}>
<Suspense
fallback={
<S.SkeletonWrapper>
<Skeleton isLarge={true} />
</S.SkeletonWrapper>
}
>
<PostListFetcher handleFocusTopContent={handleFocusTopContent} />
</Suspense>
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍👍 분리 너무 좋아요

</ErrorBoundary>
<S.HiddenLink aria-label="게시글 작성 페이지로 이동" to={PATH.POST_WRITE} />
<S.PostListContainer>
{isPostListEmpty && (
<EmptyPostList status={selectedStatusOption} keyword={postOptionalOption.keyword} />
)}
{data?.pages.map((postListInfo, pageIndex) => (
<React.Fragment key={pageIndex}>
{postListInfo.postList.map((post, index) => {
if (index === 7) {
return <Post key={post.postId} ref={targetRef} isPreview={true} postInfo={post} />;
}

return <Post key={post.postId} isPreview={true} postInfo={post} />;
})}
<li key={`${pageIndex}UserButton`}>
<S.HiddenButton onClick={focusTopContent} aria-label="스크롤 맨 위로가기" />
<S.HiddenLink aria-label="게시글 작성 페이지로 이동" to={PATH.POST_WRITE} />
</li>
</React.Fragment>
))}
{isFetchingNextPage && <Skeleton isLarge={false} />}
</S.PostListContainer>
</S.Container>
);
}
6 changes: 3 additions & 3 deletions frontend/src/components/post/PostList/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export const SelectWrapper = styled.div`
}
`;

export const HiddenButton = styled.button`
export const HiddenLink = styled(Link)`
position: absolute;
`;

export const HiddenLink = styled(Link)`
position: absolute;
export const SkeletonWrapper = styled.div`
padding: 30px 20px;
`;
Loading
Loading