Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/epigram5-9/epigram into mer…
Browse files Browse the repository at this point in the history
…ge/FE-29
  • Loading branch information
jangmoonwon committed Aug 4, 2024
2 parents 86efe6f + e2a072e commit 41042ef
Show file tree
Hide file tree
Showing 32 changed files with 386 additions and 299 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ module.exports = {
'react/jsx-props-no-spreading': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['off'],
"react/require-default-props": 'off',
"react/self-closing-comp": 'off',
'react/require-default-props': 'off',
'react/self-closing-comp': 'off',
},
settings: {
react: {
Expand Down
Empty file removed src/apis/.http
Empty file.
10 changes: 3 additions & 7 deletions src/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,9 @@ const queries = createQueryKeyStore({
}),
},
epigramComment: {
getComments: (request: CommentRequestType) => ({
queryKey: ['epigramComments', request],
queryFn: () => getEpigramComments(request),
}),
getCommentList: (request: CommentRequestType) => ({
queryKey: ['epigramComments', request] as const,
queryFn: ({ pageParam }: { pageParam: number | undefined }) => getEpigramComments({ ...request, cursor: pageParam }),
getComments: (epigramId: number) => ({
queryKey: ['epigramComments', epigramId],
queryFn: ({ pageParam }: { pageParam?: number }) => getEpigramComments({ id: epigramId, limit: 3, cursor: pageParam }),
}),
getMyComments: (request: CommentRequestType) => ({
queryKey: ['myEpigramComments', request],
Expand Down
12 changes: 10 additions & 2 deletions src/components/main/TodayEmotion.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React, { useState } from 'react';
import { useEmotionContext } from '@/context/EmotionContext';
import EmotionSelector from '../Emotion/EmotionSelector';

function TodayEmotion() {
interface TodayEmotionProps {
isMyPage?: boolean;
}

function TodayEmotion({ isMyPage = false }: TodayEmotionProps) {
const [isVisible, setIsVisible] = useState<boolean>(true);
// NOTE: 오늘의 감정 선택 시 감정 달력 및 감정 차트 동기화를 위한 context 추가
const { setShouldRefetch } = useEmotionContext();

const handleEmotionSaved = () => {
setIsVisible(false);
if (isMyPage) setShouldRefetch(true);
else setIsVisible(false);
};

if (!isVisible) return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { subMonths } from 'date-fns';
import { subMonths, isSameMonth, isSameDay } from 'date-fns';
import { EmotionLog, EmotionTypeEN } from '@/types/emotion';
import useCalendar from '../../hooks/useCalendar';
import { DAY_LIST, DATE_MONTH_FIXER, iconPaths } from '../utill/constants';
import { DAY_LIST, DATE_MONTH_FIXER, iconPaths } from '../../user/utill/constants';
import CalendarHeader from './CalendarHeader';

interface CalendarProps {
Expand Down Expand Up @@ -64,12 +64,22 @@ export default function Calendar({ currentDate, setCurrentDate, monthlyEmotionLo
// eslint-disable-next-line react/no-array-index-key
<div key={weekIndex} className='flex'>
{week.map((day, dayIndex) => {
// 현재 날짜와 비교
const isToday = day === currentDate.getDate() && currentDate.getMonth() === new Date().getMonth() && currentDate.getFullYear() === new Date().getFullYear();
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
const dateString = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const emotion: EmotionTypeEN = filteredEmotionMap[dateString]; // 날짜에 해당하는 감정 가져오기

// 현재 월의 날짜인지 확인
const isDateInCurrentMonth = isSameMonth(date, currentDate);

// 첫 주에 7보다 큰 날짜 또는 마지막 주에 7보다 작은 날짜는 감정 출력하지 않음
const isFirstWeek = weekIndex === 0 && day > 7;
const isLastWeek = weekIndex === weekCalendarList.length - 1 && day < 7;

const emotion = isDateInCurrentMonth && !isFirstWeek && !isLastWeek ? filteredEmotionMap[dateString] : undefined;
const iconPath = emotion && iconPaths[emotion] ? iconPaths[emotion].path : '/icon/BW/SmileFaceBWIcon.svg';

// 오늘 날짜 체크
const isToday = isSameDay(date, new Date());

return (
<div
// TODO: index 값 Lint error. 임시로 주석 사용. 추후 수정 예정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { EmotionTypeEN } from '@/types/emotion';
import ARROW_BOTTOM_ICON from '../../../public/icon/arrow-bottom-icon.svg';
import ARROW_RIGHT_ICON from '../../../public/icon/arrow-right-icon.svg';
import ARROW_LEFT_ICON from '../../../public/icon/arrow-left-icon.svg';
import { iconPaths } from '../utill/constants';
import { iconPaths } from '../../user/utill/constants';

interface CalendarHeaderProps {
currentDate: Date;
Expand All @@ -16,14 +16,17 @@ interface CalendarHeaderProps {
}

export default function CalendarHeader({ currentDate, onPrevMonth, onNextMonth, onEmotionSelect, selectEmotion }: CalendarHeaderProps) {
// 드롭다운 버튼 텍스트 설정
const filterText = selectEmotion ? `필터: ${iconPaths[selectEmotion]?.name}` : '필터: 없음';

return (
<div className='w-full flex justify-between items-center'>
<div className='flex w-full h-[52px] justify-between items-center'>
<div className='text-neutral-700 text-2xl font-semibold leading-loose'>{`${currentDate.getFullYear()}${currentDate.getMonth() + 1}월`}</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' className='flex items-center gap-1 bg-slate-100 rounded-[14px] text-center text-stone-300 text-xl'>
필터: 감동
{filterText}
<div className='w-9 h-9 relative'>
<Image src={ARROW_BOTTOM_ICON} alt='필터 선택' width={36} height={36} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EmotionLog, EmotionTypeEN } from '@/types/emotion';
import Image from 'next/image';
import { iconPaths } from '../utill/constants';
import { iconPaths } from '../../user/utill/constants';

interface ChartProps {
monthlyEmotionLogs: EmotionLog[];
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UserProfileProps } from '@/types/user';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog';
import { sampleImage } from '../utill/constants';
import { sampleImage } from '../../user/utill/constants';
import ProfileEdit from './ProfileEdit';

export default function Profile({ image, nickname }: UserProfileProps) {
Expand Down
118 changes: 118 additions & 0 deletions src/components/mypage/ProfileEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Image from 'next/image';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useRef, useState } from 'react';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { useCreatePresignedUrl, useUpdateMe } from '@/hooks/userQueryHooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { PatchMeRequestType, PatchMeRequest } from '@/schema/user';
import fileNameChange from '../../user/utill/fileNameChange';

interface UserProfileEditProps {
initialValues: {
image: string;
nickname: string;
};
onModalClose: () => void;
}

export default function ProfileEdit({ initialValues, onModalClose }: UserProfileEditProps) {
// 이미지 업로드 훅
const createPresignedUrl = useCreatePresignedUrl();
// 닉네임 중복 처리를 위한 변수
const [focusedField, setFocusedField] = useState<boolean>(false);

const fileInputRef = useRef<HTMLInputElement | null>(null);

const form = useForm<PatchMeRequestType>({
resolver: zodResolver(PatchMeRequest),
mode: 'onBlur',
defaultValues: initialValues,
});

const { setValue, getValues, setFocus } = form;

// error 반환 시 닉네임 focus를 위한 함수
const handleFieldError = () => {
setFocus('nickname');
setFocusedField(true);
};

// 회원정보 수정 훅
const updateMe = useUpdateMe(handleFieldError, onModalClose);

// 프로필 사진 변경 클릭
const handleImageEditClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};

// 이미지 변경 시
async function handleImageChange(e: React.ChangeEvent<HTMLInputElement>): Promise<void> {
const { files } = e.currentTarget;

if (files && files.length > 0) {
const file = files[0];

// 중복된 파일명 및 한글파일이 저장되지 않도록 파일이름 포멧 변경
const newFileName = fileNameChange();
const newFile = new File([file], `${newFileName}.${file.name.split('.').pop()}`, { type: file.type });

createPresignedUrl.mutate(
{ image: newFile },
{
onSuccess: (data) => {
setValue('image', data.url);
},
},
);
}
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit((values: PatchMeRequestType) => updateMe.mutate(values))}>
<DialogHeader>
<DialogTitle>프로필 수정</DialogTitle>
<div className='flex flex-col justify-center items-center pt-8'>
<div className='w-[200px] h-[200px] rounded-full overflow-hidden cursor-pointer border border-gray-300 shadow-sm'>
<Image src={getValues('image') || initialValues.image} alt='유저 프로필' className='w-full h-full object-cover' width={200} height={200} priority onClick={handleImageEditClick} />
<Input type='file' accept='image/*' name='image' onChange={(e) => handleImageChange(e)} className='hidden' ref={fileInputRef} />
</div>
<div className='mt-10 flex flex-col items-start gap-4'>
<FormField
control={form.control}
name='nickname'
render={({ field, fieldState }) => (
<FormItem className='flex flex-col w-full lg:max-w-[640px] md:max-w-[384px] space-y-0 md:mb-10 mb-5'>
<FormLabel className={`md:mb-5 mb-4 font-pretendard lg:text-xl md:text-base sm:text-sm ${fieldState.invalid || focusedField ? 'text-state-error' : 'text-blue-900'}`}>
닉네임
</FormLabel>
<FormControl>
<Input
{...field}
type='text'
placeholder='닉네임'
onBlur={(e) => setValue('nickname', e.target.value.trim())}
className={`lg:h-16 h-11 px-4 lg:text-xl md:text-base placeholder-blue-400 rounded-xl bg-blue-200 font-pretendard ${fieldState.invalid || focusedField ? 'border-2 border-state-error' : 'focus:border-blue-500'}`}
/>
</FormControl>
<FormMessage className='flex justify-end text-[13px] text-state-error' />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter>
<Button type='submit' className='bg-slate-600 text-white' disabled={!form.formState.isValid}>
수정하기
</Button>
</DialogFooter>
</DialogHeader>
</form>
</Form>
);
}
19 changes: 8 additions & 11 deletions src/components/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,16 @@ function SearchResults({ results, query, isLoading }: SearchResultsProps) {
}, [results, query]);

const filteredResults = useMemo(
() => sortedResults.filter((item) => item.content.includes(query) || item.author.includes(query) || item.tags.some((tag) => tag.name.includes(query))),
() =>
sortedResults.filter(
(item) =>
item.content.toLowerCase().includes(query.toLowerCase()) ||
item.author.toLowerCase().includes(query.toLowerCase()) ||
item.tags.some((tag) => tag.name.toLowerCase().includes(query.toLowerCase())),
),
[sortedResults, query],
);

if (isLoading) {
return (
<div className='flex flex-col py-4 px-6 lg:p-6 gap-2 lg:gap-[16px]'>
<div className='flex flex-col gap-1 md:gap-2 lg:gap-6'>
<span className='text-black-600 font-iropkeBatang iropke-lg lg:iropke-xl'>검색 결과를 불러오는 중 입니다...</span>
</div>
</div>
);
}

if (!results || filteredResults.length === 0) {
return (
<div className='flex flex-col py-4 px-6 lg:p-6 gap-2 lg:gap-[16px]'>
Expand Down Expand Up @@ -94,6 +90,7 @@ function SearchResults({ results, query, isLoading }: SearchResultsProps) {
</div>
</Link>
))}
{isLoading && <div className='flex justify-center py-4'>로딩 중...</div>}
</div>
);
}
Expand Down
23 changes: 23 additions & 0 deletions src/context/EmotionContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext, useState, useContext, ReactNode, useMemo } from 'react';

interface EmotionContextType {
shouldRefetch: boolean;
setShouldRefetch: (value: boolean) => void;
}

const EmotionContext = createContext<EmotionContextType>({
shouldRefetch: false,
setShouldRefetch: () => {},
});

export function EmotionProvider({ children }: { children: ReactNode }) {
const [shouldRefetch, setShouldRefetch] = useState(false);

const value = useMemo(() => ({ shouldRefetch, setShouldRefetch }), [shouldRefetch]);

return <EmotionContext.Provider value={value}>{children}</EmotionContext.Provider>;
}

export function useEmotionContext() {
return useContext(EmotionContext);
}
15 changes: 5 additions & 10 deletions src/hooks/useEpigramCommentsQueryHook.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// hooks/useEpigramCommentHook.ts

import { useInfiniteQuery } from '@tanstack/react-query';
import queries from '@/apis/queries';
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
import { CommentResponseType } from '@/schema/comment';
import queries from '@/apis/queries';

const useEpigramCommentsQuery = (epigramId: number) => {
const query = queries.epigramComment.getCommentList({ id: epigramId, limit: 3 });

return useInfiniteQuery({
...query,
const useEpigramCommentsQuery = (epigramId: number) =>
useInfiniteQuery<CommentResponseType, Error, InfiniteData<CommentResponseType>>({
...queries.epigramComment.getComments(epigramId),
initialPageParam: undefined,
getNextPageParam: (lastPage: CommentResponseType) => lastPage.nextCursor ?? undefined,
});
};

export default useEpigramCommentsQuery;
7 changes: 6 additions & 1 deletion src/hooks/useGetEmotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { EmotionType } from '@/types/emotion';
import { GetMonthlyEmotionLogsRequestType } from '@/schema/emotion';
import { useQuery } from '@tanstack/react-query';

export const useMonthlyEmotionLogs = (requset: GetMonthlyEmotionLogsRequestType) => useQuery(quries.emotion.getMonthlyEmotionLogs(requset));
export const useMonthlyEmotionLogs = (request: GetMonthlyEmotionLogsRequestType) =>
useQuery({
...quries.emotion.getMonthlyEmotionLogs(request),
refetchOnWindowFocus: false,
enabled: !!request,
});

export const useGetEmotion = () =>
useQuery<EmotionType | null, Error>({
Expand Down
27 changes: 19 additions & 8 deletions src/hooks/useGetEpigramsHooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import getEpigrams from '@/apis/getEpigrams';
import { useInfiniteQuery, QueryFunctionContext, QueryKey } from '@tanstack/react-query';
import { GetEpigramsResponseType } from '@/schema/epigrams';
import getEpigrams from '@/apis/getEpigrams';

type EpigramsQueryKey = [string, string];

const fetchEpigrams = async ({ pageParam = 0, queryKey }: QueryFunctionContext<EpigramsQueryKey>): Promise<GetEpigramsResponseType> => {
const [, keyword] = queryKey;
const cursor = typeof pageParam === 'number' ? pageParam : undefined;
const response = await getEpigrams({ keyword, limit: 10, cursor });
return response;
};

const useEpigrams = (query: string, page: number, limit: number = 10) =>
useQuery<GetEpigramsResponseType, Error>({
queryKey: ['epigrams', query, page, limit],
queryFn: () => getEpigrams({ keyword: query, limit, cursor: page * limit }),
enabled: !!query,
staleTime: 5 * 60 * 1000, // 데이터 신선도 설정
const useEpigrams = (currentSearch: string) =>
useInfiniteQuery<GetEpigramsResponseType, Error>({
queryKey: ['epigrams', currentSearch] as unknown as QueryKey,
queryFn: fetchEpigrams as unknown as ({ pageParam, queryKey }: QueryFunctionContext<QueryKey>) => Promise<GetEpigramsResponseType>,
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
enabled: !!currentSearch,
staleTime: 5 * 60 * 1000,
});

export default useEpigrams;
Loading

0 comments on commit 41042ef

Please sign in to comment.