diff --git a/FE/package-lock.json b/FE/package-lock.json index d4a950d..8d41f2d 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -14,6 +14,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "date-fns": "^2.30.0", + "dayjs": "^1.11.10", "react": "^18.2.0", "react-datepicker": "^4.24.0", "react-dom": "^18.2.0", @@ -7209,6 +7210,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/FE/package.json b/FE/package.json index 22e94f7..a2a36fc 100644 --- a/FE/package.json +++ b/FE/package.json @@ -8,6 +8,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "dayjs": "^1.11.10", "date-fns": "^2.30.0", "react": "^18.2.0", "react-datepicker": "^4.24.0", diff --git a/FE/src/assets/leftIcon.svg b/FE/src/assets/leftIcon.svg new file mode 100644 index 0000000..29d75ea --- /dev/null +++ b/FE/src/assets/leftIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/FE/src/assets/picket.svg b/FE/src/assets/picket.svg new file mode 100644 index 0000000..ecc07e6 --- /dev/null +++ b/FE/src/assets/picket.svg @@ -0,0 +1,3 @@ + + + diff --git a/FE/src/assets/rightIcon.svg b/FE/src/assets/rightIcon.svg new file mode 100644 index 0000000..4c4d281 --- /dev/null +++ b/FE/src/assets/rightIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/FE/src/assets/toggleIcon.svg b/FE/src/assets/toggleIcon.svg new file mode 100644 index 0000000..5097b11 --- /dev/null +++ b/FE/src/assets/toggleIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/FE/src/atoms/diaryAtom.js b/FE/src/atoms/diaryAtom.js index 96641a4..6237fe3 100644 --- a/FE/src/atoms/diaryAtom.js +++ b/FE/src/atoms/diaryAtom.js @@ -8,6 +8,7 @@ const diaryAtom = atom({ isUpdate: false, isDelete: false, isList: false, + isAnalysis: false, diaryUuid: "", diaryPoint: "", diaryList: [], diff --git a/FE/src/components/Button/SwitchButton.js b/FE/src/components/Button/SwitchButton.js index fe330f1..432beaf 100644 --- a/FE/src/components/Button/SwitchButton.js +++ b/FE/src/components/Button/SwitchButton.js @@ -48,6 +48,16 @@ const SwitchButtonWrapper = styled.div` overflow: hidden; cursor: pointer; + + animation: modalFadeIn 0.5s; + @keyframes modalFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } `; const SwitchButtonContent = styled.div` diff --git a/FE/src/components/DiaryModal/Calendar.js b/FE/src/components/DiaryModal/Calendar.js new file mode 100644 index 0000000..813ae3a --- /dev/null +++ b/FE/src/components/DiaryModal/Calendar.js @@ -0,0 +1,306 @@ +import React, { useState, useLayoutEffect } from "react"; +import styled from "styled-components"; +import toggleIcon from "../../assets/toggleIcon.svg"; +import leftIcon from "../../assets/leftIcon.svg"; +import rightIcon from "../../assets/rightIcon.svg"; + +function getCalendarDate(date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + + const firstDay = new Date(year, month - 1, 1); + const lastDay = new Date(year, month, 0); + + const firstDayOfWeek = firstDay.getDay(); + const lastDayOfWeek = lastDay.getDay(); + + const firstDate = firstDay.getDate(); + const lastDate = lastDay.getDate(); + + const calendarDate = []; + + for (let i = 0; i < firstDayOfWeek; i += 1) { + calendarDate.push(""); + } + + for (let i = firstDate; i <= lastDate; i += 1) { + calendarDate.push(i); + } + + for (let i = lastDayOfWeek; i < 6; i += 1) { + calendarDate.push(""); + } + + return calendarDate; +} + +const getColor = (index) => { + if (index % 7 === 0) { + return "red"; + } + if (index % 7 === 6) { + return "blue"; + } + return "black"; +}; + +function Calendar(props) { + const { date, setData } = props; + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + const [calendarDate, setCalendarDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date()); + + useLayoutEffect(() => { + setCalendarDate(date); + setSelectedDate(date); + }, [date]); + + return ( + + setIsCalendarOpen(!isCalendarOpen)}> + + {selectedDate.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + })} + + + + {isCalendarOpen && ( + + + + setCalendarDate( + new Date( + calendarDate.getFullYear(), + calendarDate.getMonth() - 1, + calendarDate.getDate(), + ), + ) + } + /> + + {calendarDate.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + })} + + + setCalendarDate( + new Date( + calendarDate.getFullYear(), + calendarDate.getMonth() + 1, + calendarDate.getDate(), + ), + ) + } + /> + + + + {["일", "월", "화", "수", "목", "금", "토"].map((item) => ( + {item} + ))} + + + {getCalendarDate(calendarDate).map((item, index) => ( + {} + : () => { + setSelectedDate( + new Date( + calendarDate.getFullYear(), + calendarDate.getMonth(), + item, + ), + ); + setData((prev) => ({ + ...prev, + date: new Date( + calendarDate.getFullYear(), + calendarDate.getMonth(), + item, + ), + })); + } + } + > + + {item} + + + ))} + + + + )} + + ); +} + +const CalendarWrapper = styled.div` + height: 1.5rem; + display: flex; + justify-content: space-between; + gap: 1rem; + cursor: pointer; +`; + +const CalendarHeader = styled.div` + height: 100%; + z-index: 3000; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.8rem; +`; + +const CalendarHeaderTitle = styled.div` + height: 100%; + display: flex; + justify-content: space-between; + gap: 1rem; + font-size: 1.2rem; +`; + +const CalendarButton = styled.img` + width: 0.9rem; + height: 0.9rem; + + position: relative; + top: -0.1rem; + + &.rotate { + transform: rotate(-180deg); + transition: transform 0.25s; + } + + &.unrotate { + transform: rotate(0deg); + transition: transform 0.25s; + } +`; + +const CalendarBodyWrapper = styled.div` + width: 15rem; + padding: 1rem; + z-index: 2000; + + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + + position: absolute; + top: 5.5rem; + left: 3.5rem; + + background-color: #bbc2d4; + border-radius: 0.5rem; + + cursor: default; +`; + +const CalendarBody = styled.div` + width: 100%; + color: black; + + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const CalendarBodyHeaderWrapper = styled.div` + width: 100%; + height: 2rem; + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + color: black; +`; + +const CalendarBodyHeader = styled.div` + width: 45%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; + +const ArrowButton = styled.img` + width: 0.7rem; + height: 0.7rem; + cursor: pointer; +`; + +const CalendarBodyDayWrapper = styled.div` + width: 100%; + height: 1.8rem; + background-color: #000000dd; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + display: flex; + justify-content: space-between; + align-items: center; + color: white; + font-size: 0.8rem; +`; + +const CalendarBodyDay = styled.div` + width: 14%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +`; + +const CalendarBodyDateWrapper = styled.div` + width: 100%; + height: 100%; + background-color: #ffffff; + border-bottom-left-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + overflow: hidden; + display: flex; + justify-content: space-between; + flex-wrap: wrap; +`; + +const CalendarBodyDate = styled.div` + width: 14%; + height: 2rem; + display: flex; + justify-content: center; + align-items: center; + + background-color: #ffffff; + cursor: pointer; + + &:hover { + background-color: #e0e0e0; + } +`; + +const CalendarBodyDateNumber = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + color: ${(props) => props.color}; + font-size: 0.8rem; +`; + +export default Calendar; diff --git a/FE/src/components/DiaryModal/DiaryAnalysisModal.js b/FE/src/components/DiaryModal/DiaryAnalysisModal.js new file mode 100644 index 0000000..763828e --- /dev/null +++ b/FE/src/components/DiaryModal/DiaryAnalysisModal.js @@ -0,0 +1,612 @@ +import React, { useEffect, useState } from "react"; +import { useQuery } from "react-query"; +import { useRecoilState, useRecoilValue } from "recoil"; +import styled from "styled-components"; +import dayjs from "dayjs"; +import userAtom from "../../atoms/userAtom"; +import shapeAtom from "../../atoms/shapeAtom"; +import { preventBeforeUnload } from "../../utils/utils"; +import DiaryEmotionIndicator from "./EmotionIndicator/DiaryEmotionIndicator"; +import Tag from "../../styles/Modal/Tag"; +import leftIcon from "../../assets/leftIcon.svg"; +import rightIcon from "../../assets/rightIcon.svg"; + +function DiaryAnalysisModal() { + const [buttonDisabled, setButtonDisabled] = useState(false); + const [currentYear, setCurrentYear] = useState(dayjs("2023")); + const [emotion, setEmotion] = useState({ + positive: 0, + negative: 0, + neutral: 0, + }); + const [monthAnalysis, setMonthAnalysis] = useState(Array(12).fill(0)); + const [userState, setUserState] = useRecoilState(userAtom); + + async function getDataFn(data) { + return fetch( + `http://223.130.129.145:3005/stat/${data}/${currentYear.year()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${userState.accessToken}`, + }, + }, + ).then((res) => { + if (res.status === 200) { + return res.json(); + } + if (res.status === 403) { + alert("로그인이 만료되었습니다. 다시 로그인해주세요."); + localStorage.removeItem("accessToken"); + sessionStorage.removeItem("accessToken"); + window.removeEventListener("beforeunload", preventBeforeUnload); + window.location.href = "/"; + } + if (res.status === 401) { + return fetch("http://223.130.129.145:3005/auth/reissue", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${userState.accessToken}`, + }, + }) + .then((res) => res.json()) + .then((data) => { + if (localStorage.getItem("accessToken")) { + localStorage.setItem("accessToken", data.accessToken); + } + if (sessionStorage.getItem("accessToken")) { + sessionStorage.setItem("accessToken", data.accessToken); + } + setUserState((prev) => ({ + ...prev, + accessToken: data.accessToken, + })); + }); + } + return {}; + }); + } + + const { data: tagsRankData, refetch: tagsRankRefetch } = useQuery( + ["tagsRank"], + async () => { + const result = await getDataFn("tags-rank"); + return result; + }, + ); + + const { data: shapesRankData, refetch: shapesRankRefetch } = useQuery( + ["shapesRank"], + async () => { + const result = await getDataFn("shapes-rank"); + return result; + }, + { + onSuccess: () => { + tagsRankRefetch(); + }, + }, + ); + + const { data: diaryAnalysisData, refetch: diaryAnalysisRefetch } = useQuery( + ["diaryAnalysis"], + async () => { + const result = await getDataFn("diaries"); + return result; + }, + { + onSuccess: (data) => { + const newEmotion = { + positive: 0, + negative: 0, + neutral: 0, + }; + const newMonthAnalysis = Array(12).fill(0); + Object.keys(data).forEach((date) => { + const { sentiment } = data[date]; + newEmotion[sentiment] += 1; + newMonthAnalysis[dayjs(date).month()] += 1; + }); + setEmotion({ + positive: + (newEmotion.positive * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + negative: + (newEmotion.negative * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + neutral: + (newEmotion.neutral * 100) / + Object.values(newEmotion).reduce((acc, cur) => acc + cur, 0), + }); + setMonthAnalysis(newMonthAnalysis); + shapesRankRefetch(); + }, + }, + ); + + useEffect(() => { + diaryAnalysisRefetch(); + }, [currentYear]); + + return ( + + + + + {currentYear.year()}년의 감정 + + + { + if (!buttonDisabled) { + setButtonDisabled(true); + setCurrentYear(currentYear.subtract(1, "y")); + + setTimeout(() => { + setButtonDisabled(false); + }, 500); + } + }} + /> + { + if (!buttonDisabled) { + setButtonDisabled(true); + setCurrentYear(currentYear.add(1, "y")); + + setTimeout(() => { + setButtonDisabled(false); + }, 500); + } + }} + /> + + + {diaryAnalysisData && ( + + {["일", "월", "화", "수", "목", "금", "토"].map((day) => ( + + {day} + + ))} + { + // dayjs로 1월 1일 이 무슨 요일인지 알아내서 그거에 맞게 빈칸 넣어주기 + Array.from({ length: currentYear.day() }, (v, i) => i + 1).map( + (day) => ( + + ), + ) + } + {Array.from( + { + length: -currentYear.diff( + dayjs(currentYear).endOf("year"), + "day", + ), + }, + (v, i) => i + 1, + ).map((day) => { + let color = "#bbbbbb"; + const date = currentYear.add(day, "d").format("YYYY-MM-DD"); + if (date in diaryAnalysisData) { + const { sentiment } = diaryAnalysisData[date]; + if (sentiment === "positive") { + color = "#618cf7"; + } else if (sentiment === "negative") { + color = "#e5575b"; + } else if (sentiment === "neutral") { + color = "#a848f6"; + } + } + + return ; + })} + + )} + {diaryAnalysisData && Object.keys(diaryAnalysisData).length !== 0 ? ( + + + + 올해의 감정 상태 + + + 마우스를 올려 수치를 확인해보세요. + + + + + + + + + 긍정 + + + + + + 부정 + + + + + + 중립 + + + + + + ) : null} + + + + + + 월별 통계 + + + 총 일기 수{" "} + {diaryAnalysisData + ? Object.values(diaryAnalysisData).reduce( + (acc, cur) => acc + cur.count, + 0, + ) + : 0} + 개 + + + + {monthAnalysis.map((month, index) => ( + + + + {index + 1} + + + ))} + + + + + + 가장 많이 쓴 태그 순위 + + + + {tagsRankData && tagsRankData?.first ? ( + + ) : null} + {tagsRankData && tagsRankData?.second ? ( + + ) : null} + {tagsRankData && tagsRankData?.third ? ( + + ) : null} + + + + + + 가장 많이 쓴 모양 순위 + + + + {shapesRankData && shapesRankData?.first ? ( + + ) : null} + {shapesRankData && shapesRankData?.second ? ( + + ) : null} + {shapesRankData && shapesRankData?.third ? ( + + ) : null} + + + + + ); +} + +function TagRanking(props) { + const { rank, tag, count } = props; + + return ( + + + {rank}위 + + {count}회 + + + {tag} + + ); +} + +function ShapeRanking(props) { + const { rank, uuid, count } = props; + const shapeState = useRecoilValue(shapeAtom); + + return ( + + + {rank}위 + + {count}회 + + +
shape.uuid === uuid)?.data, + }} + style={{ width: "100%", height: "100%" }} + /> + + ); +} + +// 일기 나열 페이지와 중복되는 부분이 많아서 일단은 일기 나열 페이지를 재활용했습니다. +const DiaryAnalysisModalWrapper = styled.div` + width: 95%; + height: 97.5%; + padding: 0 2.5%; + position: absolute; + top: 2.5%; + z-index: 1001; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2%; +`; + +const DiaryAnalysisModalItem = styled.div` + width: ${(props) => props.width || "33%"}; + height: ${(props) => props.height || "85%"}; + background-color: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 1rem; + + display: flex; + flex-direction: column; + align-items: center; + + font-size: 1.3rem; + color: #ffffff; + + overflow: auto; + + animation: modalFadeIn 0.5s; + @keyframes modalFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const ArrowButtonWrapper = styled.div` + width: 5%; + height: 2rem; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const ArrowButton = styled.img` + width: 1rem; + height: 1rem; + filter: ${(props) => props.filter || "invert(1)"}; + cursor: pointer; +`; + +const StreakBar = styled.div` + width: 65rem; + padding: 2% 0; + margin: 0 auto; + display: grid; + grid-auto-flow: column; + grid-template-columns: repeat(54, 1fr); + grid-template-rows: repeat(7, 1fr); + gap: 0.2rem; +`; + +const DailyStreak = styled.div` + width: 1rem; + height: 1rem; + flex-shrink: 0; + border-radius: 20%; + background-color: ${(props) => props.$bg || "#bbbbbb"}; + font-size: 0.8rem; + display: flex; + justify-content: center; + align-items: center; +`; + +const EmotionBar = styled.div` + width: 85%; + height: 15%; + margin: 3rem 0; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 2.5rem; +`; + +const EmotionBarTextWrapper = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: flex-start; + align-items: flex-end; + gap: 1.5rem; +`; + +const EmotionBarContentWrapper = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 1rem; +`; + +const EmotionStreakBar = styled.div` + width: 14rem; + height: 100%; + display: flex; + justify-content: space-between; + gap: 0.5rem; +`; + +const EmotionStreak = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; +`; + +const DiaryAnalysisModalSubItemWrapper = styled.div` + width: 80%; + height: 30%; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.3%; + + overflow: hidden; +`; + +const DiaryAnalysisModalTitleWrapper = styled.div` + width: ${(props) => props.width || "80%"}; + height: 5rem; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const DiaryAnalysisModalText = styled.div` + font-size: ${(props) => props.size || "1.3rem"}; + color: ${(props) => props.color || "#ffffff"}; +`; + +const DiaryAnalysisModalContentWrapper = styled.div` + width: 100%; + height: 65%; + display: flex; + flex-direction: ${(props) => props.direction || "column"}; + justify-content: center; + align-items: center; +`; + +const MonthGraphBar = styled.div` + width: 85%; + flex-grow: 0.8; + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 5%; +`; + +const MonthGraphWrapper = styled.div` + width: 0.7rem; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + gap: 10%; +`; + +const MonthGraph = styled.div` + width: 120%; + height: ${(props) => props.height || "100%"}; + background-color: #bbbbbb; + border-radius: 0.2rem; +`; + +const TagRankingWrapper = styled.div` + width: 80%; + height: 15%; + padding-bottom: 7%; + display: flex; + align-items: center; + gap: 5%; +`; + +const TagRankingTextWrapper = styled.div` + width: 5rem; + display: flex; + align-items: flex-end; + gap: 1rem; +`; + +const ShapeRankingWrapper = styled.div` + width: 30%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; +`; + +const ShapeRankingTextWrapper = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: flex-end; + gap: 10%; +`; + +export default DiaryAnalysisModal; diff --git a/FE/src/components/DiaryModal/DiaryCreateModal.js b/FE/src/components/DiaryModal/DiaryCreateModal.js index 82dc77c..6fb2dd6 100644 --- a/FE/src/components/DiaryModal/DiaryCreateModal.js +++ b/FE/src/components/DiaryModal/DiaryCreateModal.js @@ -6,9 +6,9 @@ import userAtom from "../../atoms/userAtom"; import diaryAtom from "../../atoms/diaryAtom"; import shapeAtom from "../../atoms/shapeAtom"; import ModalWrapper from "../../styles/Modal/ModalWrapper"; -import DiaryModalHeader from "../../styles/Modal/DiaryModalHeader"; +import Calendar from "./Calendar"; import deleteIcon from "../../assets/deleteIcon.svg"; -import preventBeforeUnload from "../../utils/utils"; +import { preventBeforeUnload, getFormattedDate } from "../../utils/utils"; function DiaryCreateModal(props) { const { refetch } = props; @@ -21,7 +21,7 @@ function DiaryCreateModal(props) { const [diaryData, setDiaryData] = useState({ title: "", content: "", - date: "2023-11-19", + date: new Date(), point: diaryState.diaryPoint, tags: [], shapeUuid: "", @@ -57,13 +57,22 @@ function DiaryCreateModal(props) { }; async function createDiaryFn(data) { + const diaryData = { + title: data.diaryData.title, + content: data.diaryData.content, + date: getFormattedDate(data.diaryData.date), + point: data.diaryData.point, + tags: data.diaryData.tags, + shapeUuid: data.diaryData.shapeUuid, + }; + return fetch("http://223.130.129.145:3005/diaries", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${data.accessToken}`, }, - body: JSON.stringify(data.diaryData), + body: JSON.stringify(diaryData), }) .then((res) => { if (res.status === 201) { @@ -93,11 +102,8 @@ function DiaryCreateModal(props) { } = useMutation(createDiaryFn); return ( - - - 새로운 별의 이야기를 적어주세요. - {diaryData.date} - + + props.width || "2.5rem"}; height: 2.5rem; - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); border-radius: 2rem; z-index: 1001; @@ -294,8 +300,7 @@ const ModalSideButton = styled.div` cursor: pointer; &:hover { - background-color: rgba(255, 255, 255, 0.5); - transition: 0.25s; + background-color: rgba(255, 255, 255, 0.3); } `; @@ -303,10 +308,6 @@ const DiaryModalTitle = styled.h1` font-size: 1.5rem; `; -const DiaryModalDate = styled.div` - color: rgba(0, 0, 0, 0.55); -`; - const DiaryModalInputBox = styled.input` width: 100%; height: 3rem; @@ -390,7 +391,7 @@ const DiaryModalTagBox = styled.div` padding: 0.5rem 1rem; border-radius: 1.5rem; border: 1px solid #ffffff; - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); flex-shrink: 0; diff --git a/FE/src/components/DiaryModal/DiaryListModal.js b/FE/src/components/DiaryModal/DiaryListModal.js index 04d42ec..aed8dd1 100644 --- a/FE/src/components/DiaryModal/DiaryListModal.js +++ b/FE/src/components/DiaryModal/DiaryListModal.js @@ -371,7 +371,7 @@ const DiaryListModalWrapper = styled.div` const DiaryListModalItem = styled.div` width: ${(props) => props.$width || "25%"}; height: 85%; - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); border-radius: 1rem; @@ -617,7 +617,7 @@ const DiaryTitleListItem = styled.div` text-overflow: ellipsis; &:hover { - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); } `; diff --git a/FE/src/components/DiaryModal/DiaryLoadingModal.js b/FE/src/components/DiaryModal/DiaryLoadingModal.js index d0229a3..3d83239 100644 --- a/FE/src/components/DiaryModal/DiaryLoadingModal.js +++ b/FE/src/components/DiaryModal/DiaryLoadingModal.js @@ -48,6 +48,7 @@ function DiaryLoadingModal() { const DiaryLoadingModalWrapper = styled(ModalWrapper)` width: 10rem; height: 6rem; + background-color: rgba(255, 255, 255, 0.2); padding: 2rem 4rem; top: 45%; left: 50%; diff --git a/FE/src/components/DiaryModal/DiaryReadModal.js b/FE/src/components/DiaryModal/DiaryReadModal.js index 71eb0e7..2b80c45 100644 --- a/FE/src/components/DiaryModal/DiaryReadModal.js +++ b/FE/src/components/DiaryModal/DiaryReadModal.js @@ -6,41 +6,11 @@ import diaryAtom from "../../atoms/diaryAtom"; import userAtom from "../../atoms/userAtom"; import shapeAtom from "../../atoms/shapeAtom"; import ModalWrapper from "../../styles/Modal/ModalWrapper"; +import Tag from "../../styles/Modal/Tag"; import DiaryDeleteModal from "./DiaryDeleteModal"; +import DiaryEmotionIndicator from "./EmotionIndicator/DiaryEmotionIndicator"; import editIcon from "../../assets/edit.svg"; import deleteIcon from "../../assets/delete.svg"; -import indicatorArrowIcon from "../../assets/indicator-arrow.svg"; - -function DiaryModalEmotionIndicator({ emotion }) { - return ( - - - - - arrow - - - - arrow - - - - - 긍정 {emotion.positive}% - 중립 {emotion.neutral}% - 부정 {emotion.negative}% - - - ); -} async function getDiary(accessToken, diaryUuid, setUserState) { return fetch(`http://223.130.129.145:3005/diaries/${diaryUuid}`, { @@ -119,20 +89,20 @@ function DiaryReadModal(props) { if (isLoading) return ( - + Loading... ); if (isError) return ( - + 에러 발생 ); return ( - + {data.title} 태그 {data.tags?.map((tag) => ( - {tag} + {tag} ))} -
props.$ratio}; - height: 100%; - background-color: ${(props) => props.color}; -`; - -const EmotionIndicatorArrow = styled.div` - display: flex; - justify-content: center; - width: 0; - height: 4rem; -`; - -const EmotionTextWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 0.5rem; -`; - -const EmotionText = styled.div` - font-size: 0.9rem; -`; - export default DiaryReadModal; diff --git a/FE/src/components/DiaryModal/DiaryUpdateModal.js b/FE/src/components/DiaryModal/DiaryUpdateModal.js index f24a593..53c7c34 100644 --- a/FE/src/components/DiaryModal/DiaryUpdateModal.js +++ b/FE/src/components/DiaryModal/DiaryUpdateModal.js @@ -1,6 +1,6 @@ /* eslint-disable */ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useLayoutEffect, useRef } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; import { useMutation, useQuery } from "react-query"; import styled from "styled-components"; @@ -8,19 +8,9 @@ import userAtom from "../../atoms/userAtom"; import diaryAtom from "../../atoms/diaryAtom"; import shapeAtom from "../../atoms/shapeAtom"; import ModalWrapper from "../../styles/Modal/ModalWrapper"; -import DiaryModalHeader from "../../styles/Modal/DiaryModalHeader"; +import Calendar from "./Calendar"; import deleteIcon from "../../assets/deleteIcon.svg"; -import preventBeforeUnload from "../../utils/utils"; - -async function getDiary(accessToken, diaryUuid) { - return fetch(`http://223.130.129.145:3005/diaries/${diaryUuid}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - }).then((res) => res.json()); -} +import { preventBeforeUnload, getFormattedDate } from "../../utils/utils"; // TODO: 일기 데이터 수정 API 연결 function DiaryUpdateModal(props) { @@ -34,7 +24,7 @@ function DiaryUpdateModal(props) { uuid: diaryState.diaryUuid, title: "", content: "", - date: "2023-11-19", + date: "", point: diaryState.diaryPoint, tags: [], shapeUuid: diaryState.diaryList.find( @@ -42,30 +32,19 @@ function DiaryUpdateModal(props) { ).shapeUuid, }); - async function updateDiaryFn(data) { - return fetch("http://223.130.129.145:3005/diaries", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${data.accessToken}`, - }, - body: JSON.stringify(data.diaryData), - }).then(() => { - refetch(); - setDiaryState((prev) => ({ - ...prev, - isLoading: true, - })); - }); - } - - useEffect(() => { - window.addEventListener("beforeunload", preventBeforeUnload); + const { + mutate: updateDiary, + // isLoading: diaryIsLoading, + // isError: diaryIsError, + } = useMutation(updateDiaryFn); - return () => { - window.removeEventListener("beforeunload", preventBeforeUnload); - }; - }, []); + const { + data: originData, + isLoading, + isError, + } = useQuery("diary", () => + getDiary(userState.accessToken, diaryState.diaryUuid), + ); const closeModal = () => { window.history.back(); @@ -92,27 +71,49 @@ function DiaryUpdateModal(props) { setDiaryData({ ...diaryData, tags: diaryData.tags.slice(0, -1) }); }; - const { - mutate: updateDiary, - // isLoading: diaryIsLoading, - // isError: diaryIsError, - } = useMutation(updateDiaryFn); + async function updateDiaryFn(data) { + const diaryData = { + uuid: data.diaryData.uuid, + title: data.diaryData.title, + content: data.diaryData.content, + date: getFormattedDate(data.diaryData.date), + point: data.diaryData.point, + tags: data.diaryData.tags, + shapeUuid: data.diaryData.shapeUuid, + }; - const { - data: originData, - isLoading, - isError, - } = useQuery("diary", () => - getDiary(userState.accessToken, diaryState.diaryUuid), - ); + return fetch("http://223.130.129.145:3005/diaries", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${data.accessToken}`, + }, + body: JSON.stringify(diaryData), + }).then(() => { + refetch(); + setDiaryState((prev) => ({ + ...prev, + isLoading: true, + })); + }); + } + async function getDiary(accessToken, diaryUuid) { + return fetch(`http://223.130.129.145:3005/diaries/${diaryUuid}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }).then((res) => res.json()); + } - useEffect(() => { + useLayoutEffect(() => { if (originData) { setDiaryData({ ...diaryData, title: originData.title, content: originData.content, - date: originData.date, + date: new Date(originData.date), tags: originData.tags, }); titleRef.current && (titleRef.current.value = originData.title); @@ -120,32 +121,31 @@ function DiaryUpdateModal(props) { } }, [originData]); + useEffect(() => { + window.addEventListener("beforeunload", preventBeforeUnload); + + return () => { + window.removeEventListener("beforeunload", preventBeforeUnload); + }; + }, []); + if (isLoading) return ( - + Loading... ); if (isError) return ( - + 에러 발생 ); return ( - - - 바뀐 별의 이야기를 적어주세요. - - {new Date().toLocaleDateString("ko-KR", { - year: "numeric", - month: "long", - day: "numeric", - })} - - + + props.width || "2.5rem"}; height: 2.5rem; - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); border-radius: 2rem; z-index: 1001; @@ -346,8 +346,7 @@ const ModalSideButton = styled.div` cursor: pointer; &:hover { - background-color: rgba(255, 255, 255, 0.5); - transition: 0.25s; + background-color: rgba(255, 255, 255, 0.3); } `; @@ -355,10 +354,6 @@ const DiaryModalTitle = styled.h1` font-size: 1.5rem; `; -const DiaryModalDate = styled.div` - color: rgba(0, 0, 0, 0.55); -`; - const DiaryModalInputBox = styled.input` width: 100%; height: 3rem; @@ -442,7 +437,7 @@ const DiaryModalTagBox = styled.div` padding: 0.5rem 1rem; border-radius: 1.5rem; border: 1px solid #ffffff; - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); flex-shrink: 0; diff --git a/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js b/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js new file mode 100644 index 0000000..38eee75 --- /dev/null +++ b/FE/src/components/DiaryModal/EmotionIndicator/DiaryEmotionIndicator.js @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import EmotionPicket from "./EmotionPicket"; +import indicatorArrowIcon from "../../../assets/indicator-arrow.svg"; + +function DiaryEmotionIndicator({ emotion, width, text }) { + const [isHover, setIsHover] = useState(""); + return ( + + + setIsHover("positive")} + onMouseLeave={() => setIsHover("")} + > + {isHover === "positive" ? ( + + ) : null} + + + arrow + + setIsHover("neutral")} + onMouseLeave={() => setIsHover("")} + > + {isHover === "neutral" ? ( + + ) : null} + + + arrow + + setIsHover("negative")} + onMouseLeave={() => setIsHover("")} + > + {isHover === "negative" ? ( + + ) : null} + + + {text === true ? ( + + 긍정 {emotion.positive.toFixed(1)}% + 중립 {emotion.neutral.toFixed(1)}% + 부정 {emotion.negative.toFixed(1)}% + + ) : null} + + ); +} + +const EmotionIndicatorWrapper = styled.div` + width: 70%; + display: flex; + align-items: center; + gap: 1.5rem; +`; + +const EmotionIndicatorBar = styled.div` + width: ${(props) => props.width || "20rem"}; + height: 1rem; + display: flex; + align-items: center; + justify-content: space-between; +`; + +const EmotionIndicator = styled.div` + width: ${(props) => props.$ratio}; + height: 100%; + background-color: ${(props) => props.color}; + display: flex; + justify-content: center; +`; + +const EmotionIndicatorArrow = styled.div` + display: flex; + justify-content: center; + width: 0; + height: 4rem; +`; + +const EmotionTextWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; +`; + +const EmotionText = styled.div` + font-size: 0.9rem; +`; + +export default DiaryEmotionIndicator; diff --git a/FE/src/components/DiaryModal/EmotionIndicator/EmotionPicket.js b/FE/src/components/DiaryModal/EmotionIndicator/EmotionPicket.js new file mode 100644 index 0000000..4595d62 --- /dev/null +++ b/FE/src/components/DiaryModal/EmotionIndicator/EmotionPicket.js @@ -0,0 +1,36 @@ +import React from "react"; +import styled from "styled-components"; +import picket from "../../../assets/picket.svg"; + +function EmotionPicket({ percent }) { + return ( + + {percent.toFixed(1)}% + + ); +} + +const EmotionPicketWrapper = styled.div` + width: 5rem; + height: 3.3rem; + position: float; + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 1rem; + color: #ffffff; +`; + +const Picket = styled.div` + width: 100%; + height: 3rem; + background-image: url(${picket}); + background-position: center; + background-repeat: no-repeat; + display: flex; + justify-content: center; + align-items: flex-end; + line-height: 2.3rem; +`; + +export default EmotionPicket; diff --git a/FE/src/components/SideBar/SideBar.js b/FE/src/components/SideBar/SideBar.js index 3ccb2fb..be461a5 100644 --- a/FE/src/components/SideBar/SideBar.js +++ b/FE/src/components/SideBar/SideBar.js @@ -31,6 +31,7 @@ function SideBar() { ...prev, isRead: false, isList: false, + isAnalysis: false, }; }); }} @@ -51,6 +52,7 @@ function SideBar() { isRead: false, isUpdate: false, isList: true, + isAnalysis: false, }, "", "", @@ -61,13 +63,45 @@ function SideBar() { isRead: false, isUpdate: false, isList: true, + isAnalysis: false, }; }); }} > 일기 목록 - 일기 분석 + { + setHeaderState((prev) => ({ + ...prev, + isSideBar: false, + })); + setDiaryState((prev) => { + window.history.pushState( + { + ...prev, + isCreate: false, + isRead: false, + isUpdate: false, + isList: false, + isAnalysis: true, + }, + "", + "", + ); + return { + ...prev, + isCreate: false, + isRead: false, + isUpdate: false, + isList: false, + isAnalysis: true, + }; + }); + }} + > + 일기 분석 + 환경 설정 별숲 상점 diff --git a/FE/src/pages/HomePage.js b/FE/src/pages/HomePage.js index 1419921..a2de8df 100644 --- a/FE/src/pages/HomePage.js +++ b/FE/src/pages/HomePage.js @@ -2,7 +2,6 @@ import React from "react"; import styled from "styled-components"; import { useRecoilValue } from "recoil"; import headerAtom from "../atoms/headerAtom"; - import homeBackground from "../assets/homeBackground.png"; import LoginModal from "../components/LoginModal/LoginModal"; import SignUpModal from "../components/SignUpModal/SignUpModal"; diff --git a/FE/src/pages/MainPage.js b/FE/src/pages/MainPage.js index 68f2439..c45f2c0 100644 --- a/FE/src/pages/MainPage.js +++ b/FE/src/pages/MainPage.js @@ -10,10 +10,11 @@ import userAtom from "../atoms/userAtom"; import DiaryCreateModal from "../components/DiaryModal/DiaryCreateModal"; import DiaryReadModal from "../components/DiaryModal/DiaryReadModal"; import DiaryListModal from "../components/DiaryModal/DiaryListModal"; +import DiaryAnalysisModal from "../components/DiaryModal/DiaryAnalysisModal"; import DiaryUpdateModal from "../components/DiaryModal/DiaryUpdateModal"; import DiaryLoadingModal from "../components/DiaryModal/DiaryLoadingModal"; import StarPage from "./StarPage"; -import preventBeforeUnload from "../utils/utils"; +import { preventBeforeUnload } from "../utils/utils"; function MainPage() { const [diaryState, setDiaryState] = useRecoilState(diaryAtom); @@ -23,7 +24,7 @@ function MainPage() { const { refetch } = useQuery( ["diaryList", userState.accessToken], - () => { + async () => { return fetch("http://223.130.129.145:3005/diaries", { method: "GET", headers: { @@ -140,6 +141,7 @@ function MainPage() { {diaryState.isRead ? : null} {diaryState.isUpdate ? : null} {diaryState.isList ? : null} + {diaryState.isAnalysis ? : null} {diaryState.isLoading ? : null} ) : null} diff --git a/FE/src/pages/StarPage.js b/FE/src/pages/StarPage.js index dc9cde2..effec0f 100644 --- a/FE/src/pages/StarPage.js +++ b/FE/src/pages/StarPage.js @@ -19,7 +19,7 @@ import arrow from "../assets/arrow.svg"; import paint from "../assets/paint.svg"; function StarPage() { - const setDiaryState = useSetRecoilState(diaryAtom); + const [diaryState, setDiaryState] = useRecoilState(diaryAtom); const [starState, setStarState] = useRecoilState(starAtom); return ( @@ -42,35 +42,37 @@ function StarPage() { - { - setStarState((prev) => ({ - ...prev, - mode: "create", - drag: true, - selected: null, - })); - }} - rightEvent={() => { - setDiaryState((prev) => ({ - ...prev, - isCreate: false, - isRead: false, - isUpdate: false, - isDelete: false, - })); - setStarState((prev) => ({ - ...prev, - mode: "stella", - drag: false, - selected: null, - })); - }} - /> + {!(diaryState.isList || diaryState.isAnalysis) ? ( + { + setStarState((prev) => ({ + ...prev, + mode: "create", + drag: true, + selected: null, + })); + }} + rightEvent={() => { + setDiaryState((prev) => ({ + ...prev, + isCreate: false, + isRead: false, + isUpdate: false, + isDelete: false, + })); + setStarState((prev) => ({ + ...prev, + mode: "stella", + drag: false, + selected: null, + })); + }} + /> + ) : null} {starState.mode !== "create" ? ( - props.selected ? "#ffffff80" : "transparent"}; + props.selected ? "rgba(255, 255, 255, 0.2)" : "transparent"}; border-radius: 1.5rem; display: flex; diff --git a/FE/src/styles/Modal/DiaryModalHeader.js b/FE/src/styles/Modal/DiaryModalHeader.js deleted file mode 100644 index 898befb..0000000 --- a/FE/src/styles/Modal/DiaryModalHeader.js +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "styled-components"; - -const DiaryModalHeader = styled.div` - width: 100%; - display: flex; - align-items: flex-end; - justify-content: space-between; -`; - -export default DiaryModalHeader; diff --git a/FE/src/styles/Modal/ModalWrapper.js b/FE/src/styles/Modal/ModalWrapper.js index aff9da1..2034e31 100644 --- a/FE/src/styles/Modal/ModalWrapper.js +++ b/FE/src/styles/Modal/ModalWrapper.js @@ -12,7 +12,7 @@ const ModalWrapper = styled.div` z-index: 1001; width: ${(props) => props.width}; height: ${(props) => props.height}; - background-color: rgba(255, 255, 255, ${(props) => props.opacity || 0.3}); + background-color: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); transform: translate(-50%, -50%); border-radius: ${(props) => props.$borderRadius || "1rem"}; diff --git a/FE/src/styles/Modal/Tag.js b/FE/src/styles/Modal/Tag.js new file mode 100644 index 0000000..9837c7a --- /dev/null +++ b/FE/src/styles/Modal/Tag.js @@ -0,0 +1,17 @@ +import styled from "styled-components"; + +const Tag = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: 1rem; + background-color: rgba(255, 255, 255, 0.2); + box-sizing: border-box; + color: #ffffff; + outline: none; + white-space: nowrap; + font-size: 1rem; +`; + +export default Tag; diff --git a/FE/src/utils/utils.js b/FE/src/utils/utils.js index a4dd834..dc2e9fd 100644 --- a/FE/src/utils/utils.js +++ b/FE/src/utils/utils.js @@ -3,4 +3,13 @@ const preventBeforeUnload = (e) => { e.returnValue = ""; }; -export default preventBeforeUnload; +const getFormattedDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + const formattedDate = `${year}-${month}-${day}`; + return formattedDate; +}; + +export { preventBeforeUnload, getFormattedDate };