From fc53c730e954b51ea82f4871fe145ffbf00c990d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=EC=A6=9D=ED=9B=88?= Date: Fri, 11 Aug 2023 11:24:21 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20refactor:=20`categories`,=20`writings`?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20(#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: `categories`, 'writings' 분리 * refactor: `categories` 데이터가 없을 때 카테고리 헤더는 보이게 수정 * refactor: `useCategoryWritings` -> `useWritings` * feat: `CategoryItem` 구현 * refactor: CategorySection 관심사 분리 * chore: isValidCategoryName 파일 경로 수정 * chore: merge --- .../CategorySection/CategorySection.tsx | 136 ------------------ .../CategorySection/useCategoryDetails.ts | 62 -------- .../CategorySection/useCategoryWritings.ts | 20 --- .../src/components/Category/Header/Header.tsx | 89 ++++++++++++ .../src/components/Category/Item/Item.tsx | 34 +++++ .../src/components/Category/List/List.tsx | 28 ++++ .../components/Category/List/useCategories.ts | 8 ++ .../Section.stories.tsx} | 8 +- .../components/Category/Section/Section.tsx | 21 +++ .../WritingList/WritingList.stories.tsx | 18 +-- .../Category/WritingList/WritingList.tsx | 15 +- .../Category/WritingList/useWritings.ts | 13 ++ .../components/Category/useCategoryInput.ts | 6 - frontend/src/pages/Layout/Layout.tsx | 2 +- 14 files changed, 214 insertions(+), 246 deletions(-) delete mode 100644 frontend/src/components/Category/CategorySection/CategorySection.tsx delete mode 100644 frontend/src/components/Category/CategorySection/useCategoryDetails.ts delete mode 100644 frontend/src/components/Category/CategorySection/useCategoryWritings.ts create mode 100644 frontend/src/components/Category/Header/Header.tsx create mode 100644 frontend/src/components/Category/Item/Item.tsx create mode 100644 frontend/src/components/Category/List/List.tsx create mode 100644 frontend/src/components/Category/List/useCategories.ts rename frontend/src/components/Category/{CategorySection/CategorySection.stories.tsx => Section/Section.stories.tsx} (50%) create mode 100644 frontend/src/components/Category/Section/Section.tsx create mode 100644 frontend/src/components/Category/WritingList/useWritings.ts diff --git a/frontend/src/components/Category/CategorySection/CategorySection.tsx b/frontend/src/components/Category/CategorySection/CategorySection.tsx deleted file mode 100644 index 773b23c21..000000000 --- a/frontend/src/components/Category/CategorySection/CategorySection.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import Accordion from 'components/@common/Accordion/Accordion'; -import { styled } from 'styled-components'; -import { PlusIcon } from 'assets/icons'; -import { KeyboardEventHandler } from 'react'; -import useCategoryInput from '../useCategoryInput'; -import Category from '../Category/Category'; -import WritingList from '../WritingList/WritingList'; -import { useCategoryDetails } from './useCategoryDetails'; -import { useCategoryMutation } from '../useCategoryMutation'; -import Input from 'components/@common/Input/Input'; -import { isValidCategoryName } from '../isValidCategoryName'; - -const CategorySection = () => { - const { categoryDetails, setSelectedCategoryId } = useCategoryDetails(); - const { - inputRef, - escapeInput: escapeAddCategory, - isInputOpen, - openInput, - resetInput, - isError, - setIsError, - } = useCategoryInput(''); - const { addCategory } = useCategoryMutation(); - - const requestAddCategory: KeyboardEventHandler = async (e) => { - if (e.key !== 'Enter') return; - - const categoryName = e.currentTarget.value.trim(); - - if (!isValidCategoryName(categoryName)) { - setIsError(true); - return; - } - - resetInput(); - addCategory({ categoryName: categoryName }); - }; - - if (!categoryDetails) return null; - - return ( - - - 카테고리 - {isInputOpen ? ( - - ) : ( - - - - )} - - - {categoryDetails.map((categoryDetail, index) => { - return ( - - setSelectedCategoryId(categoryDetail.id)} - aria-label={`${categoryDetail.categoryName} 카테고리 왼쪽 사이드바에서 열기`} - > - - - - {categoryDetail.writings && } - - - ); - })} - - - ); -}; - -export default CategorySection; - -const S = { - Section: styled.section` - width: 26rem; - overflow: auto; - `, - - Header: styled.header` - display: flex; - justify-content: space-between; - align-items: center; - height: 2.8rem; - font-size: 1.2rem; - font-weight: 400; - padding-right: 0.8rem; - `, - - Button: styled.button` - display: flex; - justify-content: center; - align-items: center; - width: 2rem; - height: 2.4rem; - border-radius: 8px; - - &:hover { - background-color: ${({ theme }) => theme.color.gray5}; - } - `, - - Title: styled.h1` - color: ${({ theme }) => theme.color.gray8}; - cursor: default; - `, - - Input: styled.input` - border: none; - outline: none; - color: ${({ theme }) => theme.color.gray10}; - font-size: 1.2rem; - font-weight: 600; - - &::placeholder { - font-weight: 300; - } - `, -}; diff --git a/frontend/src/components/Category/CategorySection/useCategoryDetails.ts b/frontend/src/components/Category/CategorySection/useCategoryDetails.ts deleted file mode 100644 index 408512f21..000000000 --- a/frontend/src/components/Category/CategorySection/useCategoryDetails.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { getCategories } from 'apis/category'; -import { useEffect, useState } from 'react'; -import { GetCategoryDetailResponse } from 'types/apis/category'; -import { CategoryWriting } from 'types/components/category'; -import { useCategoryWritings } from './useCategoryWritings'; - -export const useCategoryDetails = () => { - const { selectedCategoryWritings, selectedCategoryId, setSelectedCategoryId } = - useCategoryWritings(); - - const { data: categories } = useQuery(['categories'], getCategories); - - const [categoryDetails, setCategoryDetails] = useState([]); - - useEffect(() => { - if (!categories) return; - - const initCategoryDetails = () => { - // 동글 첫 진입 시, 카테고리 목록 데이터 만드는 함수 - return categories.categories.map((category) => ({ - id: category.id, - categoryName: category.categoryName, - writings: null, - })); - }; - - const updatedCategoryDetails = (prevDetails: GetCategoryDetailResponse[]) => { - // 카테고리가 변경됐을 때 동기화 - return categories.categories.map((category) => { - const prevDetail = prevDetails.find((detail) => detail.id === category.id); - - return { - id: category.id, - categoryName: category.categoryName, - writings: prevDetail?.writings ?? null, - }; - }); - }; - - setCategoryDetails((prevDetails) => { - return prevDetails ? updatedCategoryDetails(prevDetails) : initCategoryDetails(); - }); - }, [categories]); - - useEffect(() => { - const updateCategoryDetails = (selectedCategoryId: number, writings: CategoryWriting[]) => { - // 카테고리 토글 클릭 시 selectedCategoryWritings 업데이트 - setCategoryDetails( - (prevDetails) => - prevDetails?.map((detail) => - detail.id === selectedCategoryId ? { ...detail, writings } : { ...detail }, - ), - ); - }; - - if (selectedCategoryId && selectedCategoryWritings) - updateCategoryDetails(selectedCategoryId, selectedCategoryWritings); - }, [selectedCategoryId, selectedCategoryWritings]); - - return { categoryDetails, setSelectedCategoryId }; -}; diff --git a/frontend/src/components/Category/CategorySection/useCategoryWritings.ts b/frontend/src/components/Category/CategorySection/useCategoryWritings.ts deleted file mode 100644 index 3fa58cb9b..000000000 --- a/frontend/src/components/Category/CategorySection/useCategoryWritings.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GetCategoryDetailResponse } from 'types/apis/category'; -import { getWritingsInCategory } from 'apis/category'; -import { useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; - -export const useCategoryWritings = () => { - const [selectedCategoryId, setSelectedCategoryId] = useState(null); - - const { data } = useQuery( - ['writingsInCategory', selectedCategoryId], - () => getWritingsInCategory(selectedCategoryId!), - { enabled: Boolean(selectedCategoryId) }, // 첫번째 요청만 disabled - ); - - return { - selectedCategoryWritings: data ? data.writings : null, - selectedCategoryId, - setSelectedCategoryId, - }; -}; diff --git a/frontend/src/components/Category/Header/Header.tsx b/frontend/src/components/Category/Header/Header.tsx new file mode 100644 index 000000000..216afa2a4 --- /dev/null +++ b/frontend/src/components/Category/Header/Header.tsx @@ -0,0 +1,89 @@ +import { KeyboardEventHandler } from 'react'; +import { styled } from 'styled-components'; +import useCategoryInput from '../useCategoryInput'; +import { useCategoryMutation } from '../useCategoryMutation'; +import { isValidCategoryName } from '../isValidCategoryName'; +import Input from 'components/@common/Input/Input'; +import { PlusCircleIcon } from 'assets/icons'; + +const Header = () => { + const { + inputRef, + escapeInput: escapeAddCategory, + isInputOpen, + openInput, + resetInput, + isError, + setIsError, + } = useCategoryInput(''); + const { addCategory } = useCategoryMutation(); + + const requestAddCategory: KeyboardEventHandler = async (e) => { + if (e.key !== 'Enter') return; + + const categoryName = e.currentTarget.value.trim(); + + if (!isValidCategoryName(categoryName)) { + setIsError(true); + return; + } + + resetInput(); + addCategory({ categoryName: categoryName }); + }; + + return ( + + 카테고리 + {isInputOpen ? ( + + ) : ( + + + + )} + + ); +}; + +export default Header; + +const S = { + Header: styled.header` + display: flex; + justify-content: space-between; + align-items: center; + height: 2.8rem; + font-size: 1.2rem; + font-weight: 400; + padding-right: 0.8rem; + `, + + Title: styled.h1` + color: ${({ theme }) => theme.color.gray8}; + cursor: default; + `, + + Button: styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 2rem; + height: 2.4rem; + border-radius: 8px; + + &:hover { + background-color: ${({ theme }) => theme.color.gray5}; + } + `, +}; diff --git a/frontend/src/components/Category/Item/Item.tsx b/frontend/src/components/Category/Item/Item.tsx new file mode 100644 index 000000000..d00975967 --- /dev/null +++ b/frontend/src/components/Category/Item/Item.tsx @@ -0,0 +1,34 @@ +import Accordion from 'components/@common/Accordion/Accordion'; +import { useState } from 'react'; +import Category from '../Category/Category'; +import WritingList from '../WritingList/WritingList'; + +type Props = { + categoryId: number; + categoryName: string; + isDefaultCategory: boolean; +}; + +const Item = ({ categoryId, categoryName, isDefaultCategory }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + setIsOpen((prev) => !prev)} + aria-label={`${categoryName} 카테고리 왼쪽 사이드바에서 열기`} + > + + + + + + + ); +}; + +export default Item; diff --git a/frontend/src/components/Category/List/List.tsx b/frontend/src/components/Category/List/List.tsx new file mode 100644 index 000000000..e0dd7f176 --- /dev/null +++ b/frontend/src/components/Category/List/List.tsx @@ -0,0 +1,28 @@ +import Accordion from 'components/@common/Accordion/Accordion'; +import { useCategories } from './useCategories'; +import Item from '../Item/Item'; + +const List = () => { + const { categories } = useCategories(); + + return ( + <> + {categories ? ( + + {categories.map((category, index) => { + return ( + + ); + })} + + ) : null} + + ); +}; + +export default List; diff --git a/frontend/src/components/Category/List/useCategories.ts b/frontend/src/components/Category/List/useCategories.ts new file mode 100644 index 000000000..1b29a0f7f --- /dev/null +++ b/frontend/src/components/Category/List/useCategories.ts @@ -0,0 +1,8 @@ +import { useQuery } from '@tanstack/react-query'; +import { getCategories } from 'apis/category'; + +export const useCategories = () => { + const { data } = useQuery(['categories'], getCategories); + + return { categories: data ? data.categories : null }; +}; diff --git a/frontend/src/components/Category/CategorySection/CategorySection.stories.tsx b/frontend/src/components/Category/Section/Section.stories.tsx similarity index 50% rename from frontend/src/components/Category/CategorySection/CategorySection.stories.tsx rename to frontend/src/components/Category/Section/Section.stories.tsx index f3cda6c9d..d4dca4302 100644 --- a/frontend/src/components/Category/CategorySection/CategorySection.stories.tsx +++ b/frontend/src/components/Category/Section/Section.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; -import CategorySection from './CategorySection'; +import Section from './Section'; -const meta: Meta = { - title: 'CategorySection', - component: CategorySection, +const meta: Meta = { + title: 'Section', + component: Section, }; export default meta; diff --git a/frontend/src/components/Category/Section/Section.tsx b/frontend/src/components/Category/Section/Section.tsx new file mode 100644 index 000000000..7d1b46f0d --- /dev/null +++ b/frontend/src/components/Category/Section/Section.tsx @@ -0,0 +1,21 @@ +import { styled } from 'styled-components'; +import Header from '../Header/Header'; +import List from '../List/List'; + +const Section = () => { + return ( + +
+ + + ); +}; + +export default Section; + +const S = { + Section: styled.section` + width: 26rem; + overflow: auto; + `, +}; diff --git a/frontend/src/components/Category/WritingList/WritingList.stories.tsx b/frontend/src/components/Category/WritingList/WritingList.stories.tsx index afdbccada..7ad455a8b 100644 --- a/frontend/src/components/Category/WritingList/WritingList.stories.tsx +++ b/frontend/src/components/Category/WritingList/WritingList.stories.tsx @@ -5,19 +5,15 @@ const meta: Meta = { title: 'WritingList', component: WritingList, args: { - writings: [ - { id: 1, title: '동글' }, - { id: 2, title: '테트리스 중독' }, - { - id: 3, - title: - '테트리스 하고 싶다 테트리스 하고 싶다 테트리스 하고 싶다 테트리스 하고 싶다 테트리스 하고 싶다 테트리스 하고 싶다', - }, - ], + categoryId: 1, + isOpen: true, }, argTypes: { - writings: { - description: '글 목록 데이터입니다.', + categoryId: { + description: '현재 속한 카테고리의 아이디입니다.', + }, + isOpen: { + description: '카테고리 토글의 열림 여부 입니다.', }, }, }; diff --git a/frontend/src/components/Category/WritingList/WritingList.tsx b/frontend/src/components/Category/WritingList/WritingList.tsx index 664e98778..f2abdacb0 100644 --- a/frontend/src/components/Category/WritingList/WritingList.tsx +++ b/frontend/src/components/Category/WritingList/WritingList.tsx @@ -2,26 +2,29 @@ import { WritingIcon } from 'assets/icons'; import { usePageNavigate } from 'hooks/usePageNavigate'; import { useParams } from 'react-router-dom'; import { styled } from 'styled-components'; -import { CategoryWriting } from 'types/components/category'; +import { useWritings } from './useWritings'; type Props = { - writings: CategoryWriting[]; + categoryId: number; + isOpen: boolean; }; -const WritingList = ({ writings }: Props) => { +const WritingList = ({ categoryId, isOpen }: Props) => { const { goWritingPage } = usePageNavigate(); + const { writings } = useWritings(categoryId, isOpen); const writingId = Number(useParams()['writingId']); - if (writings.length === 0) return No Writings inside; + if (!writings || writings?.length === 0) + return No Writings inside; return (
    {writings.map((writing) => ( goWritingPage(writing.id)} + $isClicked={writingId === writing.id} aria-label={`${writing.title}글 메인화면에 열기`} + onClick={() => goWritingPage(writing.id)} > diff --git a/frontend/src/components/Category/WritingList/useWritings.ts b/frontend/src/components/Category/WritingList/useWritings.ts new file mode 100644 index 000000000..0a0068372 --- /dev/null +++ b/frontend/src/components/Category/WritingList/useWritings.ts @@ -0,0 +1,13 @@ +import { GetCategoryDetailResponse } from 'types/apis/category'; +import { getWritingsInCategory } from 'apis/category'; +import { useQuery } from '@tanstack/react-query'; + +export const useWritings = (categoryId: number, isOpen: boolean) => { + const { data } = useQuery( + ['writingsInCategory', categoryId], + () => getWritingsInCategory(categoryId), + { enabled: Boolean(isOpen) }, // 첫번째 요청만 disabled + ); + + return { writings: data ? data.writings : null }; +}; diff --git a/frontend/src/components/Category/useCategoryInput.ts b/frontend/src/components/Category/useCategoryInput.ts index 1646e81a6..f0a79608f 100644 --- a/frontend/src/components/Category/useCategoryInput.ts +++ b/frontend/src/components/Category/useCategoryInput.ts @@ -2,7 +2,6 @@ import { ChangeEventHandler, KeyboardEventHandler, useEffect, useRef, useState } const useCategoryInput = (initialValue: string) => { const [isError, setIsError] = useState(false); - const [value, setValue] = useState(initialValue); const [isInputOpen, setIsInputOpen] = useState(false); const inputRef = useRef(null); @@ -12,14 +11,11 @@ const useCategoryInput = (initialValue: string) => { } }, [isInputOpen]); - const handleOnChange: ChangeEventHandler = (e) => setValue(e.target.value); - const openInput = () => setIsInputOpen(true); const resetInput = () => { setIsError(false); setIsInputOpen(false); - setValue(''); }; const escapeInput: KeyboardEventHandler = (e) => { @@ -29,9 +25,7 @@ const useCategoryInput = (initialValue: string) => { }; return { - value, inputRef, - handleOnChange, escapeInput, isInputOpen, openInput, diff --git a/frontend/src/pages/Layout/Layout.tsx b/frontend/src/pages/Layout/Layout.tsx index 20ddc1274..bd23ec6d0 100644 --- a/frontend/src/pages/Layout/Layout.tsx +++ b/frontend/src/pages/Layout/Layout.tsx @@ -6,7 +6,7 @@ import Button from 'components/@common/Button/Button'; import { HEADER_STYLE, LAYOUT_STYLE, sidebarStyle } from 'styles/layoutStyle'; import Header from 'components/Header/Header'; import WritingSideBar from 'components/WritingSideBar/WritingSideBar'; -import CategorySection from 'components/Category/CategorySection/CategorySection'; +import CategorySection from 'components/Category/Section/Section'; import { useModal } from 'hooks/@common/useModal'; import FileUploadModal from 'components/FileUploadModal/FileUploadModal';