diff --git a/FE/src/App.tsx b/FE/src/App.tsx
index 89087d7b5..815367fcf 100644
--- a/FE/src/App.tsx
+++ b/FE/src/App.tsx
@@ -24,7 +24,10 @@ function App() {
}>
}>
}>
- }>
+ }
+ >
}>
}>
diff --git a/FE/src/components/MilestoneList/EditMilestone.tsx b/FE/src/components/MilestoneList/EditMilestone.tsx
new file mode 100644
index 000000000..09b79364c
--- /dev/null
+++ b/FE/src/components/MilestoneList/EditMilestone.tsx
@@ -0,0 +1,145 @@
+import { styled } from "styled-components";
+import LabelInput from "../common/TextInput/LabelInput";
+import Button from "../common/Button/Button";
+
+type Props = {
+ id?: number;
+ type?: "edit" | "add";
+ title: string;
+ description: string;
+ deadline: string;
+ onChangeTitle(e: React.ChangeEvent): void;
+ onChangeDescription(e: React.ChangeEvent): void;
+ onChangeDeadline(e: React.ChangeEvent): void;
+ cancelEdit(): void;
+ confirmEdit(): void;
+};
+
+export default function EditMilestone({
+ id,
+ type = "edit",
+ title,
+ description,
+ deadline,
+ onChangeTitle,
+ onChangeDescription,
+ onChangeDeadline,
+ cancelEdit,
+ confirmEdit,
+}: Props) {
+ const confirmEditMilestone = async () => {
+ const URL = `http://3.34.141.196/api/milestones/${id}`;
+
+ const putData = {
+ title: title,
+ description: description,
+ deadline: deadline,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ try {
+ const response = await fetch(URL, {
+ method: "PUT",
+ headers: headers,
+ body: JSON.stringify(putData),
+ });
+
+ if (response.status === 204) {
+ confirmEdit();
+ } else {
+ console.log("PUT 요청에 실패하였습니다. 상태 코드:", response.status);
+ }
+ } catch (error) {
+ console.error("API 호출 오류:", error);
+ }
+ };
+
+ return (
+
+
+ {type === "edit" ? "마일스톤 편집" : "새로운 마일스톤 추가"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const Container = styled.li<{ $type: "edit" | "add" }>`
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding: 36px 32px 32px 32px;
+ width: 100%;
+ height: 284px;
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ border: ${({ $type, theme }) =>
+ $type === "add"
+ ? `${theme.border.default} ${theme.colorSystem.neutral.border.default}`
+ : ""};
+ border-radius: ${({ theme }) => theme.radius.large};
+`;
+
+const Title = styled.h1`
+ width: 100%;
+ font: ${({ theme }) => theme.font.displayBold20};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.strong};
+`;
+
+const InputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: end;
+ gap: 16px;
+ width: 100%;
+`;
+
+const UpperWrapper = styled.div`
+ display: flex;
+ gap: 16px;
+`;
diff --git a/FE/src/components/MilestoneList/MilestoneList.tsx b/FE/src/components/MilestoneList/MilestoneList.tsx
new file mode 100644
index 000000000..c84486cfe
--- /dev/null
+++ b/FE/src/components/MilestoneList/MilestoneList.tsx
@@ -0,0 +1,300 @@
+import { styled } from "styled-components";
+import ProgressIndicator from "../ProgressIndicator/ProgressIndicator";
+import Button from "../common/Button/Button";
+import { MilestoneData } from "../../type";
+import { useState } from "react";
+import EditMilestone from "./EditMilestone";
+
+export default function MilestoneList({
+ id,
+ title,
+ progress,
+ deadline,
+ isOpen,
+ description,
+ openIssueCount,
+ closeIssueCount,
+}: MilestoneData) {
+ const [isEdit, setIsEdit] = useState(false);
+ const [milestoneTitle, setMilestoneTitle] = useState(title);
+ const [milestoneDeadline, setMilestoneDeadline] = useState(deadline);
+ const [milestoneDescription, setMilestoneDescription] =
+ useState(description);
+
+ const editMilestone = () => {
+ setIsEdit(true);
+ };
+
+ const confirmEdit = () => {
+ setIsEdit(false);
+ };
+
+ const cancelEditMilestone = () => {
+ setMilestoneTitle(title);
+ setMilestoneDeadline(deadline);
+ setMilestoneDescription(description);
+ setIsEdit(false);
+ };
+
+ const handleTitleInputChange = (e: React.ChangeEvent) => {
+ setMilestoneTitle(e.target.value);
+ };
+
+ const handleDeadlineInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setMilestoneDeadline(e.target.value);
+ };
+
+ const handleDescriptionInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setMilestoneDescription(e.target.value);
+ };
+
+ const closeMilestone = async () => {
+ const URL = `http://3.34.141.196/api/milestones/${id}/open`;
+
+ const patchData = {
+ isOpen: false,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ 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 openMilestone = async () => {
+ const URL = `http://3.34.141.196/api/milestones/${id}/open`;
+
+ const patchData = {
+ isOpen: true,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ 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 deleteMilestone = async () => {
+ const URL = `http://3.34.141.196/api/milestones/${id}`; // DELETE 요청을 보낼 리소스 URL로 변경
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ try {
+ const response = await fetch(URL, {
+ method: "DELETE",
+ headers: headers,
+ });
+
+ if (response.status === 204) {
+ window.location.reload();
+ } else {
+ console.log(
+ "DELETE 요청에 실패하였습니다. 상태 코드:",
+ response.status,
+ );
+ }
+ } catch (error) {
+ console.error("API 호출 오류:", error);
+ }
+ };
+
+ return (
+
+ {!isEdit && (
+
+
+
+
+
+ {milestoneTitle}
+
+
+
+ {milestoneDeadline}
+
+
+
+ {milestoneDescription}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ {isEdit && (
+
+ )}
+
+ );
+}
+
+const Container = styled.li`
+ position: relative;
+ width: 100%;
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 1280px;
+ height: 1px;
+ background-color: ${({ theme }) =>
+ theme.colorSystem.neutral.border.default};
+ }
+`;
+
+const Default = styled.div`
+ padding: 16px 32px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 104px;
+`;
+
+const TitleInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 932px;
+ height: 56px;
+`;
+
+const TitleWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ width: 932px;
+`;
+
+const Title = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 24px;
+`;
+
+const Name = styled.span`
+ font: ${({ theme }) => theme.font.availableMedium16};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.strong};
+`;
+
+const MilestoneIcon = styled.img`
+ width: 16px;
+ height: 16px;
+ filter: ${({ theme }) => theme.filter.brand.text.weak};
+`;
+
+const DescriptionWrapper = styled.div``;
+
+const DeadlineWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 24px;
+`;
+
+const DeadlineIcon = styled.img`
+ width: 16px;
+ height: 16px;
+`;
+
+const DeadlineDate = styled.span`
+ font: ${({ theme }) => theme.font.displayMedium12};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.weak};
+`;
+
+const Info = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+ gap: 8px;
+ width: 244px;
+`;
+
+const Description = styled.p`
+ font: ${({ theme }) => theme.font.displayMedium16};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.weak};
+`;
+
+const ButtonTap = styled.div`
+ display: flex;
+ justify-content: end;
+ gap: 24px;
+ width: 100%;
+ height: 32px;
+`;
diff --git a/FE/src/pages/MilestonesPage.tsx b/FE/src/pages/MilestonesPage.tsx
index 5b1d767c3..f6861183b 100644
--- a/FE/src/pages/MilestonesPage.tsx
+++ b/FE/src/pages/MilestonesPage.tsx
@@ -1,24 +1,117 @@
import { styled } from "styled-components";
import Button from "../components/common/Button/Button";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { FetchedMilestone } from "../type";
+import MilestoneList from "../components/MilestoneList/MilestoneList";
+import EditMilestone from "../components/MilestoneList/EditMilestone";
+import parseFilter from "../utils/parseFilter";
export default function MilestonesPage() {
const navigate = useNavigate();
+ const { state } = useParams();
+ const [data, setData] = useState(null);
+ const [isAdd, setIsAdd] = useState(false);
+ const [addTitle, setAddTitle] = useState("");
+ const [addDescription, setAddDescription] = useState("");
+ const [addDeadline, setAddDeadline] = useState("");
+
const goLabelsPage = () => {
navigate("/labels");
};
const goMilestonesPage = () => {
- navigate("/milestones");
+ navigate("/milestones/isOpen=true");
+ window.location.reload();
+ };
+
+ const goCloseMilestonesPage = () => {
+ navigate("/milestones/isOpen=false");
+ window.location.reload();
+ };
+
+ const openAdd = () => {
+ setIsAdd(true);
+ };
+
+ const cancelAdd = () => {
+ setIsAdd(false);
+ };
+
+ const handleTitleInputChange = (e: React.ChangeEvent) => {
+ setAddTitle(e.target.value);
+ };
+
+ const handleDeadlineInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setAddDeadline(e.target.value);
+ };
+
+ const handleDescriptionInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setAddDescription(e.target.value);
+ };
+
+ const createMilestone = async () => {
+ const URL = "http://3.34.141.196/api/milestones";
+
+ const data = {
+ title: addTitle,
+ description: addDescription,
+ deadline: addDeadline,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ try {
+ const response = await fetch(URL, {
+ method: "POST",
+ headers: headers,
+ body: JSON.stringify(data),
+ });
+
+ if (response.status === 201) {
+ cancelAdd();
+ navigate("/milestones/isOpen=true");
+ window.location.reload();
+ } else {
+ console.log("이슈 생성에 실패하였습니다. 상태 코드:", response.status);
+ }
+ } catch (error) {
+ console.error("오류 발생:", error);
+ }
};
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const response = await fetch(
+ `http://3.34.141.196/api/milestones${parseFilter(state)}`,
+ );
+ if (!response.ok) {
+ throw new Error("데이터를 가져오는 데 실패했습니다.");
+ }
+ const jsonData = await response.json();
+ setData(jsonData);
+ } catch (error) {
+ console.log("error");
+ }
+ };
+
+ fetchData();
+ }, []);
+
return (
-
+ {isAdd && (
+
+ )}
+
+
+
+
+
+
+
+ {data?.milestones?.map((milestone) => (
+
+ ))}
+ {data?.milestones?.length === 0 && (
+
+ 생성된 마일스톤이 없습니다.
+
+ )}
+
);
}
@@ -42,6 +191,7 @@ export default function MilestonesPage() {
const Main = styled.div`
padding: 32px 0px;
display: flex;
+ gap: 24px;
flex-direction: column;
width: 1280px;
`;
@@ -88,3 +238,50 @@ const TapButtonWrapper = styled.div`
background-color: ${({ theme }) => theme.colorSystem.neutral.surface.bold};
}
`;
+
+const LabelsTable = styled.ul`
+ width: 100%;
+ border: ${({ theme }) =>
+ `${theme.border.default} ${theme.colorSystem.neutral.border.default}`};
+ border-radius: ${({ theme }) => theme.radius.large};
+ > li:last-child {
+ border-bottom-left-radius: inherit;
+ border-bottom-right-radius: inherit;
+ }
+`;
+const TableHeader = styled.li`
+ width: 100%;
+ padding: 20px 32px;
+ font: ${({ theme }) => theme.font.displayBold16};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.default};
+`;
+
+const SelectButtonTap = styled.div`
+ display: flex;
+ gap: 24px;
+`;
+
+const EmptyList = styled.li`
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 96px;
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 1280px;
+ height: 1px;
+ background-color: ${({ theme }) =>
+ theme.colorSystem.neutral.border.default};
+ }
+`;
+
+const EmptyContent = styled.span`
+ font: ${({ theme }) => theme.font.displayMedium16};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.weak};
+`;
diff --git a/FE/src/type.tsx b/FE/src/type.tsx
index f01321627..f41c7aaf1 100644
--- a/FE/src/type.tsx
+++ b/FE/src/type.tsx
@@ -60,6 +60,10 @@ export type AssigneesProps = {
};
export type FetchedLabels = {
+ metadata: {
+ totalLabelCount: number;
+ totalMilestoneCount: number;
+ };
labels: Label[] | null;
};
@@ -86,3 +90,21 @@ export type FetchedDetail = {
milestone: Milestone | null;
comments: Comment[] | null;
};
+
+export type FetchedMilestone = {
+ metadata: {
+ totalLabelCount: number;
+ totalMilestoneCount: number;
+ openMilestoneCount: number;
+ closeMilestoneCount: number;
+ };
+ milestones: MilestoneData[] | null;
+};
+
+export interface MilestoneData extends Milestone {
+ deadline: string;
+ isOpen: boolean;
+ description: string;
+ openIssueCount: number;
+ closeIssueCount: number;
+}