diff --git a/src/apis/.http b/src/apis/.http new file mode 100644 index 00000000..e69de29b diff --git a/src/apis/epigramComment.ts b/src/apis/epigramComment.ts new file mode 100644 index 00000000..4368fb9a --- /dev/null +++ b/src/apis/epigramComment.ts @@ -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 => { + 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(`/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; +}; diff --git a/src/apis/getEmotion.ts b/src/apis/getEmotion.ts new file mode 100644 index 00000000..044ff3c3 --- /dev/null +++ b/src/apis/getEmotion.ts @@ -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 => { + const user = await getMe(); + if (!user) { + throw new Error('로그인이 필요합니다.'); + } + + const response = await httpClient.get('/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; diff --git a/src/apis/getEpigrams.ts b/src/apis/getEpigrams.ts new file mode 100644 index 00000000..9685bc60 --- /dev/null +++ b/src/apis/getEpigrams.ts @@ -0,0 +1,12 @@ +import { GetEpigramsParamsType, GetEpigramsResponseType, GetEpigramsResponse } from '@/schema/epigrams'; +import httpClient from '.'; + +const getEpigrams = async (params: GetEpigramsParamsType): Promise => { + const response = await httpClient.get(`/epigrams`, { params }); + + // 데이터 일치하는지 확인 + const parsedResponse = GetEpigramsResponse.parse(response.data); + return parsedResponse; +}; + +export default getEpigrams; diff --git a/src/apis/index.ts b/src/apis/index.ts index e58d8047..6e81823b 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -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); + }, +); diff --git a/src/apis/oauth.ts b/src/apis/oauth.ts new file mode 100644 index 00000000..ae6bd068 --- /dev/null +++ b/src/apis/oauth.ts @@ -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; diff --git a/src/apis/postEmotion.ts b/src/apis/postEmotion.ts new file mode 100644 index 00000000..98130bc7 --- /dev/null +++ b/src/apis/postEmotion.ts @@ -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 => { + const user = await getMe(); + if (!user) { + throw new Error('로그인이 필요합니다.'); + } + + const englishEmotion = translateEmotionToEnglish(emotion); + const request: PostEmotionRequestType = { emotion: englishEmotion }; + + const response = await httpClient.post('/emotionLogs/today', { + ...request, + userId: user.id, + }); + + return response.data; +}; + +export default postEmotion; diff --git a/src/apis/user.ts b/src/apis/user.ts index 395b0167..cd192766 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -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 => { +export const getMe = async (): Promise => { const response = await httpClient.get('/users/me'); return response.data; }; -export const getUser = async (request: GetUserRequestType): Promise => { +export const getUser = async (request: GetUserRequestType): Promise => { const { id } = request; const response = await httpClient.get(`/users/${id}`); return response.data; }; -export const updateMe = async (request: PatchMeRequestType): Promise => { +export const updateMe = async (request: PatchMeRequestType): Promise => { const response = await httpClient.patch('/users/me', { ...request }); return response.data; }; diff --git a/src/components/Emotion/EmotionCard.tsx b/src/components/Emotion/EmotionCard.tsx index 1896f7cd..74134281 100644 --- a/src/components/Emotion/EmotionCard.tsx +++ b/src/components/Emotion/EmotionCard.tsx @@ -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 = { diff --git a/src/components/Emotion/EmotionSaveToast.tsx b/src/components/Emotion/EmotionSaveToast.tsx new file mode 100644 index 00000000..e7d24105 --- /dev/null +++ b/src/components/Emotion/EmotionSaveToast.tsx @@ -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: ( + router.push('/mypage')}> + 확인하기 + + ), + }); + }, [iconType, toast, router]); + + return null; +} + +export default EmotionSaveToast; diff --git a/src/components/Emotion/EmotionSelector.tsx b/src/components/Emotion/EmotionSelector.tsx index 5a73639e..fc92728b 100644 --- a/src/components/Emotion/EmotionSelector.tsx +++ b/src/components/Emotion/EmotionSelector.tsx @@ -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>({ 감동: 'Default', 기쁨: 'Default', @@ -22,13 +24,37 @@ function EmotionSelector() { 분노: 'Default', }); - // 감정 카드 클릭 핸들러 - const handleCardClick = (iconType: EmotionType) => { + // 현재 선택된 감정을 관리하는 useState 훅 + const [selectedEmotion, setSelectedEmotion] = useState(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'; }); @@ -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'; @@ -54,12 +92,19 @@ function EmotionSelector() { cardSize = 'sm'; } + if (isGetLoading) return

Loading...

; + if (getError) return

{getError.message}

; + return ( -
- {(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => ( - handleCardClick(iconType)} /> - ))} -
+ <> +
+ {(['감동', '기쁨', '고민', '슬픔', '분노'] as const).map((iconType) => ( + handleCardClick(iconType)} /> + ))} +
+ {/* 감정이 선택되었을 때 토스트 메시지 표시 */} + {selectedEmotion && } + ); } diff --git a/src/hooks/useDeleteCommentHook.ts b/src/hooks/useDeleteCommentHook.ts new file mode 100644 index 00000000..006019b5 --- /dev/null +++ b/src/hooks/useDeleteCommentHook.ts @@ -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; diff --git a/src/hooks/useGetEmotion.ts b/src/hooks/useGetEmotion.ts new file mode 100644 index 00000000..d8017abc --- /dev/null +++ b/src/hooks/useGetEmotion.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import getEmotion from '@/apis/getEmotion'; +import { EmotionType } from '@/types/emotion'; + +const useGetEmotion = () => + useQuery({ + queryKey: ['emotion'], + queryFn: getEmotion, + }); + +export default useGetEmotion; diff --git a/src/hooks/useKakaoLogin.ts b/src/hooks/useKakaoLogin.ts new file mode 100644 index 00000000..14d8d81b --- /dev/null +++ b/src/hooks/useKakaoLogin.ts @@ -0,0 +1,42 @@ +import postOauth from '@/apis/oauth'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { useRouter } from 'next/router'; + +const useKakaoLogin = () => { + const router = useRouter(); + + return useMutation({ + mutationFn: async (code: string) => { + const result = await postOauth(code); + localStorage.setItem('accessToken', result.accessToken); + localStorage.setItem('refreshToken', result.refreshToken); + return result; + }, + onSuccess: () => { + router.push('/'); + }, + onError: (error) => { + if (isAxiosError(error)) { + const status = error.response?.status; + + if (!status) return; + + if (status === 400) { + toast({ description: '잘못된 요청입니다. 요청을 확인해 주세요.', className: 'bg-state-error text-white font-semibold' }); + router.push('/auth/SignIn'); + return; + } + + if (status >= 500) { + toast({ description: '서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.', className: 'bg-state-error text-white font-semibold' }); + } + } + + toast({ description: '알 수 없는 에러가 발생했습니다.', className: 'bg-state-error text-white font-semibold' }); + }, + }); +}; + +export default useKakaoLogin; diff --git a/src/hooks/usePatchCommentHook.ts b/src/hooks/usePatchCommentHook.ts new file mode 100644 index 00000000..b215624d --- /dev/null +++ b/src/hooks/usePatchCommentHook.ts @@ -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; diff --git a/src/hooks/usePostEmotion.ts b/src/hooks/usePostEmotion.ts new file mode 100644 index 00000000..f13540ae --- /dev/null +++ b/src/hooks/usePostEmotion.ts @@ -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({ + mutationFn: postEmotion, + }); + +export default usePostEmotion; diff --git a/src/hooks/userQueryHooks.ts b/src/hooks/userQueryHooks.ts index 7c28fe75..29ba4e2e 100644 --- a/src/hooks/userQueryHooks.ts +++ b/src/hooks/userQueryHooks.ts @@ -1,6 +1,6 @@ import quries from '@/apis/queries'; import { updateMe } from '@/apis/user'; -import { GetUserReponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user'; +import { GetUserResponseType, GetUserRequestType, PatchMeRequestType } from '@/schema/user'; import { MutationOptions } from '@/types/query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -8,7 +8,7 @@ export const useMeQuery = () => useQuery(quries.user.getMe()); export const useUserQuery = (requset: GetUserRequestType) => useQuery(quries.user.getUser(requset)); -export const useUpdateMe = (options: MutationOptions) => { +export const useUpdateMe = (options: MutationOptions) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (request: PatchMeRequestType) => updateMe(request), diff --git a/src/pages/auth/SignIn.tsx b/src/pages/auth/SignIn.tsx index 400d0edd..68ef07ec 100644 --- a/src/pages/auth/SignIn.tsx +++ b/src/pages/auth/SignIn.tsx @@ -85,15 +85,15 @@ export default function SignIn() {
- - - +
); diff --git a/src/pages/auth/redirect/kakao/index.tsx b/src/pages/auth/redirect/kakao/index.tsx new file mode 100644 index 00000000..46e82db7 --- /dev/null +++ b/src/pages/auth/redirect/kakao/index.tsx @@ -0,0 +1,18 @@ +import useKakaoLogin from '@/hooks/useKakaoLogin'; +import { useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function Kakao() { + const searchParams = useSearchParams(); + const code = searchParams.get('code'); + const { mutate: login } = useKakaoLogin(); + + useEffect(() => { + if (code) { + login(code); + } else { + /* eslint-disable no-console */ + console.log(code); // code가 없을 때 콘솔에 출력 + } + }, [code, login]); +} diff --git a/src/schema/auth.ts b/src/schema/auth.ts index 0a9069a8..5de73332 100644 --- a/src/schema/auth.ts +++ b/src/schema/auth.ts @@ -20,6 +20,7 @@ export const PostSignUpRequest = z }); // NOTE: 로그인 스키마 + export const PostSigninRequest = z.object({ email: z.string().min(1, { message: '이메일은 필수 입력입니다.' }).email({ message: '올바른 이메일 주소가 아닙니다.' }), password: z.string().min(1, { message: '비밀번호는 필수 입력입니다.' }), diff --git a/src/schema/comment.ts b/src/schema/comment.ts new file mode 100644 index 00000000..391557df --- /dev/null +++ b/src/schema/comment.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +const WriterSchema = z.object({ + image: z.string().nullable(), + nickname: z.string(), + id: z.number(), +}); + +const CommentContentSchema = z.string().min(1); + +const CommentSchema = z.object({ + epigramId: z.number(), + writer: WriterSchema, + updatedAt: z.string().datetime(), + createdAt: z.string().datetime(), + isPrivate: z.boolean(), + content: CommentContentSchema, + id: z.number(), +}); + +const CommentResponseSchema = z.object({ + totalCount: z.number(), + nextCursor: z.number().nullable(), + list: z.array(CommentSchema), +}); + +const CommentRequestSchema = z.object({ + id: z.number().int().positive(), + limit: z.number().int().positive().max(100), + cursor: z.number().optional(), +}); + +const CommentFormSchema = z.object({ + content: z.string().min(1, '댓글을 입력해주세요.').max(100, '100자 이내로 입력해주세요.'), + isPrivate: z.boolean().default(true), +}); + +export type CommentFormValues = z.infer; +export type CommentRequestType = z.infer; +export type CommentResponseType = z.infer; +export type CommentType = z.infer; +export type Writer = z.infer; + +export { CommentRequestSchema, CommentResponseSchema, CommentFormSchema, CommentSchema, WriterSchema }; diff --git a/src/schema/emotion.ts b/src/schema/emotion.ts new file mode 100644 index 00000000..a2239be9 --- /dev/null +++ b/src/schema/emotion.ts @@ -0,0 +1,23 @@ +import * as z from 'zod'; + +export const PostEmotionRequest = z.object({ + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), +}); + +export const PostEmotionResponse = z.object({ + createdAt: z.coerce.date(), + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), + userId: z.number(), + id: z.number(), +}); + +export const GetEmotionResponse = z.object({ + createdAt: z.coerce.date(), + emotion: z.enum(['MOVED', 'JOY', 'WORRY', 'SADNESS', 'ANGER']), + userId: z.number(), + id: z.number(), +}); + +export type PostEmotionRequestType = z.infer; +export type PostEmotionResponseType = z.infer; +export type GetEmotionResponseType = z.infer; diff --git a/src/schema/epigram.ts b/src/schema/epigram.ts new file mode 100644 index 00000000..f72fdec7 --- /dev/null +++ b/src/schema/epigram.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; + +// Tag 스키마 +const TagSchema = z.object({ + name: z.string().min(1).max(10), + id: z.number().int().positive(), +}); + +// GetEpigramResponseType 스키마 +const GetEpigramResponseSchema = z.object({ + id: z.number().int().positive(), + content: z.string().min(1).max(500), + author: z.string().min(1).max(30), + referenceTitle: z.string().max(100).nullable().optional(), + referenceUrl: z.string().url().nullable().optional(), + writerId: z.number().int().positive(), + tags: z.array(TagSchema), + likeCount: z.number(), + isLiked: z.boolean().optional(), +}); + +const EpigramRequestSchema = z.object({ + id: z.union([z.string(), z.number(), z.undefined()]), +}); + +export type Tag = z.infer; +export type GetEpigramResponseType = z.infer; +export type EpigramRequestType = z.infer; + +export { TagSchema, GetEpigramResponseSchema, EpigramRequestSchema }; diff --git a/src/schema/epigrams.ts b/src/schema/epigrams.ts new file mode 100644 index 00000000..46a7cb85 --- /dev/null +++ b/src/schema/epigrams.ts @@ -0,0 +1,33 @@ +import * as z from 'zod'; + +export const GetEpigramsParams = z.object({ + limit: z.number(), + cursor: z.number().optional(), + keyword: z.string().optional(), + writerId: z.number().optional(), +}); + +export const GetEpigramsResponse = z.object({ + totalCount: z.number(), + nextCursor: z.number(), + list: z.array( + z.object({ + likeCount: z.number(), + tags: z.array( + z.object({ + name: z.string(), + id: z.number(), + }), + ), + writerId: z.number(), + referenceUrl: z.string(), + referenceTitle: z.string(), + author: z.string(), + content: z.string(), + id: z.number(), + }), + ), +}); + +export type GetEpigramsParamsType = z.infer; +export type GetEpigramsResponseType = z.infer; diff --git a/src/schema/user.ts b/src/schema/user.ts index 83d1f8d8..4e9fde85 100644 --- a/src/schema/user.ts +++ b/src/schema/user.ts @@ -18,6 +18,6 @@ export const GetUserReponse = z.object({ id: z.number(), }); -export type GetUserReponseType = z.infer; +export type GetUserResponseType = z.infer; export type GetUserRequestType = z.infer; export type PatchMeRequestType = z.infer; diff --git a/src/types/EmotionTypes.ts b/src/types/emotion.ts similarity index 100% rename from src/types/EmotionTypes.ts rename to src/types/emotion.ts diff --git a/src/types/epigram.types.ts b/src/types/epigram.types.ts new file mode 100644 index 00000000..226ee9ee --- /dev/null +++ b/src/types/epigram.types.ts @@ -0,0 +1,24 @@ +import { GetEpigramResponseType } from '@/schema/epigram'; +import { GetUserResponseType } from '@/schema/user'; + +export interface EpigramFigureProps { + epigram: GetEpigramResponseType; + currentUserId: GetUserResponseType['id'] | undefined; +} + +export interface EpigramCommentProps { + epigramId: number; + currentUserId: GetUserResponseType['id'] | undefined; + userImage?: string | undefined; +} + +export interface PostCommentRequest { + epigramId: number; + isPrivate: boolean; + content: string; +} + +export interface PatchCommentRequest { + isPrivate: boolean; + content: string; +} diff --git a/src/utils/emotionMap.ts b/src/utils/emotionMap.ts new file mode 100644 index 00000000..84b5663e --- /dev/null +++ b/src/utils/emotionMap.ts @@ -0,0 +1,23 @@ +import { EmotionType } from '@/types/emotion'; + +const emotionMap: Record = { + 감동: 'MOVED', + 기쁨: 'JOY', + 고민: 'WORRY', + 슬픔: 'SADNESS', + 분노: 'ANGER', +}; + +const reverseEmotionMap: Record<'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER', EmotionType> = { + MOVED: '감동', + JOY: '기쁨', + WORRY: '고민', + SADNESS: '슬픔', + ANGER: '분노', +}; + +const translateEmotionToEnglish = (emotion: EmotionType): 'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER' => emotionMap[emotion]; + +const translateEmotionToKorean = (emotion: 'MOVED' | 'JOY' | 'WORRY' | 'SADNESS' | 'ANGER'): EmotionType => reverseEmotionMap[emotion]; + +export { translateEmotionToEnglish, translateEmotionToKorean };