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-66 🔀 브랜치 최신화 #109

Merged
merged 40 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4a0d730
Merge pull request #11 from epigram5-9/merge/FE-29
jangmoonwon Jul 10, 2024
8ee83af
:heavy_plus_sign: 이미지 파일 추가
jangmoonwon Jul 10, 2024
f472f6f
:lipstick: 로그인 페이지 레이아웃 생성
jangmoonwon Jul 10, 2024
5b33ad7
:lipstick: 로그인 페이지 UI 생성 및 반응형 디자인 구현
jangmoonwon Jul 10, 2024
f69dd4f
Merge branch 'epic/FE-29' into feat/FE-30--signin-page-ui
jangmoonwon Jul 10, 2024
342bf76
Merge pull request #13 from epigram5-9/feat/FE-30--signin-page-ui
jangmoonwon Jul 11, 2024
c38c937
FE-60 :sparkles: react hook form, zod 추가
jangmoonwon Jul 12, 2024
03c6928
FE-60 :lipstick: 로그인 폼 스타일 수정
jangmoonwon Jul 12, 2024
8717c2f
FE-60 :recycle: 로그인 스키마 분리
jangmoonwon Jul 12, 2024
665deb9
Merge pull request #20 from epigram5-9/feat/FE-60--signin-vaildate
jangmoonwon Jul 13, 2024
3b7f965
:twisted_rightwards_arrows: Merge branch 'epic/FE-29' into merge/FE-29
jangmoonwon Jul 14, 2024
bad3c27
Merge pull request #23 from epigram5-9/merge/FE-29
jangmoonwon Jul 15, 2024
07493a9
:sparkles: 로그인 응답 데이터 스키마 정의
jangmoonwon Jul 17, 2024
10c6440
:sparkles: 로그인 api 생성
jangmoonwon Jul 17, 2024
5392384
:sparkles: 요청과 응답에 관한 인터셉터 추가
jangmoonwon Jul 17, 2024
22106ac
:sparkles: useSignin mutation hook 생성
jangmoonwon Jul 17, 2024
e4bebb9
:zap: useSignin hook 로그인 폼에 적용
jangmoonwon Jul 17, 2024
7da1e94
Merge pull request #36 from epigram5-9/feat/FE-63
jangmoonwon Jul 18, 2024
265c6e7
🔀 Merge branch 'epic/FE-29' into merge/FE-29
jangmoonwon Jul 20, 2024
9a253df
Merge pull request #50 from epigram5-9/merge/FE-29
jangmoonwon Jul 21, 2024
a81913f
:fire: AuthLayout 삭제
jangmoonwon Jul 22, 2024
d7cc6ad
:art: onSubmit 함수 인라인으로 정의
jangmoonwon Jul 22, 2024
da2ccc7
:recycle: 응답 인터셉터의 에러 처리 및 토큰 갱신 로직 개선
jangmoonwon Jul 22, 2024
9f18429
:recycle: postSignin api 에러처리 로직 삭제
jangmoonwon Jul 22, 2024
ba68251
:fire: useSignin hook 삭제
jangmoonwon Jul 22, 2024
9b205a5
:truck: useSigninMutation hook으로 이름 변경 및 파일 이동
jangmoonwon Jul 22, 2024
243e509
:sparkles: Toaster 컴포넌트 추가
jangmoonwon Jul 22, 2024
2c484ba
:sparkles: toast로 에러메시지 띄우기
jangmoonwon Jul 22, 2024
e4b2064
Merge pull request #54 from epigram5-9/fix/FE-29
jangmoonwon Jul 22, 2024
fbe86b8
:sparkles: oauth api 생성
jangmoonwon Jul 27, 2024
a8378ef
:sparkles: 카카오톡 리디렉트 uri 설정
jangmoonwon Jul 27, 2024
0039236
:sparkles: useKakaoLogin mutation hook 생성
jangmoonwon Jul 27, 2024
599efae
:zap: 네이버 구글 카카오 간편 로그인 링크 설정
jangmoonwon Jul 27, 2024
ae7c00e
:twisted_rightwards_arrows: Merge branch 'epic/FE-29' of into merge/F…
jangmoonwon Jul 28, 2024
33faa00
Merge pull request #94 from epigram5-9/merge/FE-29
jangmoonwon Jul 28, 2024
ffd5e62
Merge branch 'epic/FE-29' into feat/FE-70
jangmoonwon Jul 28, 2024
1cc4151
Merge pull request #83 from epigram5-9/feat/FE-70
jangmoonwon Jul 28, 2024
9daf9fe
Merge pull request #72 from epigram5-9/epic/FE-66
jangmoonwon Jul 30, 2024
197e37c
Merge pull request #96 from epigram5-9/epic/FE-29
jangmoonwon Jul 30, 2024
547e7d3
FE-51 :twisted_rightwards_arrows: 공용 API 머지 요청 (#92)
JeonYumin94 Jul 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
42 changes: 42 additions & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,45 @@ httpClient.interceptors.response.use(
);

export default httpClient;

// NOTE: eslint-disable no-param-reassign 미해결로 인한 설정
httpClient.interceptors.request.use((config) => {
const accessToken = localStorage.getItem('accessToken');
/* eslint-disable no-param-reassign */
if (accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
/* eslint-enable no-param-reassign */
return config;
});

httpClient.interceptors.response.use(
(response) => response,

(error) => {
if (error.response && error.response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken');

if (!refreshToken) {
window.location.href = '/auth/SignIn';
return Promise.reject(error);
}

return httpClient
.post('/auth/refresh-token', null, {
headers: { Authorization: `Bearer ${refreshToken}` },
})
.then((response) => {
const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);

const originalRequest = error.config;
return httpClient(originalRequest);
})
.catch(() => {
window.location.href = '/auth/SignIn';
return Promise.reject(error);
});
}
return Promise.reject(error);
},
);
11 changes: 11 additions & 0 deletions src/apis/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from 'axios';

const postOauth = async (code: string) => {
const response = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/auth/signIn/KAKAO`, {
redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI,
token: code,
});
return response.data;
};

export default postOauth;
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;
Loading
Loading