Skip to content

Commit

Permalink
FE-72 ✨ 에피그램 등록 api연동 (#52)
Browse files Browse the repository at this point in the history
* 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: 우지석 <[email protected]>
  • Loading branch information
jisurk and 우지석 authored Jul 22, 2024
1 parent e4df5eb commit 613693b
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 60 deletions.
9 changes: 9 additions & 0 deletions src/apis/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AddEpigramRequestType, AddEpigramResponseType } from '@/schema/addEpigram';
import httpClient from '.';

const postEpigram = async (request: AddEpigramRequestType): Promise<AddEpigramResponseType> => {
const response = await httpClient.post<AddEpigramResponseType>('/epigrams', request);
return response.data;
};

export default postEpigram;
23 changes: 23 additions & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import axios from 'axios';
import qs from 'qs';

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

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 };
const token = getToken();
if (token) {
newConfig.headers.Authorization = `Bearer ${token}`;
}

if (newConfig.data instanceof FormData) {
newConfig.headers['Content-Type'] = 'multipart/form-data';
} else {
newConfig.headers['Content-Type'] = 'application/json';
}

return newConfig;
},
(error) => Promise.reject(error),
);

export default httpClient;
24 changes: 24 additions & 0 deletions src/hooks/epigramQueryHook.ts
Original file line number Diff line number Diff line change
@@ -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<AddEpigramFormType, AddEpigramResponseType>) => {
const queryClient = useQueryClient();

return useMutation<AddEpigramResponseType, AxiosError, AddEpigramFormType>({
mutationFn: (newEpigram: AddEpigramFormType) => postEpigram(newEpigram),
...options,
onSuccess: (...args) => {
queryClient.invalidateQueries({ queryKey: ['epigrams'] });
if (options?.onSuccess) {
options.onSuccess(...args);
}
},
});
};

export default useAddEpigram;
309 changes: 249 additions & 60 deletions src/pageLayout/Epigram/AddEpigram.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,266 @@
import React, { KeyboardEvent, 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 Label from '@/components/ui/label';
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';

function AddEpigram() {
const [currentTag, setCurrentTag] = useState('');
const [isAlertOpen, setIsAlertOpen] = useState(false);
const [alertContent, setAlertContent] = useState({ title: '', description: '' });
const router = useRouter();

const form = useForm<AddEpigramFormType>({
resolver: zodResolver(AddEpigramFormSchema),
defaultValues: {
content: '',
author: '',
referenceTitle: '',
referenceUrl: '',
tags: [],
},
});

const handleAlertClose = () => {
setIsAlertOpen(false);
if (alertContent.title === '등록 완료') {
router.push(`/epigram/${addEpigramMutation.data?.id}`);
}
};

const addEpigramMutation = useAddEpigram({
onSuccess: () => {
setAlertContent({
title: '등록 완료',
description: '등록이 완료되었습니다.',
});
setIsAlertOpen(true);
form.reset();
},
// TODO : 유효성검사 브랜치만들어서 alert창에 유효성검사 틀린부분을 보이게 할 예정
onError: () => {
setAlertContent({
title: '등록 실패',
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 handleRemoveTag = (tagToRemove: string) => {
const currentTags = form.getValues('tags') || [];
form.setValue(
'tags',
currentTags.filter((tag) => tag !== tagToRemove),
);
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
};

const handleSubmit = (data: AddEpigramFormType) => {
addEpigramMutation.mutate(data);
};

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 className='flex flex-col justify-center item-center gap-8 w-[312px] md:w-[384px] lg:w-[640px] py-6'>
<div className='flex flex-col gap-2 lg:gap-4 h-44 lg:h-52'>
<Label htmlFor='content' className='text-semibold lg:text-2xl text-black-600'>
내용
<span className='text-state-error'>*</span>
</Label>
<Textarea className='h-[132px] lg:h-[148px] lg:text-xl border-blue-300 border-2 rounded-xl resize-none p-2' name='content' id='content' placeholder='500자 이내로 입력해주세요.' />
</div>
<div className='flex flex-col gap-2 lg:gap-4'>
<Label className='text-semibold lg:text-2xl text-black-600'>
저자
<span className='text-state-error'>*</span>
</Label>
<RadioGroup>
<div className='flex gap-2'>
<div className='flex items-center space-x-2 text-xl'>
<RadioGroupItem value='directly' id='directly' />
<Label htmlFor='directly' className='font-medium lg:text-xl'>
직접 입력
</Label>
</div>
<div className='flex items-center space-x-2'>
<RadioGroupItem value='unknown' id='unknown' />
<Label htmlFor='unknown' className='font-medium lg:text-xl'>
알 수 없음
</Label>
</div>
<div className='flex items-center space-x-2 text-xl'>
<RadioGroupItem value='me' id='me' />
<Label htmlFor='me' className='font-medium lg:text-xl'>
본인
</Label>
</div>
</div>
</RadioGroup>
<Input className='w-full h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2' id='authorName' name='authorName' type='text' placeholder='저자 이름 입력' />
</div>
<fieldset className='flex flex-col gap-2 lg:gap-4'>
<legend className='text-semibold lg:text-2xl text-black-600'>출처</legend>
<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'>
<FormField
control={form.control}
name='content'
render={({ field }) => (
<FormItem className='flex flex-col gap-2 lg:gap-4 h-44 lg:h-52'>
<FormLabel htmlFor='content' className='text-semibold lg:text-2xl text-black-600'>
내용
<span className='text-state-error'>*</span>
</FormLabel>
<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 />
</FormItem>
)}
/>

<div className='flex flex-col gap-2 lg:gap-4'>
<Input className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2' id='sourceTitle' name='sourceTitle' type='text' placeholder='출처 제목 입력' aria-label='출처 제목' />
<Input
className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2'
id='sourceUrl'
name='sourceUrl'
type='url'
placeholder='URL (ex.http://www.website.com)'
aria-label='출처 URL'
/>
<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', '');
}}
>
<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>
</div>
</RadioGroup>
</div>
</fieldset>
<div className='flex flex-col gap-2 lg:gap-4'>
<Label htmlFor='tags' className='text-semibold lg:text-2xl text-black-600'>
태그
</Label>
<Input className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2 ' id='tags' name='tags' type='text' placeholder='입력하여 태그 검색(최대10자)' />
</div>
<Button className='h-11 lg:h-16 rounded-xl text-semibold lg:text-2xl bg-blue-300 text-white' type='button'>
작성 완료
</Button>
</form>

<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} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<fieldset className='flex flex-col gap-2 lg:gap-4'>
<legend className='text-semibold lg:text-2xl text-black-600'>출처</legend>
<FormField
control={form.control}
name='referenceTitle'
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2'
id='referenceTitle'
type='text'
placeholder='출처 제목 입력'
aria-label='출처 제목'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='referenceUrl'
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2'
id='referenceUrl'
type='url'
placeholder='URL (ex.http://www.website.com)'
aria-label='출처 URL'
{...field}
/>
</FormControl>
<FormMessage />
</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'>
태그
</FormLabel>
<div className='relative'>
<Input
className='h-11 lg:h-16 lg:text-2xl border-blue-300 border-2 rounded-xl p-2 pr-20'
id='tags'
type='text'
placeholder='입력하여 태그 추가(최대10자)'
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
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'
onClick={handleAddTag}
disabled={field.value.length >= 3 || currentTag.length === 0}
>
저장
</Button>
</div>
{/* TODO: 태그 key값 수정 예정 */}
{/* 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)}>
×
</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}>
{addEpigramMutation.isPending ? '제출 중...' : '작성 완료'}
</Button>
</form>
</Form>
</div>

<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen}>
<AlertDialogContent className='bg-white'>
<AlertDialogHeader>
<AlertDialogTitle>{alertContent.title}</AlertDialogTitle>
<AlertDialogDescription>{alertContent.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={handleAlertClose}>확인</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
Expand Down
Loading

0 comments on commit 613693b

Please sign in to comment.