From 79cf92954500e825cc98c4247549c968800e82cc Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 04:08:42 +0900 Subject: [PATCH 01/12] feat: Add deleteWordbook api and renameWordbook api --- src/DTO/wordbook.dto.ts | 39 +++++++++++++++++++++++++++ src/back/api/routes/wordbook.ts | 24 +++++++++++++++++ src/back/loaders/errorHandler.ts | 8 +++--- src/back/repository/wordbook.repo.ts | 27 ++++++++++++++++++- src/back/services/wordbook.service.ts | 12 +++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/DTO/wordbook.dto.ts b/src/DTO/wordbook.dto.ts index a1bf227..a4f8613 100644 --- a/src/DTO/wordbook.dto.ts +++ b/src/DTO/wordbook.dto.ts @@ -92,7 +92,46 @@ export const showWordbookSchema = { }, } as const; +export const deleteWordbookSchema = { + tags: ['Wordbook'], + summary: '단어장 삭제', + headers: AuthorizationHeader, + body: { + type: 'object', + required: ['bookId'], + properties: { + bookId: { type: 'string' }, + }, + }, + response: { + 200: returnType, + ...errorSchema( + ) + }, +} as const; + +export const renameWordbookSchema = { + tags: ['Wordbook'], + summary: '단어장 이름 변경', + headers: AuthorizationHeader, + body: { + type: 'object', + required: ['bookId', 'title'], + properties: { + bookId: { type: 'string' }, + title: { type: 'string' }, + }, + }, + response: { + 200: returnType, + ...errorSchema( + ) + }, +} as const; + export type getWordbookListInterface = SchemaToInterface & { Body: { userId: string } }; export type createWordbookInterface = SchemaToInterface & { Body: { userId: string } }; export type hideWordbookInterface = SchemaToInterface & { Body: { userId: string } }; export type showWordbookInterface = SchemaToInterface & { Body: { userId: string } }; +export type deleteWordbookInterface = SchemaToInterface & { Body: { userId: string } }; +export type renameWordbookInterface = SchemaToInterface & { Body: { userId: string } }; diff --git a/src/back/api/routes/wordbook.ts b/src/back/api/routes/wordbook.ts index 0e594e2..9b12ab2 100644 --- a/src/back/api/routes/wordbook.ts +++ b/src/back/api/routes/wordbook.ts @@ -51,6 +51,30 @@ const api: FastifyPluginAsync = async (server: FastifyInstance) => { reply.status(200).send(result); } ); + + server.delete( + '/', + { + schema: WordbookDTO.deleteWordbookSchema, + preValidation: checkUser + }, + async (request, reply) => { + const result = await WordbookService.deleteWordbook(request.body); + reply.status(200).send(result); + } + ); + + server.patch( + '/name', + { + schema: WordbookDTO.renameWordbookSchema, + preValidation: checkUser + }, + async (request, reply) => { + const result = await WordbookService.renameWordbook(request.body); + reply.status(200).send(result); + } + ); }; export default api; diff --git a/src/back/loaders/errorHandler.ts b/src/back/loaders/errorHandler.ts index a4d3215..93b7af6 100644 --- a/src/back/loaders/errorHandler.ts +++ b/src/back/loaders/errorHandler.ts @@ -1,5 +1,5 @@ import { FastifyRequest, FastifyReply, FastifyError } from 'fastify'; -import { ErrorWithToast, ValidationError } from '@errors'; +import { ErrorWithToast, ValidationError,NotFoundError } from '@errors'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import ErrorConfig from '@errors/config'; @@ -34,8 +34,10 @@ export default ( } if(error instanceof PrismaClientKnownRequestError) { if(error.code === 'P2025') { - //error.toast = '찾을 수 없는 데이터가 포함되어 있습니다.'; - return reply.code(404).send(error); + return reply.code(404).send({ + error: NotFoundError.name, + message: '요청하신 데이터를 찾을 수 없습니다.', + }); } //error.toast = '잘못된 데이터가 입력되었습니다.'; return reply.code(400).send(error); diff --git a/src/back/repository/wordbook.repo.ts b/src/back/repository/wordbook.repo.ts index b421b98..58df4c9 100644 --- a/src/back/repository/wordbook.repo.ts +++ b/src/back/repository/wordbook.repo.ts @@ -12,7 +12,8 @@ export const getWordbook = async (wordbookId: string, userId: string) => await p const getWordbookList = (isHidden:boolean) => async (userId: string) => await prisma.wordbook.findMany({ where: { userId, - isHidden + isHidden, + deletedAt: null }, include: { _count: { @@ -51,3 +52,27 @@ const changeWordbookHidden = (isHidden:boolean) => async (userId: string, wordbo export const hideWordbook = changeWordbookHidden(true); export const showWordbook = changeWordbookHidden(false); + +export const deleteWordbook = async (userId: string, wordbookId: string) => { + await prisma.wordbook.update({ + where: { + uuid: wordbookId, + userId + }, + data: { + deletedAt: new Date() + } + }); +} + +export const renameWordbook = async (userId: string, wordbookId: string, title: string) => { + await prisma.wordbook.update({ + where: { + uuid: wordbookId, + userId + }, + data: { + title + } + }); +} diff --git a/src/back/services/wordbook.service.ts b/src/back/services/wordbook.service.ts index 41a3213..a94a48c 100644 --- a/src/back/services/wordbook.service.ts +++ b/src/back/services/wordbook.service.ts @@ -38,3 +38,15 @@ export async function showWordbook({userId, bookId}: WordbookDTO.showWordbookInt await WordbookRepo.showWordbook(userId, bookId); return getWrodbookList({userId}); } + +export async function deleteWordbook({userId, bookId}: WordbookDTO.deleteWordbookInterface['Body']) +: Promise { + await WordbookRepo.deleteWordbook(userId, bookId); + return getWrodbookList({userId}); +} + +export async function renameWordbook({userId, bookId, title}: WordbookDTO.renameWordbookInterface['Body']) +: Promise { + await WordbookRepo.renameWordbook(userId, bookId, title); + return getWrodbookList({userId}); +} From 170deda7b187109d328122923524c18b22582423 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 04:10:48 +0900 Subject: [PATCH 02/12] feat: Add deleteWordbook and renameWordbook API functions at front --- src/front/utils/apis/wordbook.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/front/utils/apis/wordbook.ts b/src/front/utils/apis/wordbook.ts index f7a263d..3300dac 100644 --- a/src/front/utils/apis/wordbook.ts +++ b/src/front/utils/apis/wordbook.ts @@ -51,3 +51,20 @@ export const showWordbook = apiErrorCatchWrapper(async (accessToken: string, boo ); return response.data; }); + +export const deleteWordbook = apiErrorCatchWrapper(async (accessToken: string, bookId: string) => { + const response = await axios.delete( + "/api/v1/wordbook", + { data: { bookId }, headers: { Authorization: `Bearer ${accessToken}` } } + ); + return response.data; +}); + +export const renameWordbook = apiErrorCatchWrapper(async (accessToken: string, bookId: string, title: string) => { + const response = await axios.patch( + "/api/v1/wordbook", + { bookId, title }, + { headers: { Authorization: `Bearer ${accessToken}` } } + ); + return response.data; +}); From e29984366b0bb4107cc80e8078f8d50b97eb67f1 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 04:14:48 +0900 Subject: [PATCH 03/12] feat: create wordbook component folder --- src/front/components/{ => wordbook}/AddWordbook.tsx | 2 +- src/front/components/{ => wordbook}/HiddenWordbookTitle.tsx | 2 +- src/front/components/{ => wordbook}/MyWordbookTitle.tsx | 2 +- src/front/components/{ => wordbook}/WordbookDetailInfo.tsx | 0 src/front/components/{ => wordbook}/WordbookElement.tsx | 2 +- src/front/pages/MyWordbook.tsx | 6 +++--- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/front/components/{ => wordbook}/AddWordbook.tsx (98%) rename src/front/components/{ => wordbook}/HiddenWordbookTitle.tsx (92%) rename src/front/components/{ => wordbook}/MyWordbookTitle.tsx (91%) rename src/front/components/{ => wordbook}/WordbookDetailInfo.tsx (100%) rename src/front/components/{ => wordbook}/WordbookElement.tsx (98%) diff --git a/src/front/components/AddWordbook.tsx b/src/front/components/wordbook/AddWordbook.tsx similarity index 98% rename from src/front/components/AddWordbook.tsx rename to src/front/components/wordbook/AddWordbook.tsx index d5e3ee5..398f7d6 100644 --- a/src/front/components/AddWordbook.tsx +++ b/src/front/components/wordbook/AddWordbook.tsx @@ -1,4 +1,4 @@ -import { WordbookInfo, WordbookMenu, WordbookName, AddWordbookContainer, Input, ButtonContainingIcon, ReverseButtonContainingIcon } from './index'; +import { WordbookInfo, WordbookMenu, WordbookName, AddWordbookContainer, Input, ButtonContainingIcon, ReverseButtonContainingIcon } from '../index'; import useFetchUpdate from "@hooks/useFetchUpdate"; import { addWordbook } from '@utils/apis/wordbook'; import { useContext,useRef } from 'react'; diff --git a/src/front/components/HiddenWordbookTitle.tsx b/src/front/components/wordbook/HiddenWordbookTitle.tsx similarity index 92% rename from src/front/components/HiddenWordbookTitle.tsx rename to src/front/components/wordbook/HiddenWordbookTitle.tsx index e2f5d10..cfba552 100644 --- a/src/front/components/HiddenWordbookTitle.tsx +++ b/src/front/components/wordbook/HiddenWordbookTitle.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import WordbookListContext from '@context/WordbookListContext'; -import { Title, Expend } from './index'; +import { Title, Expend } from '../index'; function HiddenWordbookTitle() { const {expend, expendOnClick} = useContext(WordbookListContext); diff --git a/src/front/components/MyWordbookTitle.tsx b/src/front/components/wordbook/MyWordbookTitle.tsx similarity index 91% rename from src/front/components/MyWordbookTitle.tsx rename to src/front/components/wordbook/MyWordbookTitle.tsx index 5e4970f..b6669cf 100644 --- a/src/front/components/MyWordbookTitle.tsx +++ b/src/front/components/wordbook/MyWordbookTitle.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { ButtonContainingIcon, Title } from './index'; +import { ButtonContainingIcon, Title } from '../index'; import AddWordbook from './AddWordbook'; function MyWordbookTitle() { diff --git a/src/front/components/WordbookDetailInfo.tsx b/src/front/components/wordbook/WordbookDetailInfo.tsx similarity index 100% rename from src/front/components/WordbookDetailInfo.tsx rename to src/front/components/wordbook/WordbookDetailInfo.tsx diff --git a/src/front/components/WordbookElement.tsx b/src/front/components/wordbook/WordbookElement.tsx similarity index 98% rename from src/front/components/WordbookElement.tsx rename to src/front/components/wordbook/WordbookElement.tsx index ac57ca6..d7c69f2 100644 --- a/src/front/components/WordbookElement.tsx +++ b/src/front/components/wordbook/WordbookElement.tsx @@ -1,4 +1,4 @@ -import { WordbookInfo, WordbookMenu, WordbookName, WordbookContainer } from './index'; +import { WordbookInfo, WordbookMenu, WordbookName, WordbookContainer } from '../index'; import { Link } from 'react-router-dom'; import useFetchUpdate from "@hooks/useFetchUpdate"; import { hideWordbook,showWordbook } from '@utils/apis/wordbook'; diff --git a/src/front/pages/MyWordbook.tsx b/src/front/pages/MyWordbook.tsx index 786f88c..a831064 100644 --- a/src/front/pages/MyWordbook.tsx +++ b/src/front/pages/MyWordbook.tsx @@ -3,9 +3,9 @@ import WordbookListContext from "@context/WordbookListContext"; import useWordbookData from "@hooks/useWordbookData"; import { Navigate } from "react-router-dom"; import Profile from "@components/Profile"; -import MyWordbookTitle from "@components/MyWordbookTitle"; -import HiddenWordbookTitle from "@components/HiddenWordbookTitle"; -import WordbookElement from "@components/WordbookElement"; +import MyWordbookTitle from "@components/wordbook/MyWordbookTitle"; +import HiddenWordbookTitle from "@components/wordbook/HiddenWordbookTitle"; +import WordbookElement from "@components/wordbook/WordbookElement"; import { WordbookListContainer } from "@components"; import ErrorConfigs from "@errors/config"; import { useMemo } from "react"; From 66c581465a9813366045545e8872e1b0ce106c99 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 04:18:57 +0900 Subject: [PATCH 04/12] feat: create vocaList component folder --- src/front/components/{ => vocaList}/EditVocaList.tsx | 2 +- src/front/components/{ => vocaList}/TestVocaList.tsx | 2 +- src/front/components/{ => vocaList}/ViewVocaList.tsx | 2 +- src/front/components/{ => vocaList}/VocaSidebar.tsx | 0 src/front/pages/VocaList.tsx | 8 ++++---- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/front/components/{ => vocaList}/EditVocaList.tsx (99%) rename src/front/components/{ => vocaList}/TestVocaList.tsx (99%) rename src/front/components/{ => vocaList}/ViewVocaList.tsx (99%) rename src/front/components/{ => vocaList}/VocaSidebar.tsx (100%) diff --git a/src/front/components/EditVocaList.tsx b/src/front/components/vocaList/EditVocaList.tsx similarity index 99% rename from src/front/components/EditVocaList.tsx rename to src/front/components/vocaList/EditVocaList.tsx index ea0945d..190826a 100644 --- a/src/front/components/EditVocaList.tsx +++ b/src/front/components/vocaList/EditVocaList.tsx @@ -14,7 +14,7 @@ import { ReverseButtonContainingIcon, ActivatableIcon, ButtonWithHoverAnimation -} from './index'; +} from '../index'; import useFetchUpdate from '@hooks/useFetchUpdate'; import { deleteVoca } from '@utils/apis/voca'; diff --git a/src/front/components/TestVocaList.tsx b/src/front/components/vocaList/TestVocaList.tsx similarity index 99% rename from src/front/components/TestVocaList.tsx rename to src/front/components/vocaList/TestVocaList.tsx index b004b13..021019b 100644 --- a/src/front/components/TestVocaList.tsx +++ b/src/front/components/vocaList/TestVocaList.tsx @@ -13,7 +13,7 @@ import { MeaningWithAnswer, ButtonWithHoverAnimation, ReverseButtonWithHoverAnimation -} from './index'; +} from '../index'; const marking = (input: string[][], answer: {id:number,meaning: string[]}[]) => { const commaSeparatedAnswer = answer.map(a => ({...a,meaning:a.meaning.map(b=>b.split(',').map(c=>c.trim()))})); diff --git a/src/front/components/ViewVocaList.tsx b/src/front/components/vocaList/ViewVocaList.tsx similarity index 99% rename from src/front/components/ViewVocaList.tsx rename to src/front/components/vocaList/ViewVocaList.tsx index 4c620b3..4331b7a 100644 --- a/src/front/components/ViewVocaList.tsx +++ b/src/front/components/vocaList/ViewVocaList.tsx @@ -18,7 +18,7 @@ import { FilpCard, MeaningHeader, SelectButton -} from './index'; +} from '../index'; const handleWord = ( id: number, diff --git a/src/front/components/VocaSidebar.tsx b/src/front/components/vocaList/VocaSidebar.tsx similarity index 100% rename from src/front/components/VocaSidebar.tsx rename to src/front/components/vocaList/VocaSidebar.tsx diff --git a/src/front/pages/VocaList.tsx b/src/front/pages/VocaList.tsx index 6b6ee67..def3726 100644 --- a/src/front/pages/VocaList.tsx +++ b/src/front/pages/VocaList.tsx @@ -4,10 +4,10 @@ import useVocaListData from "@hooks/useVocaListData"; import { useState,useEffect } from "react"; import { useParams,Navigate } from "react-router-dom"; import { VocaMode } from "@utils/vocaModeEnum"; -import ViewVocaList from "@components/ViewVocaList"; -import EditVocaList from "@components/EditVocaList"; -import TestVocaList from "@components/TestVocaList"; -import VocaSidebar from "@components/VocaSidebar"; +import ViewVocaList from "@components/vocaList/ViewVocaList"; +import EditVocaList from "@components/vocaList/EditVocaList"; +import TestVocaList from "@components/vocaList/TestVocaList"; +import VocaSidebar from "@components/vocaList/VocaSidebar"; import ErrorConfigs from "@errors/config"; const VocaModeWithComponent = [ From 54e7f7a1e17f311aa3896808655c8c9cefe6d70c Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 16:56:25 +0900 Subject: [PATCH 05/12] refactor: VocaList components and context --- .../components/vocaList/EditVocaList.tsx | 5 ++- .../components/vocaList/TestVocaList.tsx | 4 +- .../components/vocaList/ViewVocaList.tsx | 4 +- src/front/components/vocaList/VocaSidebar.tsx | 43 +++++++++++++------ src/front/context/VocaListContext.ts | 11 +++-- src/front/hooks/useVocaListData.ts | 37 ++++++++-------- src/front/pages/VocaList.tsx | 13 ++---- 7 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/front/components/vocaList/EditVocaList.tsx b/src/front/components/vocaList/EditVocaList.tsx index 190826a..7e611ee 100644 --- a/src/front/components/vocaList/EditVocaList.tsx +++ b/src/front/components/vocaList/EditVocaList.tsx @@ -1,4 +1,6 @@ import { VocaMode } from "@utils/vocaModeEnum"; +import VocaListContext from '@context/VocaListContext'; +import { useContext } from 'react'; import useEditVocaList from "@hooks/useEditVocaList"; import { VocaListElement, @@ -40,7 +42,8 @@ function WordInputWithMenu({word, onChange, disabled, moveWordUp, moveWordDown, ); } -function EditVocaList({setVocaMode}: {setVocaMode: React.Dispatch>}) { +function EditVocaList() { + const { setVocaMode } = useContext(VocaListContext); const { vocaList, loadingSaveVocaList, onChangeWord, onChangeMeans, reset, save, deleteNewVoca, deleteMean, diff --git a/src/front/components/vocaList/TestVocaList.tsx b/src/front/components/vocaList/TestVocaList.tsx index 021019b..2279aea 100644 --- a/src/front/components/vocaList/TestVocaList.tsx +++ b/src/front/components/vocaList/TestVocaList.tsx @@ -31,8 +31,8 @@ const marking = (input: string[][], answer: {id:number,meaning: string[]}[]) => return {detailMarking: detailMarking.map(({result}) => result), markingResult}; } -function TestVocaList({setVocaMode}: {setVocaMode: React.Dispatch>}) { - const { vocaList } = useContext(VocaListContext); +function TestVocaList() { + const { vocaList, setVocaMode } = useContext(VocaListContext); const emptyVocaList = vocaList.map(voca => new Array(voca.meaning.length).fill("")); const [inputList, setInputList] = useState(emptyVocaList); const [result, setResult] = useState<{meaning: string[],correct: boolean}[][]|null>(null); diff --git a/src/front/components/vocaList/ViewVocaList.tsx b/src/front/components/vocaList/ViewVocaList.tsx index 4331b7a..1e9a6f3 100644 --- a/src/front/components/vocaList/ViewVocaList.tsx +++ b/src/front/components/vocaList/ViewVocaList.tsx @@ -68,8 +68,8 @@ function FlippableMeaning({children, reversed, refresh}: {children: string, reve ); } -function ViewVocaList({setVocaMode}: {setVocaMode: React.Dispatch>}) { - const { vocaList } = useContext(VocaListContext); +function ViewVocaList() { + const { vocaList, setVocaMode } = useContext(VocaListContext); const [defaultVisible, setDefaultVisible] = useState(false); const [showCount, setShowCount] = useState(0); const [refresh, setRefresh] = useState({}); diff --git a/src/front/components/vocaList/VocaSidebar.tsx b/src/front/components/vocaList/VocaSidebar.tsx index 5f32c08..412a923 100644 --- a/src/front/components/vocaList/VocaSidebar.tsx +++ b/src/front/components/vocaList/VocaSidebar.tsx @@ -2,6 +2,8 @@ import { styled } from 'styled-components'; import { VocaMode } from "@utils/vocaModeEnum"; import { ISOStringToDateString } from '@utils'; import { ButtonWithHoverAnimation, ReverseButtonWithHoverAnimation } from '@components'; +import VocaListContext from '@context/VocaListContext'; +import { useContext } from 'react'; const VocaSidebarContainer = styled.div` display: flex; @@ -52,10 +54,24 @@ const ChildListElement = styled.div` const WordbookName = styled.div` margin-bottom: 10px; - &>span:last-child { - font-size: 1.5rem; - font-weight: 600; - color: var(--main-color); + display: flex; + align-items: center; + &>div { + display: inline-flex; + width: 100%; + align-items: center; + border-bottom: 1px solid var(--main-color); + padding-left: 5px; + padding-bottom: 2px; + &>span:nth-child(1) { + font-size: 1.5rem; + font-weight: 600; + color: var(--main-color); + } + &>span:nth-child(2) { + margin-left: auto; + cursor: pointer; + } } .material-icons-sharp { font-size: 1rem; @@ -80,20 +96,23 @@ const WordbookInfo = styled.div` `; -function VocaSidebar({vocaMode,setVocaMode,wordbook}:{ - vocaMode: VocaMode, - setVocaMode: React.Dispatch>, - wordbook: {title: string, createdAt: string, wordCount: number} -}) { +function VocaSidebar() { + const { wordbook, setVocaMode, vocaMode } = useContext(VocaListContext); + return ( menu_book - - {wordbook.title} - +
+ + {wordbook.title} + + + edit + +
diff --git a/src/front/context/VocaListContext.ts b/src/front/context/VocaListContext.ts index f3e27f0..d42c563 100644 --- a/src/front/context/VocaListContext.ts +++ b/src/front/context/VocaListContext.ts @@ -1,11 +1,14 @@ import { createContext } from 'react'; -import { getVocaList } from "@utils/apis/voca"; +import useVocaListData from '@hooks/useVocaListData'; const VocaListContext = createContext( {} as { - vocaList: Awaited>['voca'], - setVocaList: React.Dispatch>['voca']>>, - wordbookId: string + vocaList: ReturnType['vocaList'], + setVocaList: ReturnType['setVocaList'], + wordbookId: string, + wordbook: ReturnType['wordbook'], + vocaMode: ReturnType['vocaMode'], + setVocaMode: ReturnType['setVocaMode'] } ); diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index 4733899..3800329 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -1,21 +1,24 @@ -import { useState, useEffect,useMemo } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import useFetchWithRendering from "@hooks/useFetchWithRendering"; import { getVocaList } from "@utils/apis/voca"; +import { VocaMode } from "@utils/vocaModeEnum"; -export default (wordbookId:string) => { - const [data, vocaListError] = useFetchWithRendering(getVocaList, wordbookId); - const [isLoading, setIsLoading] = useState(true); - const [wordbook, setWordbook] = useState['wordbook']&{wordCount: number}>({ - title: '', createdAt: '', uuid: "", wordCount: 0 - }); - const [vocaList, setVocaList] = useState['voca']>([]); - useEffect(() => { - if (data) { - setWordbook({...data.wordbook, wordCount: data.voca.length}); - setVocaList(data.voca); - setIsLoading(false); - } - }, [data]); - - return useMemo(() => ({ isLoading, wordbook, vocaList, setVocaList, vocaListError }), [isLoading, wordbook, vocaList, setVocaList, vocaListError]); +export default (wordbookId: string) => { + const [data, vocaListError] = useFetchWithRendering(getVocaList, wordbookId); + const [isLoading, setIsLoading] = useState(true); + const [wordbook, setWordbook] = useState['wordbook'] & { wordCount: number }>({ + title: '', createdAt: '', uuid: "", wordCount: 0 + }); + const [vocaList, setVocaList] = useState['voca']>([]); + const [vocaMode, setVocaMode] = useState(VocaMode.EDIT); + useEffect(() => { + if (data) { + setWordbook({ ...data.wordbook, wordCount: data.voca.length }); + setVocaList(data.voca); + setIsLoading(false); + if(data.voca.length > 0) setVocaMode(VocaMode.VIEW); + } + }, [data]); + + return useMemo(() => ({ isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode, setVocaMode }), [isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode]); } diff --git a/src/front/pages/VocaList.tsx b/src/front/pages/VocaList.tsx index def3726..cf6f73f 100644 --- a/src/front/pages/VocaList.tsx +++ b/src/front/pages/VocaList.tsx @@ -18,12 +18,7 @@ const VocaModeWithComponent = [ function VocaList() { const { wordbookId } = useParams(); - const { isLoading, wordbook, vocaList, setVocaList, vocaListError } = useVocaListData(wordbookId!); - const [ vocaMode, setVocaMode ] = useState(VocaMode.EDIT); - useEffect(() => { - if(vocaList.length > 0) setVocaMode(VocaMode.VIEW); - // eslint-disable-next-line - }, [isLoading]); + const { isLoading, vocaListError, wordbook, vocaList, setVocaList, vocaMode, setVocaMode } = useVocaListData(wordbookId!); if(vocaListError) { const errorConfig = ErrorConfigs[vocaListError.name]; if(errorConfig) @@ -32,13 +27,13 @@ function VocaList() { } return ( - + - + {isLoading &&
} {!isLoading && VocaModeWithComponent.map(([mode, Component], i) => - vocaMode === mode && + vocaMode === mode && ) }
From 0b9c998af25bca943f54267f0ecf3a9def58c4b8 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 17:31:39 +0900 Subject: [PATCH 06/12] feat: voca mode encapsulation --- .../components/vocaList/EditVocaList.tsx | 14 +++++-------- .../components/vocaList/TestVocaList.tsx | 4 ++-- .../components/vocaList/ViewVocaList.tsx | 4 ++-- src/front/components/vocaList/VocaSidebar.tsx | 8 ++++---- src/front/context/VocaListContext.ts | 4 +++- src/front/hooks/useEditVocaList.ts | 20 +++++++++---------- src/front/hooks/useVocaListData.ts | 8 ++++++-- src/front/pages/VocaList.tsx | 5 ++--- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/front/components/vocaList/EditVocaList.tsx b/src/front/components/vocaList/EditVocaList.tsx index 7e611ee..f311b21 100644 --- a/src/front/components/vocaList/EditVocaList.tsx +++ b/src/front/components/vocaList/EditVocaList.tsx @@ -1,6 +1,3 @@ -import { VocaMode } from "@utils/vocaModeEnum"; -import VocaListContext from '@context/VocaListContext'; -import { useContext } from 'react'; import useEditVocaList from "@hooks/useEditVocaList"; import { VocaListElement, @@ -43,10 +40,9 @@ function WordInputWithMenu({word, onChange, disabled, moveWordUp, moveWordDown, } function EditVocaList() { - const { setVocaMode } = useContext(VocaListContext); const { vocaList, loadingSaveVocaList, onChangeWord, onChangeMeans, reset, save, - deleteNewVoca, deleteMean, + deleteWord, deleteMean, moveWordUp, moveWordDown } = useEditVocaList(); return ( @@ -54,8 +50,8 @@ function EditVocaList() { <span>단어 편집</span> <div style={{display: 'flex', gap: '10px'}}> - <ReverseButtonContainingIcon onClick={()=>reset(()=>setVocaMode(VocaMode.VIEW))}>취소</ReverseButtonContainingIcon> - <ButtonContainingIcon onClick={()=>save(()=>setVocaMode(VocaMode.VIEW))}>저장</ButtonContainingIcon> + <ReverseButtonContainingIcon onClick={reset}>취소</ReverseButtonContainingIcon> + <ButtonContainingIcon onClick={save}>저장</ButtonContainingIcon> </div> @@ -67,7 +63,7 @@ function EditVocaList() { 뜻
{vocaList.flatMap((voca,i) => [ - , + , , {voca.meaning.map((m,j) =>( @@ -79,7 +75,7 @@ function EditVocaList() { ])} - save(()=>setVocaMode(VocaMode.VIEW))}>저장 + 저장 ); } diff --git a/src/front/components/vocaList/TestVocaList.tsx b/src/front/components/vocaList/TestVocaList.tsx index 2279aea..44fc877 100644 --- a/src/front/components/vocaList/TestVocaList.tsx +++ b/src/front/components/vocaList/TestVocaList.tsx @@ -32,7 +32,7 @@ const marking = (input: string[][], answer: {id:number,meaning: string[]}[]) => } function TestVocaList() { - const { vocaList, setVocaMode } = useContext(VocaListContext); + const { vocaList, viewMode } = useContext(VocaListContext); const emptyVocaList = vocaList.map(voca => new Array(voca.meaning.length).fill("")); const [inputList, setInputList] = useState(emptyVocaList); const [result, setResult] = useState<{meaning: string[],correct: boolean}[][]|null>(null); @@ -80,7 +80,7 @@ function TestVocaList() { {result.filter(voca => voca.every(m => m.correct)).length}/{vocaList.length} } {result!==null&& - setVocaMode(VocaMode.VIEW)}> + 돌아가기 } diff --git a/src/front/components/vocaList/ViewVocaList.tsx b/src/front/components/vocaList/ViewVocaList.tsx index 1e9a6f3..98e8c88 100644 --- a/src/front/components/vocaList/ViewVocaList.tsx +++ b/src/front/components/vocaList/ViewVocaList.tsx @@ -69,7 +69,7 @@ function FlippableMeaning({children, reversed, refresh}: {children: string, reve } function ViewVocaList() { - const { vocaList, setVocaMode } = useContext(VocaListContext); + const { vocaList, editMode } = useContext(VocaListContext); const [defaultVisible, setDefaultVisible] = useState(false); const [showCount, setShowCount] = useState(0); const [refresh, setRefresh] = useState({}); @@ -79,7 +79,7 @@ function ViewVocaList() { <span>단어 목록</span> - <ButtonContainingIcon onClick={() => setVocaMode(VocaMode.EDIT)}>수정</ButtonContainingIcon> + <ButtonContainingIcon onClick={editMode}>수정</ButtonContainingIcon>
setShowCount(0)} $active={showCount===0}>All diff --git a/src/front/components/vocaList/VocaSidebar.tsx b/src/front/components/vocaList/VocaSidebar.tsx index 412a923..ee1e0ef 100644 --- a/src/front/components/vocaList/VocaSidebar.tsx +++ b/src/front/components/vocaList/VocaSidebar.tsx @@ -97,7 +97,7 @@ const WordbookInfo = styled.div` function VocaSidebar() { - const { wordbook, setVocaMode, vocaMode } = useContext(VocaListContext); + const { wordbook, viewMode, testMode, vocaMode } = useContext(VocaListContext); return ( @@ -122,7 +122,7 @@ function VocaSidebar() { 단어 {wordbook.wordCount} 개
- setVocaMode(VocaMode.EDIT)}> + event {wordbook.createdAt&&{ISOStringToDateString(wordbook.createdAt)}} @@ -154,10 +154,10 @@ function VocaSidebar() { 전체 단어 테스트 진행 후 틀린 단어만 모아서 한 번 더 외우기 {vocaMode===VocaMode.VIEW&& - setVocaMode(VocaMode.TEST)}>테스트 시작 + 테스트 시작 } {vocaMode!==VocaMode.VIEW&& - setVocaMode(VocaMode.VIEW)}>단어 학습으로 돌아가기 + 단어 학습으로 돌아가기 } ); diff --git a/src/front/context/VocaListContext.ts b/src/front/context/VocaListContext.ts index d42c563..aba7cd1 100644 --- a/src/front/context/VocaListContext.ts +++ b/src/front/context/VocaListContext.ts @@ -8,7 +8,9 @@ const VocaListContext = createContext( wordbookId: string, wordbook: ReturnType['wordbook'], vocaMode: ReturnType['vocaMode'], - setVocaMode: ReturnType['setVocaMode'] + editMode: () => void, + viewMode: () => void, + testMode: () => void } ); diff --git a/src/front/hooks/useEditVocaList.ts b/src/front/hooks/useEditVocaList.ts index 95e054f..c544f8c 100644 --- a/src/front/hooks/useEditVocaList.ts +++ b/src/front/hooks/useEditVocaList.ts @@ -6,7 +6,7 @@ import { saveVocaList } from '@utils/apis/voca'; type Voca = { word: string, meaning: string[], id: number | null }; const useEditVocaList = () => { - const { vocaList, setVocaList, wordbookId } = useContext(VocaListContext); + const { vocaList, setVocaList, wordbookId, viewMode } = useContext(VocaListContext); const newVocaList = vocaList.map(voca => ({ ...voca, meaning: [...voca.meaning, ''] })) as Voca[]; newVocaList.push({ word: '', meaning: [''], id: null }); @@ -31,12 +31,12 @@ const useEditVocaList = () => { setEditingVocaList(newVocaList); }, [editingVocaList]); - const reset = useCallback((callback:Function) => { + const reset = useCallback(() => { setEditingVocaList(vocaList); - callback(); + viewMode(); }, [vocaList]); - const save = useCallback(async (callback:Function) => { + const save = async () => { const removeEmptyList = editingVocaList.filter(voca => voca.word !== '').map(voca => ({...voca, meaning: voca.meaning.filter(m => m !== '')})); if (removeEmptyList.some(voca => voca.meaning.length === 0)) { alert('뜻이 없는 단어가 있습니다.'); @@ -44,10 +44,10 @@ const useEditVocaList = () => { } const newVocaList = await fetchSaveVocaList(wordbookId, removeEmptyList); setVocaList(newVocaList); - callback(); - }, [editingVocaList, wordbookId, fetchSaveVocaList, setVocaList]); + viewMode(); + }; - const deleteNewVoca = useCallback( + const deleteWord = useCallback( (i: number, id: number | null) => (fetchDeleteVoca: (id: number) => Promise) => async () => { @@ -80,18 +80,18 @@ const useEditVocaList = () => { setEditingVocaList(newVocaList); }, [editingVocaList]); - return useMemo(() => ({ + return { vocaList: editingVocaList, loadingSaveVocaList, onChangeWord, onChangeMeans, reset, save, - deleteNewVoca, + deleteWord, deleteMean, moveWordUp, moveWordDown - }), [editingVocaList, loadingSaveVocaList, onChangeWord, onChangeMeans, reset, save, deleteNewVoca, deleteMean, moveWordUp, moveWordDown]); + }; } export default useEditVocaList; diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index 3800329..8f8458c 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo,useCallback } from 'react'; import useFetchWithRendering from "@hooks/useFetchWithRendering"; import { getVocaList } from "@utils/apis/voca"; import { VocaMode } from "@utils/vocaModeEnum"; @@ -20,5 +20,9 @@ export default (wordbookId: string) => { } }, [data]); - return useMemo(() => ({ isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode, setVocaMode }), [isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode]); + const editMode = useCallback(() => setVocaMode(VocaMode.EDIT), []); + const viewMode = useCallback(() => setVocaMode(VocaMode.VIEW), []); + const testMode = useCallback(() => setVocaMode(VocaMode.TEST), []); + + return useMemo(() => ({ isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode, editMode, viewMode, testMode }), [isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode]); } diff --git a/src/front/pages/VocaList.tsx b/src/front/pages/VocaList.tsx index cf6f73f..981c991 100644 --- a/src/front/pages/VocaList.tsx +++ b/src/front/pages/VocaList.tsx @@ -1,7 +1,6 @@ import { MainContainer } from "@components"; import VocaListContext from "@context/VocaListContext"; import useVocaListData from "@hooks/useVocaListData"; -import { useState,useEffect } from "react"; import { useParams,Navigate } from "react-router-dom"; import { VocaMode } from "@utils/vocaModeEnum"; import ViewVocaList from "@components/vocaList/ViewVocaList"; @@ -18,7 +17,7 @@ const VocaModeWithComponent = [ function VocaList() { const { wordbookId } = useParams(); - const { isLoading, vocaListError, wordbook, vocaList, setVocaList, vocaMode, setVocaMode } = useVocaListData(wordbookId!); + const { isLoading, vocaListError, wordbook, vocaList, setVocaList, vocaMode, editMode, viewMode, testMode } = useVocaListData(wordbookId!); if(vocaListError) { const errorConfig = ErrorConfigs[vocaListError.name]; if(errorConfig) @@ -27,7 +26,7 @@ function VocaList() { } return ( - + {isLoading &&
} From 0f1c517a328a07adffc28db23a3355a91fdfc8e8 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 18:10:03 +0900 Subject: [PATCH 07/12] refactor: vocaList context do not export set state function --- .../components/vocaList/TestVocaList.tsx | 2 -- src/front/context/VocaListContext.ts | 7 +++-- src/front/hooks/useEditVocaList.ts | 18 +++--------- src/front/hooks/useVocaListData.ts | 28 ++++++++++++++++++- src/front/pages/VocaList.tsx | 6 ++-- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/front/components/vocaList/TestVocaList.tsx b/src/front/components/vocaList/TestVocaList.tsx index 44fc877..c65d265 100644 --- a/src/front/components/vocaList/TestVocaList.tsx +++ b/src/front/components/vocaList/TestVocaList.tsx @@ -1,6 +1,4 @@ -import { VocaMode } from "@utils/vocaModeEnum"; import VocaListContext from '@context/VocaListContext'; -import useFetchUpdate from '@hooks/useFetchUpdate'; import { useContext,useState } from 'react'; import { VocaListElement, diff --git a/src/front/context/VocaListContext.ts b/src/front/context/VocaListContext.ts index aba7cd1..2482ae8 100644 --- a/src/front/context/VocaListContext.ts +++ b/src/front/context/VocaListContext.ts @@ -4,13 +4,14 @@ import useVocaListData from '@hooks/useVocaListData'; const VocaListContext = createContext( {} as { vocaList: ReturnType['vocaList'], - setVocaList: ReturnType['setVocaList'], - wordbookId: string, wordbook: ReturnType['wordbook'], vocaMode: ReturnType['vocaMode'], editMode: () => void, viewMode: () => void, - testMode: () => void + testMode: () => void, + loadingSaveVocaList: ReturnType['loadingSaveVocaList'], + saveEditedVocaList: ReturnType['saveEditedVocaList'], + excludeVoca: ReturnType['excludeVoca'], } ); diff --git a/src/front/hooks/useEditVocaList.ts b/src/front/hooks/useEditVocaList.ts index c544f8c..130c42f 100644 --- a/src/front/hooks/useEditVocaList.ts +++ b/src/front/hooks/useEditVocaList.ts @@ -1,17 +1,14 @@ import { useState, useContext, useCallback, useMemo } from 'react'; import VocaListContext from '@context/VocaListContext'; -import useFetchUpdate from '@hooks/useFetchUpdate'; -import { saveVocaList } from '@utils/apis/voca'; type Voca = { word: string, meaning: string[], id: number | null }; const useEditVocaList = () => { - const { vocaList, setVocaList, wordbookId, viewMode } = useContext(VocaListContext); + const { vocaList, loadingSaveVocaList, saveEditedVocaList, excludeVoca, viewMode } = useContext(VocaListContext); const newVocaList = vocaList.map(voca => ({ ...voca, meaning: [...voca.meaning, ''] })) as Voca[]; newVocaList.push({ word: '', meaning: [''], id: null }); const [editingVocaList, setEditingVocaList] = useState(newVocaList); - const [loadingSaveVocaList, fetchSaveVocaList] = useFetchUpdate(saveVocaList); const onChangeWord = useCallback((i: number) => (e: React.ChangeEvent) => { const newVocaList = [...editingVocaList]; @@ -37,14 +34,7 @@ const useEditVocaList = () => { }, [vocaList]); const save = async () => { - const removeEmptyList = editingVocaList.filter(voca => voca.word !== '').map(voca => ({...voca, meaning: voca.meaning.filter(m => m !== '')})); - if (removeEmptyList.some(voca => voca.meaning.length === 0)) { - alert('뜻이 없는 단어가 있습니다.'); - return; - } - const newVocaList = await fetchSaveVocaList(wordbookId, removeEmptyList); - setVocaList(newVocaList); - viewMode(); + await saveEditedVocaList(editingVocaList); }; const deleteWord = useCallback( @@ -53,11 +43,11 @@ const useEditVocaList = () => { async () => { if(id !== null) { await fetchDeleteVoca(id); - setVocaList(vocaList.filter(voca => voca.id !== id)); + excludeVoca(id); } setEditingVocaList(editingVocaList.filter((_, index) => index !== i)); }, - [editingVocaList, setVocaList, vocaList] + [editingVocaList, vocaList] ); const deleteMean = useCallback((i: number, j: number) => ()=> { diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index 8f8458c..1bc7a52 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -2,6 +2,10 @@ import { useState, useEffect, useMemo,useCallback } from 'react'; import useFetchWithRendering from "@hooks/useFetchWithRendering"; import { getVocaList } from "@utils/apis/voca"; import { VocaMode } from "@utils/vocaModeEnum"; +import useFetchUpdate from '@hooks/useFetchUpdate'; +import { saveVocaList } from '@utils/apis/voca'; + +type Voca = { word: string, meaning: string[], id: number | null }; export default (wordbookId: string) => { const [data, vocaListError] = useFetchWithRendering(getVocaList, wordbookId); @@ -11,6 +15,8 @@ export default (wordbookId: string) => { }); const [vocaList, setVocaList] = useState['voca']>([]); const [vocaMode, setVocaMode] = useState(VocaMode.EDIT); + const [loadingSaveVocaList, fetchSaveVocaList] = useFetchUpdate(saveVocaList); + useEffect(() => { if (data) { setWordbook({ ...data.wordbook, wordCount: data.voca.length }); @@ -23,6 +29,26 @@ export default (wordbookId: string) => { const editMode = useCallback(() => setVocaMode(VocaMode.EDIT), []); const viewMode = useCallback(() => setVocaMode(VocaMode.VIEW), []); const testMode = useCallback(() => setVocaMode(VocaMode.TEST), []); + const saveEditedVocaList = useCallback(async (editingVocaList: Voca[]) => { + const removeEmptyList = editingVocaList.filter(voca => voca.word !== '').map(voca => ({...voca, meaning: voca.meaning.filter(m => m !== '')})); + if (removeEmptyList.some(voca => voca.meaning.length === 0)) { + alert('뜻이 없는 단어가 있습니다.'); + return; + } + + const newVocaList = await fetchSaveVocaList(wordbookId, removeEmptyList); + setVocaList(newVocaList); + setWordbook({ ...wordbook, wordCount: newVocaList.length }); + viewMode(); + }, [fetchSaveVocaList, wordbookId]); + + const excludeVoca = useCallback((id: number) => { + setVocaList(vocaList.filter(voca => voca.id !== id)); + setWordbook({ ...wordbook, wordCount: wordbook.wordCount - 1 }); + } , [vocaList]); - return useMemo(() => ({ isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode, editMode, viewMode, testMode }), [isLoading, wordbook, vocaList, setVocaList, vocaListError, vocaMode]); + return useMemo(() => ({ isLoading, wordbook, vocaList, + vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, + editMode, viewMode, testMode, excludeVoca }), + [isLoading, wordbook, vocaList, vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, editMode, viewMode, testMode]); } diff --git a/src/front/pages/VocaList.tsx b/src/front/pages/VocaList.tsx index 981c991..2c20781 100644 --- a/src/front/pages/VocaList.tsx +++ b/src/front/pages/VocaList.tsx @@ -17,7 +17,7 @@ const VocaModeWithComponent = [ function VocaList() { const { wordbookId } = useParams(); - const { isLoading, vocaListError, wordbook, vocaList, setVocaList, vocaMode, editMode, viewMode, testMode } = useVocaListData(wordbookId!); + const { isLoading, vocaListError, ...rest} = useVocaListData(wordbookId!); if(vocaListError) { const errorConfig = ErrorConfigs[vocaListError.name]; if(errorConfig) @@ -26,13 +26,13 @@ function VocaList() { } return ( - + {isLoading &&
} {!isLoading && VocaModeWithComponent.map(([mode, Component], i) => - vocaMode === mode && + rest.vocaMode === mode && ) }
From 62fc86cb04f95747e64b9f8f539ada65db0293b4 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 18:24:33 +0900 Subject: [PATCH 08/12] fix: update check count --- src/front/components/vocaList/ViewVocaList.tsx | 18 +++++------------- src/front/context/VocaListContext.ts | 1 + src/front/hooks/useEditVocaList.ts | 1 + src/front/hooks/useVocaListData.ts | 18 +++++++++++++++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/front/components/vocaList/ViewVocaList.tsx b/src/front/components/vocaList/ViewVocaList.tsx index 98e8c88..6f57a21 100644 --- a/src/front/components/vocaList/ViewVocaList.tsx +++ b/src/front/components/vocaList/ViewVocaList.tsx @@ -23,28 +23,20 @@ import { const handleWord = ( id: number, fetchFunction: (id: number) => Promise, - setVocaList: React.Dispatch>['voca']>>, + updateCheckCount: (id: number, callback: Function) => void, callback: (vocaCount: number) => number ) => async () => { await fetchFunction(id); - setVocaList((prev) => { - const newVocaList = prev.map(voca => { - if (voca.id === id) { - return { ...voca, checkCount: callback(voca.checkCount) }; - } - return voca; - }); - return newVocaList; - }); + updateCheckCount(id, callback); } function CheckableWord({word,checkCount,id}: {word: string, checkCount: number, id: number}) { const [loadingIncrease, fetchIncrease]= useFetchUpdate(increaseCheckCount); const [loadingDecrease, fetchDecrease] = useFetchUpdate(decreaseCheckCount); - const { setVocaList } = useContext(VocaListContext); + const { updateCheckCount } = useContext(VocaListContext); - const handleIncrease = handleWord(id, fetchIncrease, setVocaList, (vocaCount) => vocaCount+1); - const handleDecrease = handleWord(id, fetchDecrease, setVocaList, (vocaCount) => vocaCount-1); + const handleIncrease = handleWord(id, fetchIncrease, updateCheckCount, (vocaCount) => vocaCount+1); + const handleDecrease = handleWord(id, fetchDecrease, updateCheckCount, (vocaCount) => vocaCount-1); return (
{word} diff --git a/src/front/context/VocaListContext.ts b/src/front/context/VocaListContext.ts index 2482ae8..3a12103 100644 --- a/src/front/context/VocaListContext.ts +++ b/src/front/context/VocaListContext.ts @@ -12,6 +12,7 @@ const VocaListContext = createContext( loadingSaveVocaList: ReturnType['loadingSaveVocaList'], saveEditedVocaList: ReturnType['saveEditedVocaList'], excludeVoca: ReturnType['excludeVoca'], + updateCheckCount: ReturnType['updateCheckCount'] } ); diff --git a/src/front/hooks/useEditVocaList.ts b/src/front/hooks/useEditVocaList.ts index 130c42f..d3b5831 100644 --- a/src/front/hooks/useEditVocaList.ts +++ b/src/front/hooks/useEditVocaList.ts @@ -44,6 +44,7 @@ const useEditVocaList = () => { if(id !== null) { await fetchDeleteVoca(id); excludeVoca(id); + return setEditingVocaList(list=>list.filter((voca) => voca.id !== id)); } setEditingVocaList(editingVocaList.filter((_, index) => index !== i)); }, diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index 1bc7a52..5ca40e1 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -43,12 +43,24 @@ export default (wordbookId: string) => { }, [fetchSaveVocaList, wordbookId]); const excludeVoca = useCallback((id: number) => { - setVocaList(vocaList.filter(voca => voca.id !== id)); + setVocaList(list => list.filter(voca => voca.id !== id)); setWordbook({ ...wordbook, wordCount: wordbook.wordCount - 1 }); } , [vocaList]); + const updateCheckCount = useCallback((id: number, callback: Function) => { + setVocaList((prev) => { + const newVocaList = prev.map(voca => { + if (voca.id === id) { + return { ...voca, checkCount: callback(voca.checkCount) }; + } + return voca; + }); + return newVocaList; + }); + }, []); + return useMemo(() => ({ isLoading, wordbook, vocaList, - vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, + vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, editMode, viewMode, testMode, excludeVoca }), - [isLoading, wordbook, vocaList, vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, editMode, viewMode, testMode]); + [isLoading, wordbook, vocaList, vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, editMode, viewMode, testMode]); } From f0b8b37d589ef9a94285e9e83cfec12cd3b63dde Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 19:23:31 +0900 Subject: [PATCH 09/12] feat: create "delete and rename wordbook button" at vocalist page --- src/front/components/vocaList/VocaSidebar.tsx | 59 +++++++++++++++---- src/front/context/VocaListContext.ts | 13 +--- src/front/hooks/useVocaListData.ts | 37 +++++++++++- src/front/utils/apis/wordbook.ts | 2 +- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/front/components/vocaList/VocaSidebar.tsx b/src/front/components/vocaList/VocaSidebar.tsx index ee1e0ef..8b00a54 100644 --- a/src/front/components/vocaList/VocaSidebar.tsx +++ b/src/front/components/vocaList/VocaSidebar.tsx @@ -1,7 +1,7 @@ import { styled } from 'styled-components'; import { VocaMode } from "@utils/vocaModeEnum"; import { ISOStringToDateString } from '@utils'; -import { ButtonWithHoverAnimation, ReverseButtonWithHoverAnimation } from '@components'; +import { ButtonWithHoverAnimation, ReverseButtonWithHoverAnimation, Input,ButtonContainingIcon, ReverseButtonContainingIcon } from '@components'; import VocaListContext from '@context/VocaListContext'; import { useContext } from 'react'; @@ -56,8 +56,8 @@ const WordbookName = styled.div` margin-bottom: 10px; display: flex; align-items: center; - &>div { - display: inline-flex; + &>div{ + display: flex; width: 100%; align-items: center; border-bottom: 1px solid var(--main-color); @@ -70,7 +70,24 @@ const WordbookName = styled.div` } &>span:nth-child(2) { margin-left: auto; + } + &>span:nth-child(n+1) { cursor: pointer; + &:hover { + color: var(--main-color); + } + } + } + &>form { + display: flex; + align-items: center; + gap: 5px; + &>input { + width: 100%; + flex: 1; + } + &>button { + padding: 5px 10px; } } .material-icons-sharp { @@ -97,7 +114,9 @@ const WordbookInfo = styled.div` function VocaSidebar() { - const { wordbook, viewMode, testMode, vocaMode } = useContext(VocaListContext); + const { wordbook, viewMode, testMode, vocaMode, title, onChangeTitle, + loadingRenameWordbook, renameBook, loadingDeleteWordbook, removeBook, + isEditingWordbookName, startEditingWordbookName, cancelEditingWordbookName } = useContext(VocaListContext); return ( @@ -105,14 +124,32 @@ function VocaSidebar() { menu_book -
- - {wordbook.title} - - + { + !isEditingWordbookName&& +
+ {loadingDeleteWordbook?"삭제중":wordbook.title} + edit - -
+
+ + delete + +
+ } + { + isEditingWordbookName&& +
{ e.preventDefault(); renameBook();}}> + + 저장 + 취소 +
+ }
diff --git a/src/front/context/VocaListContext.ts b/src/front/context/VocaListContext.ts index 3a12103..57709bd 100644 --- a/src/front/context/VocaListContext.ts +++ b/src/front/context/VocaListContext.ts @@ -2,18 +2,7 @@ import { createContext } from 'react'; import useVocaListData from '@hooks/useVocaListData'; const VocaListContext = createContext( - {} as { - vocaList: ReturnType['vocaList'], - wordbook: ReturnType['wordbook'], - vocaMode: ReturnType['vocaMode'], - editMode: () => void, - viewMode: () => void, - testMode: () => void, - loadingSaveVocaList: ReturnType['loadingSaveVocaList'], - saveEditedVocaList: ReturnType['saveEditedVocaList'], - excludeVoca: ReturnType['excludeVoca'], - updateCheckCount: ReturnType['updateCheckCount'] - } + {} as Omit, "isLoading" | "vocaListError"> ); export default VocaListContext; diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index 5ca40e1..a45c4cd 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -4,6 +4,8 @@ import { getVocaList } from "@utils/apis/voca"; import { VocaMode } from "@utils/vocaModeEnum"; import useFetchUpdate from '@hooks/useFetchUpdate'; import { saveVocaList } from '@utils/apis/voca'; +import { renameWordbook, deleteWordbook } from '@utils/apis/wordbook'; +import { useNavigate } from 'react-router-dom'; type Voca = { word: string, meaning: string[], id: number | null }; @@ -16,6 +18,11 @@ export default (wordbookId: string) => { const [vocaList, setVocaList] = useState['voca']>([]); const [vocaMode, setVocaMode] = useState(VocaMode.EDIT); const [loadingSaveVocaList, fetchSaveVocaList] = useFetchUpdate(saveVocaList); + const [isEditingWordbookName, setIsEditingWordbookName] = useState(false); + const [title, setTitle] = useState(wordbook.title); + const [loadingRenameWordbook, fetchRenameWordbook] = useFetchUpdate(renameWordbook); + const [loadingDeleteWordbook, fetchDeleteWordbook] = useFetchUpdate(deleteWordbook); + const navigate = useNavigate(); useEffect(() => { if (data) { @@ -59,8 +66,34 @@ export default (wordbookId: string) => { }); }, []); + const onChangeTitle = useCallback((e: React.ChangeEvent) => setTitle(e.target.value), []); + + const renameBook = useCallback(async () => { + await fetchRenameWordbook(wordbookId, title); + setWordbook({ ...wordbook, title }); + setIsEditingWordbookName(false); + }, [fetchRenameWordbook, wordbookId]); + + const removeBook = useCallback(async () => { + if (!window.confirm('정말로 삭제하시겠습니까? 단어장 내부의 단어들도 모두 삭제됩니다.')) return; + await fetchDeleteWordbook(wordbookId); + navigate('/mywordbook'); + }, [fetchDeleteWordbook, wordbookId]); + + const startEditingWordbookName = useCallback(() => setIsEditingWordbookName(true), []); + const cancelEditingWordbookName = useCallback(() => { + setIsEditingWordbookName(false); + setTitle(wordbook.title); + }, [wordbook.title]); + return useMemo(() => ({ isLoading, wordbook, vocaList, - vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, + vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, title, onChangeTitle, + loadingRenameWordbook, renameBook, loadingDeleteWordbook, removeBook, + isEditingWordbookName, startEditingWordbookName, cancelEditingWordbookName, editMode, viewMode, testMode, excludeVoca }), - [isLoading, wordbook, vocaList, vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, editMode, viewMode, testMode]); + [isLoading, wordbook, vocaList, + vocaListError, vocaMode, loadingSaveVocaList, saveEditedVocaList, updateCheckCount, title, onChangeTitle, + loadingRenameWordbook, renameBook, loadingDeleteWordbook, removeBook, + isEditingWordbookName, startEditingWordbookName, cancelEditingWordbookName, + editMode, viewMode, testMode]); } diff --git a/src/front/utils/apis/wordbook.ts b/src/front/utils/apis/wordbook.ts index 3300dac..da9d301 100644 --- a/src/front/utils/apis/wordbook.ts +++ b/src/front/utils/apis/wordbook.ts @@ -62,7 +62,7 @@ export const deleteWordbook = apiErrorCatchWrapper(async (accessToken: string, b export const renameWordbook = apiErrorCatchWrapper(async (accessToken: string, bookId: string, title: string) => { const response = await axios.patch( - "/api/v1/wordbook", + "/api/v1/wordbook/name", { bookId, title }, { headers: { Authorization: `Bearer ${accessToken}` } } ); From 2adaeb906bdd720c079a412ae0f89a0dba475d90 Mon Sep 17 00:00:00 2001 From: raipen Date: Sun, 31 Mar 2024 20:35:21 +0900 Subject: [PATCH 10/12] feat: ellipsis to long titles, refactor: wordbook context --- src/front/components/index.tsx | 5 +++++ src/front/components/vocaList/VocaSidebar.tsx | 8 ++++++-- src/front/components/wordbook/WordbookElement.tsx | 1 + src/front/context/WordbookListContext.ts | 9 +-------- src/front/hooks/useVocaListData.ts | 1 + src/front/pages/MyWordbook.tsx | 8 +++----- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/front/components/index.tsx b/src/front/components/index.tsx index f1c75de..6d3583d 100644 --- a/src/front/components/index.tsx +++ b/src/front/components/index.tsx @@ -238,12 +238,17 @@ export const WordbookInfo = styled.div` ${FlexColumnLeftStart}; font-size: 1rem; font-weight: 300; + max-width: calc(100% - 50px); color: var(--muted-text-color); `; export const WordbookName = styled.div` font-size: 1.5rem; font-weight: 600; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; &>a:last-child { color: var(--main-color); &:hover { diff --git a/src/front/components/vocaList/VocaSidebar.tsx b/src/front/components/vocaList/VocaSidebar.tsx index 8b00a54..7b70cc2 100644 --- a/src/front/components/vocaList/VocaSidebar.tsx +++ b/src/front/components/vocaList/VocaSidebar.tsx @@ -58,7 +58,7 @@ const WordbookName = styled.div` align-items: center; &>div{ display: flex; - width: 100%; + width: calc(100% - 16px); align-items: center; border-bottom: 1px solid var(--main-color); padding-left: 5px; @@ -67,11 +67,15 @@ const WordbookName = styled.div` font-size: 1.5rem; font-weight: 600; color: var(--main-color); + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } &>span:nth-child(2) { margin-left: auto; } - &>span:nth-child(n+1) { + &>span:nth-child(n+2) { cursor: pointer; &:hover { color: var(--main-color); diff --git a/src/front/components/wordbook/WordbookElement.tsx b/src/front/components/wordbook/WordbookElement.tsx index d7c69f2..403322d 100644 --- a/src/front/components/wordbook/WordbookElement.tsx +++ b/src/front/components/wordbook/WordbookElement.tsx @@ -26,6 +26,7 @@ function WordbookElement({ wordbook: { uuid, title, isHidden, createdAt, vocaCou menu_book {title} + diff --git a/src/front/context/WordbookListContext.ts b/src/front/context/WordbookListContext.ts index d0af89f..d5d3fe6 100644 --- a/src/front/context/WordbookListContext.ts +++ b/src/front/context/WordbookListContext.ts @@ -1,15 +1,8 @@ import { createContext } from 'react'; import useWordbookData from '@hooks/useWordbookData'; -type returnData = ReturnType; - const WordbookListContext = createContext( - {} as { - profile: returnData['profile'], - onClickWordbookElement: returnData['onClickWordbookElement'], - expend: returnData['expend'], - expendOnClick: returnData['expendOnClick'], - } + {} as Omit, 'Error'|'wordbookList'|'hiddenWordbookList'> ); export default WordbookListContext; diff --git a/src/front/hooks/useVocaListData.ts b/src/front/hooks/useVocaListData.ts index a45c4cd..0f4444f 100644 --- a/src/front/hooks/useVocaListData.ts +++ b/src/front/hooks/useVocaListData.ts @@ -27,6 +27,7 @@ export default (wordbookId: string) => { useEffect(() => { if (data) { setWordbook({ ...data.wordbook, wordCount: data.voca.length }); + setTitle(data.wordbook.title); setVocaList(data.voca); setIsLoading(false); if(data.voca.length > 0) setVocaMode(VocaMode.VIEW); diff --git a/src/front/pages/MyWordbook.tsx b/src/front/pages/MyWordbook.tsx index a831064..5cc45b3 100644 --- a/src/front/pages/MyWordbook.tsx +++ b/src/front/pages/MyWordbook.tsx @@ -8,11 +8,9 @@ import HiddenWordbookTitle from "@components/wordbook/HiddenWordbookTitle"; import WordbookElement from "@components/wordbook/WordbookElement"; import { WordbookListContainer } from "@components"; import ErrorConfigs from "@errors/config"; -import { useMemo } from "react"; function MyWordbook() { - const { profile, wordbookList, hiddenWordbookList, onClickWordbookElement, Error, expend, expendOnClick } = useWordbookData(); - const contextValue = useMemo(() => ({ profile, onClickWordbookElement, expend, expendOnClick }), [profile, onClickWordbookElement, expend, expendOnClick]); + const { wordbookList, hiddenWordbookList, Error, ...rest } = useWordbookData(); if(Error) { const errorConfig = ErrorConfigs[Error.name]; if(errorConfig) @@ -20,7 +18,7 @@ function MyWordbook() { return } return ( - + @@ -29,7 +27,7 @@ function MyWordbook() { - {expend&&hiddenWordbookList.map((wordbook, index) =>)} + {rest.expend&&hiddenWordbookList.map((wordbook, index) =>)} From 53941e72e8dbd6dcf44b07009ced240010814f6f Mon Sep 17 00:00:00 2001 From: raipen Date: Mon, 1 Apr 2024 01:18:20 +0900 Subject: [PATCH 11/12] feat: create "delete and rename wordbook button" at wordbook page --- src/front/components/index.tsx | 8 ++- .../components/wordbook/WordbookElement.tsx | 55 ++++++++++++++++--- src/front/hooks/useWordbookData.ts | 4 +- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/front/components/index.tsx b/src/front/components/index.tsx index 6d3583d..d406bd8 100644 --- a/src/front/components/index.tsx +++ b/src/front/components/index.tsx @@ -219,6 +219,7 @@ export const WordbookContainer = styled.div` margin-bottom: 10px; border-top: 1px solid var(--main-color); padding: 10px; + gap: 10px; `; export const AddWordbookContainer = styled(WordbookContainer)` @@ -228,7 +229,10 @@ export const AddWordbookContainer = styled(WordbookContainer)` export const WordbookMenu = styled.div` ${FlexColumnCenter}; - ${clickable}; + &>span{ + ${clickable}; + font-size: 1.2rem; + } color: var(--muted-text-color); font-weight: 300; gap: 10px; @@ -239,6 +243,7 @@ export const WordbookInfo = styled.div` font-size: 1rem; font-weight: 300; max-width: calc(100% - 50px); + flex: 1; color: var(--muted-text-color); `; @@ -258,6 +263,7 @@ export const WordbookName = styled.div` } &>input{ font-weight: 600; + width: calc(100% - 21px); } &>.material-icons-sharp{ font-size: 1rem; diff --git a/src/front/components/wordbook/WordbookElement.tsx b/src/front/components/wordbook/WordbookElement.tsx index 403322d..ab59051 100644 --- a/src/front/components/wordbook/WordbookElement.tsx +++ b/src/front/components/wordbook/WordbookElement.tsx @@ -1,8 +1,8 @@ -import { WordbookInfo, WordbookMenu, WordbookName, WordbookContainer } from '../index'; +import { WordbookInfo, WordbookMenu, WordbookName, WordbookContainer, Input, ButtonContainingIcon, ReverseButtonContainingIcon } from '../index'; import { Link } from 'react-router-dom'; import useFetchUpdate from "@hooks/useFetchUpdate"; -import { hideWordbook,showWordbook } from '@utils/apis/wordbook'; -import { useContext } from 'react'; +import { hideWordbook,showWordbook, deleteWordbook, renameWordbook } from '@utils/apis/wordbook'; +import { useContext,useState } from 'react'; import WordbookListContext from '@context/WordbookListContext'; import WordbookDetailInfo from './WordbookDetailInfo'; @@ -16,8 +16,37 @@ function WordbookElement({ wordbook: { uuid, title, isHidden, createdAt, vocaCou } }) { const [loadingWordbook, fetchWordbook] = useFetchUpdate(isHidden ? showWordbook : hideWordbook); + const [loadingDelete, fetchDelete] = useFetchUpdate(deleteWordbook); + const [loadingRename, fetchRename] = useFetchUpdate(renameWordbook); + const [isEditing, setIsEditing] = useState(false); + const [newTitle, setNewTitle] = useState(title); + const { onClickWordbookElement } = useContext(WordbookListContext); + const onChangeTitle = (e: React.ChangeEvent) => { + setNewTitle(e.target.value); + } + + const onClickDelete = async () => { + if (!window.confirm('정말로 삭제하시겠습니까? 단어장 내부의 단어들도 모두 삭제됩니다.')) return; + await onClickWordbookElement(fetchDelete, uuid)(); + } + + const startRename = () => { + setIsEditing(true); + setNewTitle(title); + } + const cancelRename = () => { + setIsEditing(false); + setNewTitle(title); + } + const onClickRename = async () => { + await onClickWordbookElement(fetchRename, uuid, newTitle)(); + setIsEditing(false); + } + const onClickVisibility = onClickWordbookElement(fetchWordbook, uuid); + + const isLoading = loadingWordbook || loadingDelete || loadingRename; return ( @@ -25,15 +54,25 @@ function WordbookElement({ wordbook: { uuid, title, isHidden, createdAt, vocaCou menu_book - {title} - + {!isEditing&&{title}} + {isEditing&&} - {!loadingWordbook && {isHidden ? "visibility" : "visibility_off"}} - {loadingWordbook && hourglass_bottom} - {loadingWordbook && 이동 중} + {!isLoading && !isEditing && [ + !isHidden&&edit, + {isHidden ? "visibility" : "visibility_off"}, + isHidden&&delete, + ]} + {loadingWordbook && [hourglass_bottom, 이동 중]} + {loadingDelete && [hourglass_bottom, 삭제 중]} + {isEditing && !loadingRename && [ + 변경, + 취소 + ]} + {loadingRename && [hourglass_bottom, 변경 중]} + ) diff --git a/src/front/hooks/useWordbookData.ts b/src/front/hooks/useWordbookData.ts index dd8d81d..8632cdc 100644 --- a/src/front/hooks/useWordbookData.ts +++ b/src/front/hooks/useWordbookData.ts @@ -27,9 +27,9 @@ export default () => { const {wordbookList, hiddenWordbookList} = wordbook; const onClickWordbookElement = useCallback( - (fetchFunction: (args:T) => Promise, args: T) => + >(fetchFunction: (...args:T) => Promise, ...args: T) => async () => { - const wordbook = await fetchFunction(args); + const wordbook = await fetchFunction(...args); const newProfile = await fetchGetProfile(); setWordbook(wordbook); setProfile(newProfile); From cac330c13cd66bb3a702b30ae653e193fcb6b71b Mon Sep 17 00:00:00 2001 From: raipen Date: Mon, 1 Apr 2024 01:56:11 +0900 Subject: [PATCH 12/12] feat: add placeholder at voca list --- src/front/components/vocaList/EditVocaList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/front/components/vocaList/EditVocaList.tsx b/src/front/components/vocaList/EditVocaList.tsx index f311b21..f3539a8 100644 --- a/src/front/components/vocaList/EditVocaList.tsx +++ b/src/front/components/vocaList/EditVocaList.tsx @@ -33,7 +33,7 @@ function WordInputWithMenu({word, onChange, disabled, moveWordUp, moveWordDown,
arrow_upward arrow_downward - + delete_forever
); @@ -68,7 +68,7 @@ function EditVocaList() { {voca.meaning.map((m,j) =>( - + remove_circle_outline ))}