Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FE-73✨ 유효성검사 추가 #66

Merged
merged 10 commits into from
Jul 24, 2024
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
Loading