From 8be58f1d0c26eed1734f3fc661012c7a325999f0 Mon Sep 17 00:00:00 2001 From: qkdflrgs Date: Fri, 18 Aug 2023 13:47:05 +0900 Subject: [PATCH] =?UTF-8?q?feat/#215:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/pages/LabelsPage.tsx | 208 +++++++----- FE/src/pages/MainPage.tsx | 658 +++++++++++++++++++++++++++++------- 2 files changed, 661 insertions(+), 205 deletions(-) diff --git a/FE/src/pages/LabelsPage.tsx b/FE/src/pages/LabelsPage.tsx index f564909e3..10637fa39 100644 --- a/FE/src/pages/LabelsPage.tsx +++ b/FE/src/pages/LabelsPage.tsx @@ -7,15 +7,25 @@ import LabelList from "../components/LabelList/LabelList"; import Alert from "../components/Alert/Alert"; import EditLabel from "../components/LabelList/EditLabel"; import { generateRandomHex } from "../utils/generateRandomHex"; +import LoadingIndicator from "../components/LoadingIndicator/LoadingIndicator"; +import Header from "../components/Header/Header"; +import { useAuth } from "../context/AuthContext"; -export default function LabelsPage() { +type Props = { + toggleTheme(): void; +}; + +export default function LabelsPage({ toggleTheme }: Props) { const navigate = useNavigate(); + const { profile } = useAuth(); const [data, setData] = useState(); const [openDeleteAlert, setOpenDeleteAlert] = useState(false); const [openAdd, setOpenAdd] = useState(false); const [addLabelName, setAddLabelName] = useState(""); const [addLabelDescription, setAddLabelDescription] = useState(""); - const [addLabelColor, setAddLabelColor] = useState("#FFFFFF"); + const [backgroundColor, setBackgroundColor] = useState("#FFFFFF"); + const [labelTextColor, setLabelTextColor] = useState("#14142B"); + const goLabelsPage = () => { navigate("/labels"); }; @@ -24,11 +34,11 @@ export default function LabelsPage() { navigate("/milestones/isOpen=true"); }; - const openDelete = () => { + const showDeleteAlert = () => { setOpenDeleteAlert(true); }; - const closeDelete = () => { + const cancelDeleteLabel = () => { setOpenDeleteAlert(false); }; @@ -40,6 +50,10 @@ export default function LabelsPage() { setOpenAdd(false); }; + const changeTextColor = (color: string) => { + setLabelTextColor(color); + }; + const handleNameInputChange = (e: React.ChangeEvent) => { setAddLabelName(e.target.value); }; @@ -49,24 +63,26 @@ export default function LabelsPage() { }; const handleColorInputChange = (e: React.ChangeEvent) => { - setAddLabelColor(e.target.value); + setBackgroundColor(e.target.value); }; const randomColor = () => { - setAddLabelColor(generateRandomHex()); + setBackgroundColor(generateRandomHex()); }; const createLabel = async () => { const URL = "http://3.34.141.196/api/labels"; + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + headers.append("Content-Type", "application/json"); + const data = { title: addLabelName, description: addLabelDescription, - color: addLabelColor, - }; - - const headers = { - "Content-Type": "application/json", + backgroundColor: backgroundColor, + textColor: labelTextColor, }; try { @@ -90,7 +106,12 @@ export default function LabelsPage() { useEffect(() => { const fetchData = async () => { try { - const response = await fetch("http://3.34.141.196/api/labels"); + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + const response = await fetch("http://3.34.141.196/api/labels", { + headers, + }); if (!response.ok) { throw new Error("데이터를 가져오는 데 실패했습니다."); } @@ -105,93 +126,105 @@ export default function LabelsPage() { }, []); return ( -
- {!data &&
} - {data && ( - - - -
+ )} + + )} + + ); } const Main = styled.div` width: 1280px; + height: 100%; `; -const Wrapper = styled.div` +const Container = styled.div` width: 100%; padding: 32px 0px; display: flex; @@ -286,15 +319,10 @@ const EmptyContent = styled.span` color: ${({ theme }) => theme.colorSystem.neutral.text.weak}; `; -const Dim = styled.div` - position: absolute; - top: 0px; - left: 0px; +const LoadingPage = styled.div` display: flex; justify-content: center; align-items: center; width: 100%; - height: 100%; - z-index: 1000; - background-color: rgba(16, 20, 26, 0.4); + height: 700px; `; diff --git a/FE/src/pages/MainPage.tsx b/FE/src/pages/MainPage.tsx index eb6916322..66e2ba1e5 100644 --- a/FE/src/pages/MainPage.tsx +++ b/FE/src/pages/MainPage.tsx @@ -1,21 +1,56 @@ import { styled } from "styled-components"; import FilterBar from "../components/FilterBar/FilterBar"; import Button from "../components/common/Button/Button"; -import DropdownIndicator from "../components/DropdownIndicator/DropdownIndicator"; import IssueList from "../components/IssueList/IssueList"; import { useNavigate, useParams } from "react-router-dom"; import { useEffect, useState } from "react"; -import { ListDataProps } from "../type"; +import { + AssigneesData, + AuthorsData, + LabelsData, + ListDataProps, + MilestonesData, +} from "../type"; import parseFilter from "../utils/parseFilter"; import parseParam from "../utils/parseParam"; +import LoadingIndicator from "../components/LoadingIndicator/LoadingIndicator"; +import DropdownIndicator from "../components/DropdownIndicator/DropdownIndicator"; +import DropdownItem from "../components/DropdownPanel/DropdownItem"; +import DropdownPanel from "../components/DropdownPanel/DropdownPanel"; +import Header from "../components/Header/Header"; +import { useAuth } from "../context/AuthContext"; -export default function MainPage() { +type Props = { + toggleTheme(): void; +}; + +export default function MainPage({ toggleTheme }: Props) { const navigate = useNavigate(); const { filter } = useParams(); + const { profile } = useAuth(); + const [loading, setLoading] = useState(true); + const [openFilterDropdown, setOpenFilterDropdown] = useState< + "assignees" | "labels" | "milestones" | "authors" | "state" | "close" + >("close"); + const [assigneesData, setAssigneesData] = useState(); + const [labelsData, setLabelsData] = useState(); + const [milestonesData, setMilestonesData] = useState(); + const [authorsData, setAuthorsData] = useState(); const [data, setData] = useState(); + const [selectAll, setSelectAll] = useState(false); + const [selectList, setSelectList] = useState([]); const [filterValue, setFilterValue] = useState( filter ? filter : "isOpen=true", ); + const isSelected = selectList.length !== 0; + + const addSelectList = (id: number) => { + setSelectList((prevList) => [...prevList, id]); + }; + + const removeSelectList = (id: number) => { + setSelectList((prevList) => prevList.filter((item) => item !== id)); + }; const goAddNewIssuePage = () => { navigate("/new"); @@ -45,128 +80,491 @@ export default function MainPage() { setFilterValue(e.target.value); }; + const closeDropdown = () => { + setOpenFilterDropdown("close"); + }; + + const showAssigneesDropdown = () => { + setOpenFilterDropdown("assignees"); + }; + + const showLabelsDropdown = () => { + setOpenFilterDropdown("labels"); + }; + + const showMilestonesDropdown = () => { + setOpenFilterDropdown("milestones"); + }; + + const showAuthorsDropdown = () => { + setOpenFilterDropdown("authors"); + }; + + const showStateDropdown = () => { + setOpenFilterDropdown("state"); + }; + + const toggleSelectAll = () => { + if (!selectAll) { + setSelectAll(true); + setSelectList(data!.issues && data!.issues.map((issue) => issue.id)); + console.log(profile); + return; + } + setSelectAll(false); + setSelectList([]); + }; + + const openSelectedIssues = async () => { + const URL = "http://3.34.141.196/api/issues/open-all"; + + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + headers.append("Content-Type", "application/json"); + + const patchData = { + ids: selectList, + isOpen: true, + }; + + try { + const response = await fetch(URL, { + method: "PATCH", + headers: headers, + body: JSON.stringify(patchData), + }); + + if (response.status === 204) { + window.location.reload(); + } else { + console.log("PATCH 요청에 실패하였습니다. 상태 코드:", response.status); + } + } catch (error) { + console.error("API 호출 오류:", error); + } + }; + + const closeSelectedIssues = async () => { + const URL = "http://3.34.141.196/api/issues/open-all"; + + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + headers.append("Content-Type", "application/json"); + + const patchData = { + ids: selectList, + isOpen: false, + }; + + try { + const response = await fetch(URL, { + method: "PATCH", + headers: headers, + body: JSON.stringify(patchData), + }); + + if (response.status === 204) { + window.location.reload(); + } else { + console.log("PATCH 요청에 실패하였습니다. 상태 코드:", response.status); + } + } catch (error) { + console.error("API 호출 오류:", error); + } + }; + useEffect(() => { + const maxRetryAttempts = 3; + let retryAttempts = 0; + + const fetchData = async () => { + while (retryAttempts < maxRetryAttempts) { + try { + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + const response = await fetch( + filter + ? `http://3.34.141.196/api/issues${parseFilter(filter)}` + : "http://3.34.141.196/api/issues", + { headers }, + ); + + if (!response.ok) { + throw new Error("데이터를 가져오는 데 실패했습니다."); + } + + const jsonData = await response.json(); + setData(jsonData); + console.log(jsonData); + setLoading(false); + return; + } catch (error) { + retryAttempts++; + console.log("에러 발생:", error); + } + } + + setLoading(false); + }; + + fetchData(); + }, [filter]); + + useEffect(() => { + setFilterValue(parseParam(filter!)); + }, [filter]); + + useEffect(() => { + if (selectAll && selectList.length === 0) setSelectAll(false); + if (!selectAll && selectList.length === data?.issues.length) + setSelectAll(true); + }, [selectList]); + + useEffect(() => { + if (openFilterDropdown === "close" || openFilterDropdown === "state") { + return; + } + const headers = new Headers(); + const accessToken = localStorage.getItem("accessToken"); + headers.append("Authorization", `Bearer ${accessToken}`); + + const URL = `http://3.34.141.196/api/issues/${openFilterDropdown}`; const fetchData = async () => { try { - const response = await fetch( - filter - ? `http://3.34.141.196/api/issues${parseFilter(filter)}` - : "http://3.34.141.196/api/issues", - ); + const response = await fetch(URL, { headers }); if (!response.ok) { throw new Error("데이터를 가져오는 데 실패했습니다."); } const jsonData = await response.json(); - setData(jsonData); + openFilterDropdown === "assignees" + ? setAssigneesData(jsonData) + : openFilterDropdown === "labels" + ? setLabelsData(jsonData) + : openFilterDropdown === "milestones" + ? setMilestonesData(jsonData) + : setAuthorsData(jsonData); } catch (error) { console.log("error"); } }; fetchData(); - }, [filter]); - - useEffect(() => { - setFilterValue(parseParam(filter!)); - }, [filter]); + }, [openFilterDropdown]); return ( -
- - - - -
+ )} + + ); } +const Page = styled.div` + display: flex; + justify-content: center; + width: 100%; + height: 100%; +`; + +const LoadingWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 700px; +`; + const Main = styled.div` padding: 32px 0px; display: flex; @@ -246,25 +644,30 @@ const CheckboxWrapper = styled.div` `; const Checkbox = styled.input` - display: none; -`; - -const CheckboxLabel = styled.label` + appearance: none; width: 16px; height: 16px; - cursor: pointer; - padding-left: 23px; - background-repeat: no-repeat; - background-image: url("/icons/checkBoxInitial.svg"); // 체크된 상태 이미지 + background-image: url("/icons/checkBoxInitial.svg"); &:checked { - background-image: url("/icons/checkBoxActive.svg"); // 체크된 상태 이미지 + border-color: transparent; + background-image: url("/icons/checkBoxActive.svg"); + background-size: 100% 100%; + background-repeat: no-repeat; + } + &:indeterminate { + border-color: transparent; + background-image: url("/icons/checkBoxDisable.svg"); + background-size: 100% 100%; + background-repeat: no-repeat; } `; const TapWrapper = styled.div` + position: relative; width: 100%; display: flex; justify-content: space-between; + align-items: center; `; const IssueTap = styled.form` @@ -321,3 +724,28 @@ const RemoveFilterWrapper = styled.div` justify-content: left; align-items: center; `; + +const SelectedListCounter = styled.span` + font: ${({ theme }) => theme.font.displayBold16}; + color: ${({ theme }) => theme.colorSystem.neutral.text.default}; +`; + +const AssigneesDropdown = styled.div` + position: relative; +`; + +const LabelsDropdown = styled.div` + position: relative; +`; + +const MilestonesDropdown = styled.div` + position: relative; +`; + +const AuthorsDropdown = styled.div` + position: relative; +`; + +const IssuesStateDropdown = styled.div` + position: relative; +`;