From 4ba94c8b13cabb933a3fba0a7d9eaa23f6e4ff32 Mon Sep 17 00:00:00 2001 From: Jiseok Woo <115205098+jisurk@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:19:45 +0900 Subject: [PATCH] =?UTF-8?q?FE-71=20=F0=9F=94=80=20=EC=97=90=ED=94=BC?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EC=9E=91=EC=84=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FE-64๐Ÿ’„ ๊ธ€์ž‘์„ฑ ํŽ˜์ด์ง€ UI์ถ”๊ฐ€ (#44) * FE-72 โœจ ์—ํ”ผ๊ทธ๋žจ ๋“ฑ๋ก api์—ฐ๋™ (#52) * FE-72โœจ ๊ธ€์ž‘์„ฑํŽ˜์ด์ง€ ์Šคํ‚ค๋งˆ ์ถ”๊ฐ€ * FE-72โœจ formํƒœ๊ทธ Form์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€๊ฒฝ * FE-72โœจ ํƒœ๊ทธ ์ €์žฅ๊ธฐ๋Šฅ ์ถ”๊ฐ€ * FE-72โœจ ์—ํ”ผ๊ทธ๋žจ ๋“ฑ๋ก api์—ฐ๋™ * FE-72โœจ ์—ํ”ผ๊ทธ๋žจ ๋“ฑ๋ก์‹œ ํ•ด๋‹น ์—ํ”ผ๊ทธ๋žจ ํŽ˜์ด์ง€๋กœ ์ด๋™ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ * FE-72โœจ ๋“ฑ๋ก ์ค‘์ผ๋•Œ์˜ ๋กœ์ง์ถ”๊ฐ€ * FE-72โœจ toast-> alert-dailog๋กœ ๋ณ€๊ฒฝ * FE-72๐Ÿ“ TODO์ฃผ์„ ์ถ”๊ฐ€ --------- Co-authored-by: ์šฐ์ง€์„ * FE-73โœจ ์œ ํšจ์„ฑ๊ฒ€์‚ฌ ์ถ”๊ฐ€ (#66) * FE-73โ™ป๏ธ Tag๊ด€๋ฆฌ ํ•จ์ˆ˜ ํ›…์œผ๋กœ ๋ถ„๋ฆฌ * FE-73โœจ RadioGroup ๋กœ์ง ์ˆ˜์ • * FE-73โœจ ์œ ํšจ์„ฑ๊ฒ€์‚ฌ ์ถ”๊ฐ€ * FE-73โ™ป๏ธ ์ €์ž ๋ณธ์ธ ์„ ํƒ์‹œ์˜ ๋กœ์ง ๋ณ€๊ฒฝ * FE-73โœจ ์ค‘๋ณต ํƒœ๊ทธ ๊ฒ€์‚ฌ ๋กœ์ง ์ถ”๊ฐ€ * FE-73โ™ป๏ธ ์ถœ์ฒ˜ ์œ ํšจ์„ฑ(optional)๊ฒ€์‚ฌ ์ˆ˜์ • * FE-73โœจ ํ•„์ˆ˜ํ•ญ๋ชฉ ์ž…๋ ฅํ–ˆ์„๋•Œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” * FE-73๐Ÿ› ํƒœ๊ทธ๋ฅผ ์ž…๋ ฅํ–ˆ๋‹ค๊ฐ€ ์ง€์› ์„๋•Œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”๋˜์žˆ๋Š” ๋ฒ„๊ทธ ์ˆ˜์ • * FE-73๐Ÿ› useEffect ์˜์กด์„ฑ๋ฐฐ์—ด lint problem ํ•ด๊ฒฐ * FE-73๐Ÿ› url์œ ํšจ์„ฑ๊ฒ€์‚ฌ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€ ์•ˆ๋œจ๋Š” ๋ฒ„๊ทธ ์ˆ˜์ • --------- Co-authored-by: ์šฐ์ง€์„ * FE-71โ™ป๏ธ epic๋ธŒ๋žœ์น˜ ์ฝ”๋“œ๋ฆฌ๋ทฐ ๋ฐ˜์˜ (#76) * FE-71โ™ป๏ธ token,interceptor ๋กœ์ง ์ˆ˜์ • * FE-71โ™ป๏ธ AddEpigram ์ฝ”๋“œ๋ฆฌ๋ทฐ ๋ฐ˜์˜ * FE-71๐Ÿ”ฅ ํ…Œ์ŠคํŠธ์šฉ ์ƒ์„ธํŽ˜์ด์ง€ ์‚ญ์ œ * FE-71โ™ป๏ธ onKeyDown -> onKeyUp ์ˆ˜์ • --------- Co-authored-by: ์šฐ์ง€์„ --- src/apis/add.ts | 9 + src/apis/index.ts | 4 +- src/hooks/epigramQueryHook.ts | 24 ++ src/hooks/useTagManagementHook.ts | 47 ++++ src/pageLayout/Epigram/AddEpigram.tsx | 316 ++++++++++++++++++++++++++ src/pages/addEpigram.tsx | 7 + src/schema/addEpigram.ts | 44 ++++ 7 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 src/apis/add.ts create mode 100644 src/hooks/epigramQueryHook.ts create mode 100644 src/hooks/useTagManagementHook.ts create mode 100644 src/pageLayout/Epigram/AddEpigram.tsx create mode 100644 src/pages/addEpigram.tsx create mode 100644 src/schema/addEpigram.ts diff --git a/src/apis/add.ts b/src/apis/add.ts new file mode 100644 index 00000000..66a6b010 --- /dev/null +++ b/src/apis/add.ts @@ -0,0 +1,9 @@ +import { AddEpigramRequestType, AddEpigramResponseType } from '@/schema/addEpigram'; +import httpClient from '.'; + +const postEpigram = async (request: AddEpigramRequestType): Promise => { + const response = await httpClient.post('/epigrams', request); + return response.data; +}; + +export default postEpigram; diff --git a/src/apis/index.ts b/src/apis/index.ts index 4167407d..e58d8047 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -7,8 +7,6 @@ const httpClient = axios.create({ paramsSerializer: (parameters) => qs.stringify(parameters, { arrayFormat: 'repeat', encode: false }), }); -export default httpClient; - // NOTE: eslint-disable no-param-reassign ๋ฏธํ•ด๊ฒฐ๋กœ ์ธํ•œ ์„ค์ • httpClient.interceptors.request.use((config) => { const accessToken = localStorage.getItem('accessToken'); @@ -50,3 +48,5 @@ httpClient.interceptors.response.use( return Promise.reject(error); }, ); + +export default httpClient; diff --git a/src/hooks/epigramQueryHook.ts b/src/hooks/epigramQueryHook.ts new file mode 100644 index 00000000..e2ca6679 --- /dev/null +++ b/src/hooks/epigramQueryHook.ts @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AddEpigramFormType, AddEpigramResponseType } from '@/schema/addEpigram'; +import { MutationOptions } from '@/types/query'; +import postEpigram from '@/apis/add'; +import { AxiosError } from 'axios'; + +// TODO: ์—ํ”ผ๊ทธ๋žจ ์ˆ˜์ •๊ณผ ์‚ญ์ œ์—๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ›… ์ˆ˜์ • ์˜ˆ์ • + +const useAddEpigram = (options?: MutationOptions) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newEpigram: AddEpigramFormType) => postEpigram(newEpigram), + ...options, + onSuccess: (...args) => { + queryClient.invalidateQueries({ queryKey: ['epigrams'] }); + if (options?.onSuccess) { + options.onSuccess(...args); + } + }, + }); +}; + +export default useAddEpigram; diff --git a/src/hooks/useTagManagementHook.ts b/src/hooks/useTagManagementHook.ts new file mode 100644 index 00000000..dd0082de --- /dev/null +++ b/src/hooks/useTagManagementHook.ts @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { UseFormSetValue, UseFormGetValues, UseFormSetError } from 'react-hook-form'; +import { AddEpigramFormType } from '@/schema/addEpigram'; + +// NOTE: setError๋ฉ”์„œ๋“œ๋กœ FormField์— ์—๋Ÿฌ ์„ค์ • ๊ฐ€๋Šฅ +const useTagManagement = ({ + setValue, + getValues, + setError, +}: { + setValue: UseFormSetValue; + getValues: UseFormGetValues; + setError: UseFormSetError; +}) => { + const [currentTag, setCurrentTag] = useState(''); + + const handleAddTag = () => { + if (!currentTag || currentTag.length > 10) { + return; + } + const currentTags = getValues('tags') || []; + + if (currentTags.length >= 3) { + return; + } + if (currentTags.includes(currentTag)) { + setError('tags', { type: 'manual', message: '์ด๋ฏธ ์ €์žฅ๋œ ํƒœ๊ทธ์ž…๋‹ˆ๋‹ค.' }); + return; + } + + setValue('tags', [...currentTags, currentTag]); + setCurrentTag(''); + setError('tags', { type: 'manual', message: '' }); + }; + + const handleRemoveTag = (tagToRemove: string) => { + const currentTags = getValues('tags') || []; + setValue( + 'tags', + currentTags.filter((tag) => tag !== tagToRemove), + ); + }; + + return { currentTag, setCurrentTag, handleAddTag, handleRemoveTag }; +}; + +export default useTagManagement; diff --git a/src/pageLayout/Epigram/AddEpigram.tsx b/src/pageLayout/Epigram/AddEpigram.tsx new file mode 100644 index 00000000..f314c730 --- /dev/null +++ b/src/pageLayout/Epigram/AddEpigram.tsx @@ -0,0 +1,316 @@ +import React, { KeyboardEvent, useCallback, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Header from '@/components/Header/Header'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Textarea } from '@/components/ui/textarea'; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; +import { AddEpigramFormSchema, AddEpigramFormType } from '@/schema/addEpigram'; +import useAddEpigram from '@/hooks/epigramQueryHook'; +import { useRouter } from 'next/router'; +import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; +import useTagManagement from '@/hooks/useTagManagementHook'; +import { useMeQuery } from '@/hooks/userQueryHooks'; + +function AddEpigram() { + const router = useRouter(); + const { data: userData, isPending, isError } = useMeQuery(); + const [isAlertOpen, setIsAlertOpen] = useState(false); + const [alertContent, setAlertContent] = useState({ title: '', description: '' }); + const [selectedAuthorOption, setSelectedAuthorOption] = useState('directly'); // ๊ธฐ๋ณธ๊ฐ’์„ 'directly'๋กœ ์„ค์ • + const [isFormValid, setIsFormValid] = useState(false); + + const form = useForm({ + resolver: zodResolver(AddEpigramFormSchema), + defaultValues: { + content: '', + author: '', + referenceTitle: '', + referenceUrl: '', + tags: [], + }, + }); + + // NOTE: ํ•„์ˆ˜ํ•ญ๋ชฉ๋“ค์— ๊ฐ’์ด ๋“ค์–ด์žˆ๋Š”์ง€ ํ™•์ธ ํ•จ์ˆ˜ + const checkFormEmpty = useCallback(() => { + const { content, author, tags } = form.getValues(); + return content.trim() !== '' && author.trim() !== '' && tags.length > 0; + }, [form]); + + // NOTE: form๊ฐ’์ด ๋ณ€๊ฒฝ๋ ๋•Œ ํ•„์ˆ˜ํ•ญ๋ชฉ๋“ค์ด ๋“ค์–ด์žˆ๋Š”์ง€ ํ™•์ธ + const watchForm = useCallback(() => { + setIsFormValid(checkFormEmpty()); + }, [checkFormEmpty]); + + useEffect(() => { + const subscription = form.watch(watchForm); + return () => subscription.unsubscribe(); + }, [form, watchForm]); + + const { currentTag, setCurrentTag, handleAddTag, handleRemoveTag } = useTagManagement({ + setValue: form.setValue, + getValues: form.getValues, + setError: form.setError, + }); + const addEpigramMutation = useAddEpigram({ + onSuccess: () => { + setAlertContent({ + title: '๋“ฑ๋ก ์™„๋ฃŒ', + description: '๋“ฑ๋ก์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', + }); + setIsAlertOpen(true); + form.reset(); + }, + onError: () => { + setAlertContent({ + title: '๋“ฑ๋ก ์‹คํŒจ', + description: '๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.', + }); + setIsAlertOpen(true); + }, + }); + + const handleAlertClose = () => { + setIsAlertOpen(false); + if (alertContent.title === '๋“ฑ๋ก ์™„๋ฃŒ') { + router.push(`/epigram/${addEpigramMutation.data?.id}`); + } + }; + + const AUTHOR_OPTIONS = [ + { value: 'directly', label: '์ง์ ‘ ์ž…๋ ฅ' }, + { value: 'unknown', label: '์•Œ ์ˆ˜ ์—†์Œ' }, + { value: 'me', label: '๋ณธ์ธ' }, + ]; + + // NOTE: default๋ฅผ ์ง์ ‘ ์ž…๋ ฅ์œผ๋กœ ์„ค์ • + // NOTE: ๋ณธ์ธ์„ ์„ ํƒ ์‹œ ์œ ์ €์˜ nickname์ด ๋“ค์–ด๊ฐ + const handleAuthorChange = async (value: string) => { + setSelectedAuthorOption(value); + let authorValue: string; + + switch (value) { + case 'unknown': + authorValue = '์•Œ ์ˆ˜ ์—†์Œ'; + break; + case 'me': + if (isPending) { + authorValue = '๋กœ๋”ฉ ์ค‘...'; + } else if (userData) { + authorValue = userData.nickname; + } else { + authorValue = '๋ณธ์ธ (์ •๋ณด ์—†์Œ)'; + } + break; + default: + authorValue = ''; + } + form.setValue('author', authorValue); + }; + + if (isPending) { + return
์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...
; + } + + if (isError) { + return
์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ฃผ์„ธ์š”.
; + } + + // NOTE: ํƒœ๊ทธ๋ฅผ ์ €์žฅํ•˜๋ ค๊ณ  ํ• ๋•Œ enterํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ํผ์ œ์ถœ์ด ๋˜๋Š”๊ฑธ ๋ฐฉ์ง€ + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddTag(); + } + }; + + // NOTE: url์™€title์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ด ์•„๋‹ˆ๋ผ์„œ ๋นˆ์นธ์œผ๋กœ ์ œ์ถœํ•  ๋•Œ ํ•ญ๋ชฉ์—์„œ ์ œ์™ธ + const handleSubmit = (data: AddEpigramFormType) => { + const submitData = { ...data }; + + if (!submitData.referenceUrl) { + delete submitData.referenceUrl; + } + + if (!submitData.referenceTitle) { + delete submitData.referenceTitle; + } + + addEpigramMutation.mutate(submitData); + }; + + return ( + <> +
{}} /> +
+
+ + ( + + + ๋‚ด์šฉ + * + + +