Skip to content

Commit

Permalink
FE-37 ✨ 마이페이지 감정차트 (#89)
Browse files Browse the repository at this point in the history
* FE-37 ♻️ 월별 감정로그 조회 코드 리팩토링

* FE-37 ✨ 감정 차트

* FE-37 🔨 상수 컨벤션 수정
  • Loading branch information
JeonYumin94 authored Jul 28, 2024
1 parent 10a42ad commit 569f2e0
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 45 deletions.
9 changes: 1 addition & 8 deletions src/hooks/useCalendar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { getDaysInMonth } from 'date-fns';
import React, { useState } from 'react';
import { CALENDAR_LENGTH, DAY_OF_WEEK } from '../user/utill/constants';

interface CalendarData {
weekCalendarList: number[][]; // 주별 날짜 리스트
currentDate: Date; // 현재 날짜
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>; // 현재 날짜를 설정하는 함수
}

// 이전 달의 날짜를 계산하는 함수
Expand All @@ -21,9 +18,7 @@ const getNextDays = (currentDayList: number[], prevDayList: number[]): number[]
return Array.from({ length: Math.max(nextDayCount, 0) }, (_, index) => index + 1);
};

const useCalendar = (): CalendarData => {
const [currentDate, setCurrentDate] = useState<Date>(new Date()); // 현재 날짜를 상태로 관리

const useCalendar = (currentDate: Date): CalendarData => {
// 현재 월의 총 날짜 수를 가져옴
const totalMonthDays = getDaysInMonth(currentDate);

Expand Down Expand Up @@ -66,8 +61,6 @@ const useCalendar = (): CalendarData => {
// 캘린더 정보를 반환
return {
weekCalendarList, // 주별 날짜 리스트
currentDate,
setCurrentDate,
};
};

Expand Down
5 changes: 2 additions & 3 deletions src/pageLayout/MypageLayout/MyPageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Header from '@/components/Header/Header';
import { useMeQuery } from '@/hooks/userQueryHooks';
import UserInfo from '@/types/user';
import Calendar from '@/user/ui-profile/Calendar';
import EmotionMonthlyLogs from '@/user/ui-profile/EmotionMonthlyLogs';
import Profile from '@/user/ui-profile/Profile';
import { useRouter } from 'next/navigation';

Expand Down Expand Up @@ -31,8 +31,7 @@ export default function MyPageLayout() {
<div className='w-full flex flex-col items-center bg-blue-100 rounded-3xl relative shadow-3xl'>
<Profile image={data.image} nickname={data.nickname} />
<div className='flex flex-col w-full lg:max-w-[640px] md:max-w-[640px] mt-[160px] space-y-0 md:mb-10 mb-5 border border-black'>오늘의 감정</div>
<Calendar userId={data.id} />
<div className='flex flex-col w-full lg:max-w-[640px] md:max-w-[640px] mt-[160px] space-y-0 md:mb-10 mb-5 border border-black'>감정차트</div>
<EmotionMonthlyLogs userId={data.id} />
</div>
<div className='bg-background-100 flex flex-col items-center w-full py-[100px]'>
<div className='flex flex-col w-full lg:max-w-[640px] md:max-w-[640px] gap-12'>
Expand Down
7 changes: 7 additions & 0 deletions src/types/emotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ export interface Emotion {

// 감정 로그 타입 지정
export type EmotionType = 'MOVED' | 'HAPPY' | 'WORRIED' | 'SAD' | 'ANGRY';

export interface EmotionLog {
id: number;
userId: number;
emotion: EmotionType;
createdAt: Date;
}
34 changes: 8 additions & 26 deletions src/user/ui-profile/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import Image from 'next/image';
import { subMonths } from 'date-fns';
import useMonthlyEmotionLogs from '@/hooks/useGetEmotion';
import { Emotion, EmotionType } from '@/types/emotion';
import { EmotionLog, EmotionType } from '@/types/emotion';
import useCalendar from '../../hooks/useCalendar';
import { DAY_LIST, DATE_MONTH_FIXER, iconPaths } from '../utill/constants';
import CalendarHeader from './CalendarHeader';

interface CalendarProps {
userId: number;
currentDate: Date; // 현재 날짜
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>; // 현재 날짜를 설정하는 함수
monthlyEmotionLogs: EmotionLog[];
}

export default function Calendar({ userId }: CalendarProps) {
export default function Calendar({ currentDate, setCurrentDate, monthlyEmotionLogs }: CalendarProps) {
// 캘린더 함수 호출
const { weekCalendarList, currentDate, setCurrentDate } = useCalendar();
const { weekCalendarList } = useCalendar(currentDate);
// 감정 필터
const [selectedEmotion, setSelectedEmotion] = useState<EmotionType | null>(null);

// 감정 달력 객체 상태 추가
const [emotionRequest, setEmotionRequest] = useState<Emotion>({
userId,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
});

// 월별 감정 로그 조회
const { data: monthlyEmotionLogs = [] } = useMonthlyEmotionLogs(emotionRequest);

// 달력에 출력할 수 있게 매핑
const emotionMap: Record<string, EmotionType> = Array.isArray(monthlyEmotionLogs)
? monthlyEmotionLogs.reduce<Record<string, EmotionType>>((acc, log) => {
Expand All @@ -37,15 +28,6 @@ export default function Calendar({ userId }: CalendarProps) {
}, {})
: {};

// '월'이 변경될 때마다 request 업데이트
useEffect(() => {
setEmotionRequest({
userId,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
});
}, [currentDate]);

// 이전 달 클릭
const handlePrevMonth = () => setCurrentDate((prevDate) => subMonths(prevDate, DATE_MONTH_FIXER));
// 다음 달 클릭
Expand Down Expand Up @@ -86,7 +68,7 @@ export default function Calendar({ userId }: CalendarProps) {
const isToday = day === currentDate.getDate() && currentDate.getMonth() === new Date().getMonth() && currentDate.getFullYear() === new Date().getFullYear();
const dateString = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const emotion: EmotionType = filteredEmotionMap[dateString]; // 날짜에 해당하는 감정 가져오기
const iconPath = iconPaths[emotion]; // 해당 감정 아이콘 출력
const iconPath = emotion && iconPaths[emotion] ? iconPaths[emotion].path : '/icon/BW/SmileFaceBWIcon.svg';

return (
<div
Expand Down
6 changes: 3 additions & 3 deletions src/user/ui-profile/CalendarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ export default function CalendarHeader({ currentDate, onPrevMonth, onNextMonth,
</DropdownMenuTrigger>
<DropdownMenuContent className='bg-white'>
<DropdownMenuGroup className='flex flex-row'>
{Object.entries(iconPaths).map(([emotionKey, iconPath]) => (
{Object.entries(iconPaths).map(([emotionKey, { path, name }]) => (
<DropdownMenuItem key={emotionKey}>
<Button
className={`p-0 w-14 h-14 bg-slate-400 bg-opacity-20 rounded-2xl flex justify-center ${selectEmotion === emotionKey ? `border-4 border-illust-yellow` : ''}`}
className={`p-0 w-14 h-14 bg-opacity-20 rounded-2xl flex justify-center ${selectEmotion === emotionKey ? 'border-4 border-illust-yellow' : ''}`}
onClick={() => onEmotionSelect(emotionKey as EmotionType)}
>
<Image src={iconPath} alt='감정' width={36} height={36} />
<Image src={path} alt={name} width={36} height={36} />
</Button>
</DropdownMenuItem>
))}
Expand Down
96 changes: 96 additions & 0 deletions src/user/ui-profile/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { EmotionLog, EmotionType } from '@/types/emotion';
import Image from 'next/image';
import { iconPaths } from '../utill/constants';

interface ChartProps {
monthlyEmotionLogs: EmotionLog[];
}

export default function Chart({ monthlyEmotionLogs }: ChartProps) {
// 감정별 빈도수 계산
const emotionCounts = monthlyEmotionLogs.reduce(
(count, log) => {
const { emotion } = log;
return {
...count, // 기존의 count를 복사
[emotion]: (count[emotion] || 0) + 1, // 현재 감정의 개수 증가
};
},
{} as Record<string, number>,
);

// 감정 종류 및 총 감정 수 계산
const TOTAL_COUNT = monthlyEmotionLogs.length;
const EMOTIONS: EmotionType[] = ['MOVED', 'HAPPY', 'WORRIED', 'SAD', 'ANGRY'];
const RADIUS = 90; // 원의 반지름
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;

// 가장 많이 나타나는 감정 찾기
const maxEmotion = EMOTIONS.reduce((max, emotion) => (emotionCounts[emotion] > emotionCounts[max] ? emotion : max), EMOTIONS[0]);

// 원형 차트의 각 감정에 대한 strokeDasharray와 strokeDashoffset 계산
let offset = 0;

return (
<div className='flex flex-col w-full lg:max-w-[640px] md:max-w-[640px] mt-[160px] space-y-0 md:mb-10 mb-5 gap-[48px]'>
<h2 className='text-neutral-700 text-2xl font-semibold leading-loose'>감정 차트</h2>
<div className='flex justify-between items-center px-[112px]'>
<div className='w-[200px] h-[200px] relative'>
<svg viewBox='0 0 200 200'>
<circle cx='100' cy='100' r={RADIUS} fill='none' stroke='beige' strokeWidth='20' />
{EMOTIONS.map((emotion) => {
const count = emotionCounts[emotion] || 0;
const percentage = TOTAL_COUNT > 0 ? count / TOTAL_COUNT : 0; // 0으로 나누기 방지
const strokeDasharray = `${CIRCUMFERENCE * percentage} ${CIRCUMFERENCE * (1 - percentage)}`;

// 색상 설정
let strokeColor;
switch (emotion) {
case 'HAPPY':
strokeColor = '#FBC85B';
break;
case 'SAD':
strokeColor = '#E3E9F1';
break;
case 'WORRIED':
strokeColor = '#C7D1E0';
break;
case 'ANGRY':
strokeColor = '#EFF3F8';
break;
default:
strokeColor = '#48BB98';
}

const circle = <circle key={emotion} cx='100' cy='100' r={RADIUS} fill='none' stroke={strokeColor} strokeWidth='20' strokeDasharray={strokeDasharray} strokeDashoffset={offset} />;

offset += CIRCUMFERENCE * percentage; // 다음 원을 위한 offset 업데이트
return circle;
})}
</svg>
{/* 중앙에 가장 많이 나타나는 감정 출력 */}
<div className='absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-3'>
<Image src={iconPaths[maxEmotion].path} alt='감정' width={40} height={40} />
<p>{iconPaths[maxEmotion].name}</p>
</div>
</div>
<div>
<div className='flex flex-col gap-4'>
{EMOTIONS.map((emotion) => {
const count = emotionCounts[emotion] || 0;
const percentage = TOTAL_COUNT > 0 ? Math.floor((count / TOTAL_COUNT) * 100) : 0; // 퍼센트 계산 및 소수점 버리기

return (
<div key={emotion} className='flex items-center gap-3'>
<p className={`${iconPaths[emotion].color} w-[16px] h-[16px]`}></p>
<Image src={iconPaths[emotion].path} alt='감정' width={24} height={24} />
<p>{percentage}%</p>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
40 changes: 40 additions & 0 deletions src/user/ui-profile/EmotionMonthlyLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import useMonthlyEmotionLogs from '@/hooks/useGetEmotion';
import { Emotion } from '@/types/emotion';
import { useEffect, useState } from 'react';
import Calendar from './Calendar';
import Chart from './Chart';

interface EmotionMonthlyLogsProps {
userId: number;
}

export default function EmotionMonthlyLogs({ userId }: EmotionMonthlyLogsProps) {
// 현재 날짜를 상태로 관리
const [currentDate, setCurrentDate] = useState<Date>(new Date());

// 감정 달력 객체 상태 추가
const [emotionRequest, setEmotionRequest] = useState<Emotion>({
userId,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
});

// '월'이 변경될 때마다 request 업데이트
useEffect(() => {
setEmotionRequest({
userId,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
});
}, [currentDate]);

// 월별 감정 로그 조회
const { data: monthlyEmotionLogs = [] } = useMonthlyEmotionLogs(emotionRequest);

return (
<>
<Calendar currentDate={currentDate} setCurrentDate={setCurrentDate} monthlyEmotionLogs={monthlyEmotionLogs} />
<Chart monthlyEmotionLogs={monthlyEmotionLogs} />
</>
);
}
10 changes: 5 additions & 5 deletions src/user/utill/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export const DEFAULT_TRASH_VALUE = -1; // 기본값 설정 (필요에 따라 사

// 아이콘 파일 경로 매핑
export const iconPaths = {
MOVED: '/icon/Color/HeartFaceColorIcon.svg',
HAPPY: '/icon/Color/SmileFaceColorIcon.svg',
WORRIED: '/icon/Color/ThinkFaceColorIcon.svg',
SAD: '/icon/Color/SadFaceColorIcon.svg',
ANGRY: '/icon/Color/AngryFaceColorIcon.svg',
MOVED: { path: '/icon/Color/HeartFaceColorIcon.svg', name: '기쁨', color: 'bg-illust-green' },
HAPPY: { path: '/icon/Color/SmileFaceColorIcon.svg', name: '감동', color: 'bg-illust-yellow' },
WORRIED: { path: '/icon/Color/ThinkFaceColorIcon.svg', name: '고민', color: 'bg-sub-gray_1' },
SAD: { path: '/icon/Color/SadFaceColorIcon.svg', name: '슬픔', color: 'bg-sub-gray_2' },
ANGRY: { path: '/icon/Color/AngryFaceColorIcon.svg', name: '분노', color: 'bg-sub-gray_3' },
};

0 comments on commit 569f2e0

Please sign in to comment.