Skip to content

Commit

Permalink
FE-51 🔀 공용 API 머지 요청 (#92)
Browse files Browse the repository at this point in the history
* FE-52  ✨에피그램 목록조회 API (#34)

* FE-52 feat: api schema 작성

* FE-52 ✨feat: getEpigrams api 작성

* FE-522 ✨fix:  default export로 변경

* FE-52 ✨test: 테스트 코드 작성

* FE-52 ✨feat: BaseUrl, TeamID 상수 추가 및 axios baseTRL 수정

* FE-52 ✨fix: schema 및 apis 파일 수정

* FE-52 ✨test:  테스트 코드 삭제

* FE-52 ✨fix: .env파일 생성 및 BaseURL 수정

* FE-52 ✨fix: limit 타입  수정(optional 삭제)

* FE-52 ✨text: 테 테스트코드 삭제

* FE-52 ✨fix: api GET요청 주소 수정('epigrams' -> '/epigrams')

* FE-53 ✨ 감정이모티콘 저장 스키마 정의

* FE-53 ✨ 오늘의 감정 저장 api 생성

* FE-53 ✨ getMe 함수를 사용해 로그인 상태 확인 기능 구현

* FE-53 ✨ 감정 한영 변환 함수

* FE-53 ✨ 감정 저장 후 토스트 알림 표시

* FE-53 ✨ 오늘의 감정 조회 api 생성

* FE-53 🔨 감정 한영 변환 함수 추가

+) post, get 함수 내부로 한영 변환 함수 이동

* FE-53 ✨ 오늘의 감정 스키마 추가 정의

* FE-53 ✨ 오늘의 감정 조회 함수 적용

* FE-53 🚚 오늘의 감정 type 이름 변경

* FE-53 ✨ useMutation 훅 사용

* FE-53 📝 EmotionSelector 주석 추가

* FE-53 🔥 api 함수 에러 처리 부분 제거

* FE-53 🔨 useQuery를 사용해 오늘의 감정 조회

데이터를 조회할 때는 useQuery를 사용하는거라 함

* FE-56 ✨ 댓글 수정 API (#84)

* FE-29 🔀 로그인 페이지 머지 요청 (#39)

* ➕ 이미지 파일 추가

* 💄 로그인 페이지 레이아웃 생성

* 💄 로그인 페이지 UI 생성 및 반응형 디자인 구현

* FE-60 ✨ react hook form, zod 추가

* FE-60 💄 로그인 폼 스타일 수정

- 텍스트 인풋 테두리
- 로그인 버튼

* FE-60 ♻️ 로그인 스키마 분리

* ✨ 로그인 응답 데이터 스키마 정의

* ✨ 로그인 api 생성

* ✨ 요청과 응답에 관한 인터셉터 추가

* ✨ useSignin mutation hook 생성

* ⚡ useSignin hook 로그인 폼에 적용

* 🔥 AuthLayout 삭제

* 🎨 onSubmit 함수 인라인으로 정의

* ♻️ 응답 인터셉터의 에러 처리 및 토큰 갱신 로직 개선

* ♻️ postSignin api 에러처리 로직 삭제

* 🔥 useSignin hook 삭제

* 🚚 useSigninMutation hook으로 이름 변경 및 파일 이동

* ✨ Toaster 컴포넌트 추가

* ✨ toast로 에러메시지 띄우기

* FE-71 🔀 에피그램 작성 페이지 (#71)

* 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: 우지석 <[email protected]>

* 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: 우지석 <[email protected]>

* FE-71♻️ epic브랜치 코드리뷰 반영 (#76)

* FE-71♻️  token,interceptor 로직 수정

* FE-71♻️  AddEpigram 코드리뷰 반영

* FE-71🔥 테스트용 상세페이지 삭제

* FE-71♻️  onKeyDown -> onKeyUp 수정

---------

Co-authored-by: 우지석 <[email protected]>

* FE-56 ✨ 댓글 수정 API

---------

Co-authored-by: MOON <[email protected]>
Co-authored-by: Jiseok Woo <[email protected]>
Co-authored-by: 우지석 <[email protected]>

* FE-57 ✨ 댓글 삭제 API (#88)

* FE-51 🔀 공용 API 최신화 (#93)

* FE-29 🔀 로그인 페이지 머지 요청 (#39)

* ➕ 이미지 파일 추가

* 💄 로그인 페이지 레이아웃 생성

* 💄 로그인 페이지 UI 생성 및 반응형 디자인 구현

* FE-60 ✨ react hook form, zod 추가

* FE-60 💄 로그인 폼 스타일 수정

- 텍스트 인풋 테두리
- 로그인 버튼

* FE-60 ♻️ 로그인 스키마 분리

* ✨ 로그인 응답 데이터 스키마 정의

* ✨ 로그인 api 생성

* ✨ 요청과 응답에 관한 인터셉터 추가

* ✨ useSignin mutation hook 생성

* ⚡ useSignin hook 로그인 폼에 적용

* 🔥 AuthLayout 삭제

* 🎨 onSubmit 함수 인라인으로 정의

* ♻️ 응답 인터셉터의 에러 처리 및 토큰 갱신 로직 개선

* ♻️ postSignin api 에러처리 로직 삭제

* 🔥 useSignin hook 삭제

* 🚚 useSigninMutation hook으로 이름 변경 및 파일 이동

* ✨ Toaster 컴포넌트 추가

* ✨ toast로 에러메시지 띄우기

* FE-71 🔀 에피그램 작성 페이지 (#71)

* 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: 우지석 <[email protected]>

* 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: 우지석 <[email protected]>

* FE-71♻️ epic브랜치 코드리뷰 반영 (#76)

* FE-71♻️  token,interceptor 로직 수정

* FE-71♻️  AddEpigram 코드리뷰 반영

* FE-71🔥 테스트용 상세페이지 삭제

* FE-71♻️  onKeyDown -> onKeyUp 수정

---------

Co-authored-by: 우지석 <[email protected]>

---------

Co-authored-by: MOON <[email protected]>
Co-authored-by: Jiseok Woo <[email protected]>
Co-authored-by: 우지석 <[email protected]>

* FE-51 🔀 공용 API 최신화 (충돌수정) (#98)

---------

Co-authored-by: imsoohyeok <[email protected]>
Co-authored-by: NEWJIN <[email protected]>
Co-authored-by: NEWJIN <[email protected]>
Co-authored-by: MOON <[email protected]>
Co-authored-by: Jiseok Woo <[email protected]>
Co-authored-by: 우지석 <[email protected]>
  • Loading branch information
7 people authored Jul 30, 2024
1 parent 197e37c commit 547e7d3
Show file tree
Hide file tree
Showing 23 changed files with 475 additions and 26 deletions.
Empty file added src/apis/.http
Empty file.
46 changes: 46 additions & 0 deletions src/apis/epigramComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import httpClient from '@/apis/index';
import { CommentRequestSchema, CommentRequestType, CommentResponseSchema, CommentResponseType } from '@/schema/comment';
import { PostCommentRequest, PatchCommentRequest } from '@/types/epigram.types';

export const getEpigramComments = async (params: CommentRequestType): Promise<CommentResponseType> => {
try {
// 요청 파라미터 유효성 검사
const validatedParams = CommentRequestSchema.parse(params);

const { id, limit, cursor } = validatedParams;

// NOTE: URL의 쿼리 문자열을 사용
// NOTE : cursor값이 있다면 ?limit=3&cursor=100, 없다면 ?limit=3,(숫자는 임의로 지정한 것)
const queryParams = new URLSearchParams({
limit: limit.toString(),
...(cursor !== undefined && { cursor: cursor.toString() }),
});

const response = await httpClient.get<CommentResponseType>(`/epigrams/${id}/comments?${queryParams.toString()}`);

// 응답 데이터 유효성 검사
const validatedData = CommentResponseSchema.parse(response.data);

return validatedData;
} catch (error) {
if (error instanceof Error) {
throw new Error(`댓글을 불러오는데 실패했습니다: ${error.message}`);
}
throw error;
}
};

export const postComment = async (commentData: PostCommentRequest) => {
const response = await httpClient.post('/comments', commentData);
return response.data;
};

export const patchComment = async (commentId: number, commentData: PatchCommentRequest) => {
const response = await httpClient.patch(`/comments/${commentId}`, commentData);
return response.data;
};

export const deleteComment = async (commentId: number) => {
const response = await httpClient.delete(`/comments/${commentId}`);
return response.data;
};
25 changes: 25 additions & 0 deletions src/apis/getEmotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EmotionType } from '@/types/emotion';
import type { GetEmotionResponseType } from '@/schema/emotion';
import { translateEmotionToKorean } from '@/utils/emotionMap';
import httpClient from '.';
import { getMe } from './user';

const getEmotion = async (): Promise<EmotionType | null> => {
const user = await getMe();
if (!user) {
throw new Error('로그인이 필요합니다.');
}

const response = await httpClient.get<GetEmotionResponseType>('/emotionLogs/today', {
params: { userId: user.id },
});

if (response.status === 204) {
return null; // No content
}

const koreanEmotion = translateEmotionToKorean(response.data.emotion);
return koreanEmotion;
};

export default getEmotion;
12 changes: 12 additions & 0 deletions src/apis/getEpigrams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GetEpigramsParamsType, GetEpigramsResponseType, GetEpigramsResponse } from '@/schema/epigrams';
import httpClient from '.';

const getEpigrams = async (params: GetEpigramsParamsType): Promise<GetEpigramsResponseType> => {
const response = await httpClient.get(`/epigrams`, { params });

// 데이터 일치하는지 확인
const parsedResponse = GetEpigramsResponse.parse(response.data);
return parsedResponse;
};

export default getEpigrams;
24 changes: 24 additions & 0 deletions src/apis/postEmotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { EmotionType } from '@/types/emotion';
import type { PostEmotionRequestType, PostEmotionResponseType } from '@/schema/emotion';
import { translateEmotionToEnglish } from '@/utils/emotionMap';
import httpClient from '.';
import { getMe } from './user';

const postEmotion = async (emotion: EmotionType): Promise<PostEmotionResponseType> => {
const user = await getMe();
if (!user) {
throw new Error('로그인이 필요합니다.');
}

const englishEmotion = translateEmotionToEnglish(emotion);
const request: PostEmotionRequestType = { emotion: englishEmotion };

const response = await httpClient.post<PostEmotionResponseType>('/emotionLogs/today', {
...request,
userId: user.id,
});

return response.data;
};

export default postEmotion;
8 changes: 4 additions & 4 deletions src/apis/user.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { GetUserReponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user';
import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user';
import httpClient from '.';

export const getMe = async (): Promise<GetUserReponseType> => {
export const getMe = async (): Promise<GetUserResponseType> => {
const response = await httpClient.get('/users/me');
return response.data;
};

export const getUser = async (request: GetUserRequestType): Promise<GetUserReponseType> => {
export const getUser = async (request: GetUserRequestType): Promise<GetUserResponseType> => {
const { id } = request;
const response = await httpClient.get(`/users/${id}`);
return response.data;
};

export const updateMe = async (request: PatchMeRequestType): Promise<GetUserReponseType> => {
export const updateMe = async (request: PatchMeRequestType): Promise<GetUserResponseType> => {
const response = await httpClient.patch('/users/me', { ...request });
return response.data;
};
2 changes: 1 addition & 1 deletion src/components/Emotion/EmotionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import React from 'react';
import cn from '@/lib/utils';
import Image from 'next/image';
import { EmotionIconCardProps } from '@/types/EmotionTypes';
import { EmotionIconCardProps } from '@/types/emotion';

// 아이콘 파일 경로 매핑
const iconPaths = {
Expand Down
34 changes: 34 additions & 0 deletions src/components/Emotion/EmotionSaveToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 오늘의 감정을 선택하면 표시되는 toast입니다.
* 감정을 확인하기 위해 마이페이지로 연결됩니다.
*/

import React, { useEffect } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { useRouter } from 'next/router';

interface EmotionSaveToastProps {
iconType: string;
}

function EmotionSaveToast({ iconType }: EmotionSaveToastProps) {
const { toast } = useToast();
const router = useRouter();

useEffect(() => {
toast({
title: '오늘의 감정이 저장되었습니다.',
description: `오늘의 감정: ${iconType}`,
action: (
<ToastAction altText='확인하기' onClick={() => router.push('/mypage')}>
확인하기
</ToastAction>
),
});
}, [iconType, toast, router]);

return null;
}

export default EmotionSaveToast;
81 changes: 63 additions & 18 deletions src/components/Emotion/EmotionSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
/*
여러 개의 EmotionIconCard를 관리합니다.
사용자 인터페이스에 필요한 상호 작용 로직을 포함합니다.
*/

import React, { useState } from 'react';
import EmotionIconCard from '@/components/Emotion/EmotionCard';
import React, { useState, useEffect } from 'react';
import useMediaQuery from '@/hooks/useMediaQuery';
import { EmotionType, EmotionState } from '@/types/EmotionTypes';
import EmotionIconCard from '@/components/Emotion/EmotionCard';
import { EmotionType, EmotionState } from '@/types/emotion';
import usePostEmotion from '@/hooks/usePostEmotion';
import useGetEmotion from '@/hooks/useGetEmotion';
import EmotionSaveToast from './EmotionSaveToast';

// EmotionSelector 컴포넌트 함수 선언
/**
* EmotionSelector 컴포넌트는 여러 개의 EmotionIconCard를 관리하고
* 사용자의 오늘의 감정을 선택하고 저장하고 출력합니다.
*/
function EmotionSelector() {
// 반응형 디자인을 위한 미디어 쿼리 훅
const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 767px)');

// 감정 카드 상태 관리
// 감정 카드 상태 관리를 위한 useState 훅
const [states, setStates] = useState<Record<EmotionType, EmotionState>>({
감동: 'Default',
기쁨: 'Default',
Expand All @@ -22,13 +24,37 @@ function EmotionSelector() {
분노: 'Default',
});

// 감정 카드 클릭 핸들러
const handleCardClick = (iconType: EmotionType) => {
// 현재 선택된 감정을 관리하는 useState 훅
const [selectedEmotion, setSelectedEmotion] = useState<EmotionType | null>(null);
// 오늘의 감정을 조회하기 위한 훅
const { data: emotion, error: getError, isLoading: isGetLoading } = useGetEmotion();
// 감정을 저장하기 위한 훅
const postEmotionMutation = usePostEmotion();

// 컴포넌트가 마운트될 때 한 번만 실행되는 useEffect 훅
// 오늘의 감정을 조회하고 상태를 업데이트합니다.
useEffect(() => {
if (emotion) {
setStates((prevStates) => ({
...prevStates,
[emotion]: 'Clicked',
}));
}
}, [emotion]);

/**
* 감정 카드 클릭 핸들러
* 사용자가 감정 카드를 클릭했을 때 호출됩니다.
* 클릭된 감정 카드를 'Clicked' 상태로 설정하고 나머지 카드는 'Unclicked' 상태로 설정합니다.
* 감정을 서버에 저장합니다.
* @param iconType - 클릭된 감정의 타입
*/
const handleCardClick = async (iconType: EmotionType) => {
setStates((prevStates) => {
const newStates = { ...prevStates };

if (prevStates[iconType] === 'Clicked') {
// 현재 클릭된 카드가 다시 클릭되면 모두 Default로 설정
// 현재 클릭된 카드가 다시 클릭되면 모든 카드를 Default로 설정
Object.keys(newStates).forEach((key) => {
newStates[key as EmotionType] = 'Default';
});
Expand All @@ -41,8 +67,20 @@ function EmotionSelector() {

return newStates;
});

// 오늘의 감정 저장
postEmotionMutation.mutate(iconType, {
onSuccess: (_, clickedIconType) => {
setSelectedEmotion(clickedIconType);
},
onError: (error: unknown) => {
// eslint-disable-next-line
console.error(error);
},
});
};

// 반응형 디자인을 위한 카드 크기 설정
let containerClass = 'w-[544px] h-[136px] gap-4';
let cardSize: 'lg' | 'md' | 'sm' = 'lg';

Expand All @@ -54,12 +92,19 @@ function EmotionSelector() {
cardSize = 'sm';
}

if (isGetLoading) return <p>Loading...</p>;
if (getError) return <p>{getError.message}</p>;

return (
<div className={`justify-start items-start inline-flex ${containerClass}`}>
{(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => (
<EmotionIconCard key={iconType} iconType={iconType} size={cardSize} state={states[iconType]} onClick={() => handleCardClick(iconType)} />
))}
</div>
<>
<div className={`justify-start items-start inline-flex ${containerClass}`}>
{(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => (
<EmotionIconCard key={iconType} iconType={iconType} size={cardSize} state={states[iconType]} onClick={() => handleCardClick(iconType)} />
))}
</div>
{/* 감정이 선택되었을 때 토스트 메시지 표시 */}
{selectedEmotion && <EmotionSaveToast iconType={selectedEmotion} />}
</>
);
}

Expand Down
31 changes: 31 additions & 0 deletions src/hooks/useDeleteCommentHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { deleteComment } from '@/apis/epigramComment';
import { toast } from '@/components/ui/use-toast';

const useDeleteCommentMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (commentId: number) => deleteComment(commentId),
onSuccess: () => {
// 댓글 목록 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['epigramComments'] });

// 성공 메시지 표시
toast({
title: '댓글 삭제 성공',
description: '댓글이 성공적으로 삭제되었습니다.',
});
},
onError: (error) => {
// 에러 메시지 표시
toast({
title: '댓글 삭제 실패',
description: `댓글 삭제 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`,
variant: 'destructive',
});
},
});
};

export default useDeleteCommentMutation;
11 changes: 11 additions & 0 deletions src/hooks/useGetEmotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import getEmotion from '@/apis/getEmotion';
import { EmotionType } from '@/types/emotion';

const useGetEmotion = () =>
useQuery<EmotionType | null, Error>({
queryKey: ['emotion'],
queryFn: getEmotion,
});

export default useGetEmotion;
32 changes: 32 additions & 0 deletions src/hooks/usePatchCommentHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { patchComment } from '@/apis/epigramComment';
import { PatchCommentRequest } from '@/types/epigram.types';
import { toast } from '@/components/ui/use-toast';

const usePatchCommentMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ commentId, ...commentData }: { commentId: number } & PatchCommentRequest) => patchComment(commentId, commentData),
onSuccess: () => {
// 댓글 목록 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['epigramComments'] });

// 성공 메시지 표시
toast({
title: '댓글 수정 성공',
description: '댓글이 성공적으로 수정되었습니다.',
});
},
onError: (error) => {
// 에러 메시지 표시
toast({
title: '댓글 수정 실패',
description: `댓글 수정 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`,
variant: 'destructive',
});
},
});
};

export default usePatchCommentMutation;
11 changes: 11 additions & 0 deletions src/hooks/usePostEmotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import postEmotion from '@/apis/postEmotion';
import { EmotionType } from '@/types/emotion';
import { PostEmotionResponseType } from '@/schema/emotion';

const usePostEmotion = () =>
useMutation<PostEmotionResponseType, Error, EmotionType>({
mutationFn: postEmotion,
});

export default usePostEmotion;
Loading

0 comments on commit 547e7d3

Please sign in to comment.