diff --git a/FE/package-lock.json b/FE/package-lock.json index a19a33c..d4a950d 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -13,7 +13,9 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "date-fns": "^2.30.0", "react": "^18.2.0", + "react-datepicker": "^4.24.0", "react-dom": "^18.2.0", "react-query": "^3.39.3", "react-scripts": "5.0.1", @@ -3354,6 +3356,15 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@react-spring/animated": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", @@ -6440,6 +6451,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -7178,6 +7194,21 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -15602,6 +15633,23 @@ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-datepicker": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", + "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -15736,6 +15784,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15750,6 +15803,33 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-query": { "version": "3.39.3", "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", @@ -18289,6 +18369,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/FE/package.json b/FE/package.json index 9c2f622..22e94f7 100644 --- a/FE/package.json +++ b/FE/package.json @@ -8,7 +8,9 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "date-fns": "^2.30.0", "react": "^18.2.0", + "react-datepicker": "^4.24.0", "react-dom": "^18.2.0", "react-query": "^3.39.3", "react-scripts": "5.0.1", diff --git a/FE/src/assets/search.svg b/FE/src/assets/search.svg new file mode 100644 index 0000000..70ad761 --- /dev/null +++ b/FE/src/assets/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/FE/src/components/DiaryModal/DiaryCreateModal.js b/FE/src/components/DiaryModal/DiaryCreateModal.js index 7656e0e..15724e4 100644 --- a/FE/src/components/DiaryModal/DiaryCreateModal.js +++ b/FE/src/components/DiaryModal/DiaryCreateModal.js @@ -131,12 +131,10 @@ function DiaryCreateModal(props) { fontSize='1rem' placeholder='태그를 입력해주세요.' onBlur={(e) => addTag(e)} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === "Enter") { addTag(e); } - }} - onKeyDown={(e) => { if (e.key === "Backspace" && e.target.value.length === 0) { deleteLastTag(); } diff --git a/FE/src/components/DiaryModal/DiaryListModal.js b/FE/src/components/DiaryModal/DiaryListModal.js index 397d90e..78127b1 100644 --- a/FE/src/components/DiaryModal/DiaryListModal.js +++ b/FE/src/components/DiaryModal/DiaryListModal.js @@ -2,17 +2,38 @@ import React, { useEffect, useLayoutEffect } from "react"; import styled from "styled-components"; -import { useRecoilState } from "recoil"; +import { useRecoilState, useRecoilValue } from "recoil"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { ko } from "date-fns/esm/locale"; import diaryAtom from "../../atoms/diaryAtom"; +import shapeAtom from "../../atoms/shapeAtom"; import zoomIn from "../../assets/zoomIn.svg"; +import search from "../../assets/search.svg"; function DiaryListModal() { const [selectedDiary, setSelectedDiary] = React.useState(null); const [diaryState, setDiaryState] = useRecoilState(diaryAtom); + const shapeState = useRecoilValue(shapeAtom); + const [filterState, setFilterState] = React.useState({ + date: { + start: null, + end: null, + }, + emotion: { + positive: false, + neutral: false, + negative: false, + }, + shape: [], + tag: [], + }); + const [filteredDiaryList, setFilteredDiaryList] = React.useState([]); useLayoutEffect(() => { if (diaryState.diaryList) { setSelectedDiary(diaryState.diaryList[0]); + setFilteredDiaryList(diaryState.diaryList); } }, [diaryState.diaryList]); @@ -26,24 +47,251 @@ function DiaryListModal() { } }, [selectedDiary]); + useEffect(() => { + if (diaryState.diaryList) { + const filteredList = diaryState.diaryList + .filter((diary) => { + if (filterState.date.start && filterState.date.end) { + if ( + new Date(diary.date) >= filterState.date.start && + new Date(diary.date) <= filterState.date.end + ) { + return true; + } + } else if (filterState.date.start) { + if (new Date(diary.date) >= filterState.date.start) { + return true; + } + } else if (filterState.date.end) { + if (new Date(diary.date) <= filterState.date.end) { + return true; + } + } else { + return true; + } + return false; + }) + .filter((diary) => { + if ( + !filterState.emotion.positive && + !filterState.emotion.neutral && + !filterState.emotion.negative + ) { + return true; + } + if (filterState.emotion.positive) { + if (diary.emotion.sentiment === "positive") { + return true; + } + } + if (filterState.emotion.neutral) { + if (diary.emotion.sentiment === "neutral") { + return true; + } + } + if (filterState.emotion.negative) { + if (diary.emotion.sentiment === "negative") { + return true; + } + } + return false; + }) + .filter((diary) => { + if (filterState.shape.length > 0) { + if (filterState.shape.includes(diary.shapeUuid)) { + return true; + } + return false; + } + return true; + }) + .filter((diary) => { + if (filterState.tag.length > 0) { + if (filterState.tag.every((tag) => diary.tags.includes(tag))) { + return true; + } + return false; + } + return true; + }); + setFilteredDiaryList([...filteredList]); + } + }, [ + filterState.date, + filterState.emotion, + filterState.shape, + filterState.tag, + ]); + return ( - + 날짜 - 필터 + + { + setFilterState((prev) => ({ + ...prev, + date: { + ...prev.date, + start: date, + }, + })); + }} + dateFormat='yyyy-MM-dd' + selectsStart + startDate={filterState.date.start} + endDate={filterState.date.end} + locale={ko} + isClearable={true} + placeholderText='시작 날짜' + /> + ~ + { + setFilterState((prev) => ({ + ...prev, + date: { + ...prev.date, + end: date, + }, + })); + }} + dateFormat='yyyy-MM-dd' + selectsEnd + startDate={filterState.date.start} + endDate={filterState.date.end} + minDate={filterState.date.start} + locale={ko} + isClearable={true} + placeholderText='종료 날짜' + /> + 감정 - 필터 + + { + setFilterState((prev) => ({ + ...prev, + emotion: { + ...prev.emotion, + positive: !prev.emotion.positive, + }, + })); + }} + > + 긍정 + + { + setFilterState((prev) => ({ + ...prev, + emotion: { + ...prev.emotion, + neutral: !prev.emotion.neutral, + }, + })); + }} + > + 중립 + + { + setFilterState((prev) => ({ + ...prev, + emotion: { + ...prev.emotion, + negative: !prev.emotion.negative, + }, + })); + }} + > + 부정 + + 모양 - 필터 + + + {shapeState?.map((shape) => ( + { + setFilterState((prev) => { + const shapeIndex = prev.shape.indexOf(shape.uuid); + if (shapeIndex !== -1) { + const updatedShape = [...prev.shape]; + updatedShape.splice(shapeIndex, 1); + return { + ...prev, + shape: updatedShape, + }; + } else { + return { + ...prev, + shape: [...prev.shape, shape.uuid], + }; + } + }); + }} + selected={filterState.shape.includes(shape.uuid)} + > +
+ + ))} + + 태그 - 필터 + + + + search + + { + if (e.key === "Enter") { + if (!filterState.tag.includes(e.target.value)) { + setFilterState((prev) => ({ + ...prev, + tag: [...prev.tag, e.target.value], + })); + } + } + }} + /> + + + {filterState.tag.map((tag) => ( + { + setFilterState((prev) => ({ + ...prev, + tag: prev.tag.filter((item) => item !== tag), + })); + }} + > + {tag} + + ))} + + @@ -53,7 +301,7 @@ function DiaryListModal() { e.target.focus(); }} > - {diaryState.diaryList?.map((diary) => ( + {filteredDiaryList.map((diary) => ( { @@ -121,6 +369,7 @@ const DiaryListModalItem = styled.div` display: flex; flex-direction: column; align-items: center; + justify-content: ${(props) => props.justifyContent || "flex-start"}; font-size: 1.3rem; color: #ffffff; @@ -134,6 +383,9 @@ const DiaryListModalItem = styled.div` opacity: 1; } } + + overflow: hidden; + overflow-y: auto; `; const DiaryListModalFilterWrapper = styled.div` @@ -143,16 +395,172 @@ const DiaryListModalFilterWrapper = styled.div` flex-direction: column; align-items: center; + margin: 1rem 0; + gap: 0.5rem; + font-size: 1.1rem; `; const DiaryListModalFilterContent = styled.div` width: 100%; - height: 4.5rem; + height: ${(props) => props.height || "7rem"}; + padding: 0 1rem; + + display: flex; + flex-direction: ${(props) => props.flexDirection || "row"}; + justify-content: space-evenly; + align-items: center; +`; + +const FilterDateInput = styled.input` + width: 40%; + height: 3rem; + + border: none; + border-radius: 0.5rem; + + font-family: "Pretendard-Medium"; + font-size: 1.1rem; + text-align: center; +`; + +const FilterEmotionButton = styled.button` + width: 25%; + height: 3rem; + + border: none; + border-radius: 0.5rem; + + font-family: "Pretendard-Medium"; + font-size: 1.1rem; + text-align: center; + + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + } + + border: ${(props) => + props.selected ? `3px solid ${props.borderColor}` : "none"}; + border-radius: 0.5rem; +`; + +const ShapeWrapper = styled.div` + width: 90%; + height: 10rem; + + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + + background-color: #3b455e; + border-radius: 0.5rem; + + overflow: auto; + overflow-x: hidden; +`; + +const ShapeSelectBoxItem = styled.div` + width: 4rem; + + margin: 0.5rem; + + cursor: pointer; + + &:hover { + transform: scale(1.2); + transition: transform 0.25s; + } + + border: ${(props) => + props.selected ? "1px solid #ffffff" : "1px solid transparent"}; + border-radius: 0.5rem; +`; + +const FilterTagInputWrapper = styled.div` + width: 88%; + height: 3rem; + + border: none; + border-radius: 1.5rem; + + background-color: rgba(255, 255, 255, 0.6); + + font-family: "Pretendard-Medium"; + font-size: 1.1rem; + + padding: 0.5rem 1rem; + box-sizing: border-box; + + display: flex; + justify-content: flex-start; + align-items: center; + + margin-bottom: 1rem; +`; + +const FilterTagInputIcon = styled.div` + width: 2rem; + height: 2rem; + + display: flex; + justify-content: center; + align-items: center; +`; + +const FilterTagInput = styled.input` + width: 100%; + height: 3rem; + + border: none; + border-radius: 1.5rem; + + background-color: transparent; + + font-family: "Pretendard-Medium"; + font-size: 1.1rem; + + padding: 0.5rem 1rem; + box-sizing: border-box; + + &:focus { + outline: none; + } +`; + +const FilterTagWrapper = styled.div` + width: 90%; + height: 10rem; + + display: flex; + flex-wrap: wrap; + + background-color: transparent; + border-radius: 0.5rem; + + overflow: auto; + overflow-x: hidden; +`; + +const FilterTagItem = styled.div` + height: 2rem; display: flex; justify-content: center; align-items: center; + + background-color: rgba(255, 255, 255, 0.3); + border-radius: 1rem; + + padding: 0 1rem; + margin: 0.5rem; + + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + } `; const DiaryTitleListHeader = styled.div` @@ -165,7 +573,7 @@ const DiaryTitleListHeader = styled.div` flex-shrink: 0; - font-size: 1.1rem; + font-size: 1.6rem; `; const DiaryTitleListItemWrapper = styled.div`