Skip to content

Commit

Permalink
FE-73✨ 유효성검사 추가 (#66)
Browse files Browse the repository at this point in the history
* 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: 우지석 <[email protected]>
  • Loading branch information
jisurk and 우지석 authored Jul 24, 2024
1 parent 613693b commit 0b2a556
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 84 deletions.
3 changes: 1 addition & 2 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import axios from 'axios';
import qs from 'qs';

const getToken = () =>
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjUsInRlYW1JZCI6IjUtOSIsInNjb3BlIjoiYWNjZXNzIiwiaWF0IjoxNzIxNjE2Mjk3LCJleHAiOjE3MjE2MTgwOTcsImlzcyI6InNwLWVwaWdyYW0ifQ.kHIq9gdLbu2tE2H8VZXJ9xKQfVA95G9RY251qfXvJy8';
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjUsInRlYW1JZCI6IjUtOSIsInNjb3BlIjoiYWNjZXNzIiwiaWF0IjoxNzIxODAxOTU1LCJleHAiOjE3MjE4MDM3NTUsImlzcyI6InNwLWVwaWdyYW0ifQ.z_QkXSBKp6gsWH7qs_wdNqQcbzIKAiJieihWfY9LZWY';

const httpClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
headers: { 'Content-Type': 'application/json' },
paramsSerializer: (parameters) => qs.stringify(parameters, { arrayFormat: 'repeat', encode: false }),
});

// NOTE: 유민님 interceptor 사용!
httpClient.interceptors.request.use(
(config) => {
const newConfig = { ...config };
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/useTagManagementHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from 'react';
import { UseFormSetValue, UseFormGetValues, UseFormSetError } from 'react-hook-form';
import { AddEpigramFormType } from '@/schema/addEpigram';

// NOTE: setError메서드로 FormField에 에러 설정 가능
const useTagManagement = (setValue: UseFormSetValue<AddEpigramFormType>, getValues: UseFormGetValues<AddEpigramFormType>, setError: UseFormSetError<AddEpigramFormType>) => {
const [currentTag, setCurrentTag] = useState('');

const handleAddTag = () => {
if (currentTag && currentTag.length <= 10) {
const currentTags = getValues('tags') || [];
if (currentTags.length < 3) {
// NOTE: 중복된 태그가 있는지 확인
if (currentTags.includes(currentTag)) {
setError('tags', { type: 'manual', message: '이미 저장된 태그입니다.' });
} else {
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;
185 changes: 112 additions & 73 deletions src/pageLayout/Epigram/AddEpigram.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { KeyboardEvent, useState } from 'react';
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';
Expand All @@ -11,12 +11,16 @@ 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 [currentTag, setCurrentTag] = useState('');
const router = useRouter();
const { data: userData, isPending, isError } = useMeQuery();
const [isAlertOpen, setIsAlertOpen] = useState(false);
const [alertContent, setAlertContent] = useState({ title: '', description: '' });
const router = useRouter();
const [selectedAuthorOption, setSelectedAuthorOption] = useState('directly'); // 기본값을 'directly'로 설정
const [isFormValid, setIsFormValid] = useState(false);

const form = useForm<AddEpigramFormType>({
resolver: zodResolver(AddEpigramFormSchema),
Expand All @@ -29,12 +33,23 @@ function AddEpigram() {
},
});

const handleAlertClose = () => {
setIsAlertOpen(false);
if (alertContent.title === '등록 완료') {
router.push(`/epigram/${addEpigramMutation.data?.id}`);
}
};
// 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(form.setValue, form.getValues, form.setError);

const addEpigramMutation = useAddEpigram({
onSuccess: () => {
Expand All @@ -45,52 +60,84 @@ function AddEpigram() {
setIsAlertOpen(true);
form.reset();
},
// TODO : 유효성검사 브랜치만들어서 alert창에 유효성검사 틀린부분을 보이게 할 예정
onError: () => {
setAlertContent({
title: '등록 실패',
description: '다시 확인해주세요.',
description: '다시 시도해주세요.',
});
setIsAlertOpen(true);
},
});

// TODO : 태그 관리 로직 분리 예정
const handleAddTag = () => {
if (currentTag && currentTag.length <= 10) {
const currentTags = form.getValues('tags') || [];
if (currentTags.length < 3) {
form.setValue('tags', [...currentTags, currentTag]);
setCurrentTag('');
}
const handleAlertClose = () => {
setIsAlertOpen(false);
if (alertContent.title === '등록 완료') {
router.push(`/epigram/${addEpigramMutation.data?.id}`);
}
};

const handleRemoveTag = (tagToRemove: string) => {
const currentTags = form.getValues('tags') || [];
form.setValue(
'tags',
currentTags.filter((tag) => tag !== tagToRemove),
);
const authorOptions = [
{ value: 'directly', label: '직접 입력' },
{ value: 'unknown', label: '알 수 없음' },
{ value: 'me', label: '본인' },
];

// NOTE: default를 직접 입력으로 설정
// NOTE: 본인을 선택 시 유저의 nickname이 들어감
const handleAuthorChange = async (value: string) => {
setSelectedAuthorOption(value);
let authorValue = '';
if (value === 'unknown') {
authorValue = '알 수 없음';
} else if (value === 'me') {
if (isPending) {
authorValue = '로딩 중...';
} else if (userData) {
authorValue = userData.nickname;
} else {
authorValue = '본인 (정보 없음)';
}
}
form.setValue('author', authorValue);
};

if (isPending) {
return <div>사용자 정보를 불러오는 중...</div>;
}

if (isError) {
return <div>사용자 정보를 불러오는 데 실패했습니다. 페이지를 새로고침 해주세요.</div>;
}

// NOTE: 태그를 저장하려고 할때 enter키를 누르면 폼제출이 되는걸 방지
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
};

// NOTE: url와title은 필수 항목이 아니라서 빈칸으로 제출할 때 항목에서 제외
const handleSubmit = (data: AddEpigramFormType) => {
addEpigramMutation.mutate(data);
const submitData = { ...data };

if (!submitData.referenceUrl) {
delete submitData.referenceUrl;
}

if (!submitData.referenceTitle) {
delete submitData.referenceTitle;
}

addEpigramMutation.mutate(submitData);
};

return (
<>
<Header icon='search' routerPage='/search' isLogo insteadOfLogo='' isProfileIcon isShareIcon={false} isButton={false} textInButton='' disabled={false} onClick={() => {}} />
<div className='border-t-2 w-full flex flex-col justify-center items-center'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='flex flex-col justify-center item-center gap-8 w-[312px] md:w-[384px] lg:w-[640px] py-6'>
<form onSubmit={form.handleSubmit(handleSubmit)} className='flex flex-col justify-center item-center gap-6 lg:gap-8 w-[312px] md:w-[384px] lg:w-[640px] py-6'>
<FormField
control={form.control}
name='content'
Expand All @@ -103,62 +150,50 @@ function AddEpigram() {
<FormControl>
<Textarea className='h-[132px] lg:h-[148px] lg:text-xl border-blue-300 border-2 rounded-xl resize-none p-2' id='content' placeholder='500자 이내로 입력해주세요.' {...field} />
</FormControl>
<FormMessage />
<FormMessage className='text-state-error text-right' />
</FormItem>
)}
/>

<div className='flex flex-col gap-2 lg:gap-4'>
<FormLabel className='text-semibold lg:text-2xl text-black-600'>
저자
<span className='text-state-error'>*</span>
</FormLabel>
{/* TODO: 라디오그룹 로직 수정 예정 */}
<RadioGroup
onValueChange={(value) => {
if (value === 'unknown') form.setValue('author', '알 수 없음');
else if (value === 'me') form.setValue('author', '본인');
else form.setValue('author', '');
}}
>
<RadioGroup onValueChange={handleAuthorChange} value={selectedAuthorOption}>
<div className='flex gap-2'>
<div className='flex items-center space-x-2 text-xl'>
<RadioGroupItem value='directly' id='directly' />
<FormLabel htmlFor='directly' className='font-medium lg:text-xl'>
직접 입력
</FormLabel>
</div>
<div className='flex items-center space-x-2'>
<RadioGroupItem value='unknown' id='unknown' />
<FormLabel htmlFor='unknown' className='font-medium lg:text-xl'>
알 수 없음
</FormLabel>
</div>
<div className='flex items-center space-x-2 text-xl'>
<RadioGroupItem value='me' id='me' />
<FormLabel htmlFor='me' className='font-medium lg:text-xl'>
본인
</FormLabel>
</div>
{authorOptions.map((option) => (
<div key={option.value} className='flex items-center space-x-2 text-xl'>
<RadioGroupItem value={option.value} id={option.value} />
<FormLabel htmlFor={option.value} className='font-medium lg:text-xl'>
{option.label}
</FormLabel>
</div>
))}
</div>
</RadioGroup>
</div>

<FormField
control={form.control}
name='author'
render={({ field }) => (
<FormItem>
<FormControl>
<Input className='w-full h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2' id='author' type='text' placeholder='저자 이름 입력' {...field} />
{/* NOTE: 직접 입력 radio버튼을 선택하지않으면 수정 불가 */}
<Input
className='w-full h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2'
id='author'
type='text'
placeholder='저자 이름 입력'
{...field}
disabled={selectedAuthorOption !== 'directly'}
/>
</FormControl>
<FormMessage />
<FormMessage className='text-state-error text-right' />
</FormItem>
)}
/>

<fieldset className='flex flex-col gap-2 lg:gap-4'>
<legend className='text-semibold lg:text-2xl text-black-600'>출처</legend>
<legend className='text-semibold lg:text-2xl text-black-600 mb-1'>출처</legend>
<FormField
control={form.control}
name='referenceTitle'
Expand All @@ -174,7 +209,7 @@ function AddEpigram() {
{...field}
/>
</FormControl>
<FormMessage />
<FormMessage className='text-state-error text-right' />
</FormItem>
)}
/>
Expand All @@ -187,25 +222,25 @@ function AddEpigram() {
<Input
className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2'
id='referenceUrl'
type='url'
type='text'
placeholder='URL (ex.http://www.website.com)'
aria-label='출처 URL'
{...field}
/>
</FormControl>
<FormMessage />
<FormMessage className='text-state-error text-right' />
</FormItem>
)}
/>
</fieldset>

<FormField
control={form.control}
name='tags'
render={({ field }) => (
<FormItem className='flex flex-col gap-2 lg:gap-4'>
<FormLabel htmlFor='tags' className='text-semibold lg:text-2xl text-black-600'>
태그
<span className='text-state-error'>*</span>
</FormLabel>
<div className='relative'>
<Input
Expand All @@ -214,10 +249,14 @@ function AddEpigram() {
type='text'
placeholder='입력하여 태그 추가(최대10자)'
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
onChange={(e) => {
setCurrentTag(e.target.value);
form.clearErrors('tags');
}}
onKeyDown={handleKeyDown}
maxLength={10}
/>

<Button
type='button'
className='absolute right-2 top-1/2 transform -translate-y-1/2 h-8 px-3 bg-blue-500 text-white rounded'
Expand All @@ -227,23 +266,23 @@ function AddEpigram() {
저장
</Button>
</div>
{/* TODO: 태그 key값 수정 예정 */}
{/* NOTE: 지금은 똑같은 태그를 입력했을때 하나를 지우면 다 지워짐 */}
<FormMessage className='text-state-error text-right' />
{/* NOTE: 태그의 키값을 변경하는 대신 중복된 태그를 저장 못하게 설정 */}
<div className='flex flex-wrap gap-2 mt-2'>
{field.value.map((tag) => (
<div key={tag} className='bg-blue-100 px-2 py-1 rounded-full flex items-center'>
<span>{tag}</span>
<button type='button' className='ml-2 text-red-500' onClick={() => handleRemoveTag(tag)}>
<div key={tag} className='bg-background-100 px-2 py-1 rounded-full flex items-center'>
<span className='text-sm md:text-lg lg:text-2xl'>{tag}</span>
<Button type='button' className='text-red-500 text-sm md:text-lg lg:text-2xl p-0 px-2' onClick={() => handleRemoveTag(tag)}>
×
</button>
</Button>
</div>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<Button className='h-11 lg:h-16 rounded-xl text-semibold lg:text-2xl bg-blue-300 text-white' type='submit' disabled={addEpigramMutation.isPending}>
{/* NOTE: 필수항목들에 값이 채워져있으면 폼제출 버튼 활성화 */}
<Button className='h-11 lg:h-16 rounded-xl text-semibold lg:text-2xl text-white bg-black-500 disabled:bg-blue-400 ' type='submit' disabled={addEpigramMutation.isPending || !isFormValid}>
{addEpigramMutation.isPending ? '제출 중...' : '작성 완료'}
</Button>
</form>
Expand Down
Loading

0 comments on commit 0b2a556

Please sign in to comment.