From e1e59eb913e252ecd9fee83d99d46fca3d6250df Mon Sep 17 00:00:00 2001 From: Jisoo Yoo Date: Fri, 11 Aug 2023 13:04:36 +0900 Subject: [PATCH] =?UTF-8?q?#125=20feat:=20=EB=A7=88=EC=9D=BC=EC=8A=A4?= =?UTF-8?q?=ED=86=A4=20=EC=88=98=EC=A0=95=20&=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/src/api/index.ts | 4 + .../components/Milestone/MilestoneEditor.tsx | 47 +++-- .../MilestonesTable/MilestonesTableItem.tsx | 169 +++++++++++++++--- fe/src/components/common/ProgressBar.tsx | 27 ++- fe/src/mocks/data.ts | 8 +- fe/src/mocks/handlers.ts | 4 + 6 files changed, 208 insertions(+), 51 deletions(-) diff --git a/fe/src/api/index.ts b/fe/src/api/index.ts index 4eecaeed6..70df12ba6 100644 --- a/fe/src/api/index.ts +++ b/fe/src/api/index.ts @@ -171,3 +171,7 @@ export const putMilestoneState = async ( `/milestones/${milestoneId}?state=${state}` ); }; + +export const deleteMilestone = async (milestoneId: number) => { + return await fetcherWithBearer.delete(`/milestones/${milestoneId}`); +}; diff --git a/fe/src/components/Milestone/MilestoneEditor.tsx b/fe/src/components/Milestone/MilestoneEditor.tsx index ef6c19aae..ab925f767 100644 --- a/fe/src/components/Milestone/MilestoneEditor.tsx +++ b/fe/src/components/Milestone/MilestoneEditor.tsx @@ -8,8 +8,8 @@ import { ChangeEvent, FormEvent, useState } from "react"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; -type MilestoneInfo = { - name: string; +export type MilestoneInfo = { + milestoneName: string; dueDate?: string; description?: string; }; @@ -29,42 +29,51 @@ export default function MilestoneEditor({ // TODO: dueDate useInput validate 개선 후 적용 const [newMilestone, setNewMilestone] = useState({ - newName: milestoneInfo?.name || "", - newDueDate: milestoneInfo?.dueDate || "", - newDescription: milestoneInfo?.description || "", + milestoneName: milestoneInfo?.milestoneName || "", + dueDate: milestoneInfo?.dueDate || "", + description: milestoneInfo?.description || "", }); - const isValidDueDate = validateDate(newMilestone.newDueDate); + const isValidDueDate = validateDate(newMilestone.dueDate); const onNameChange = (e: ChangeEvent) => { setNewMilestone((prev) => { - return { ...prev, newName: e.target.value }; + return { ...prev, milestoneName: e.target.value }; }); }; const onDueDateChange = (e: ChangeEvent) => { setNewMilestone((prev) => { - return { ...prev, newDueDate: e.target.value }; + return { ...prev, dueDate: e.target.value }; }); }; const onDescriptionChange = (e: ChangeEvent) => { setNewMilestone((prev) => { - return { ...prev, newDescription: e.target.value }; + return { ...prev, description: e.target.value }; }); }; - const isReadyToSubmit = newMilestone.newName !== (milestoneInfo?.name || ""); + // TODO: 개선 필요 + const isReadyToSubmit = { + add: !!newMilestone.milestoneName, + edit: + !!newMilestone.milestoneName && + (newMilestone.milestoneName !== milestoneInfo?.milestoneName || + newMilestone.dueDate !== milestoneInfo?.dueDate || + newMilestone.description !== milestoneInfo?.description), + }; const onSubmit = async (e: FormEvent) => { e.preventDefault(); + // TODO: 수정된 내용이 있는 영역만 보내기 try { - const { newName, newDueDate, newDescription } = newMilestone; + const { milestoneName, dueDate, description } = newMilestone; const body = { - milestoneName: newName, - description: newDescription, - dueDate: newDueDate, + milestoneName, + description, + dueDate, }; const res = milestoneId @@ -91,24 +100,24 @@ export default function MilestoneEditor({ @@ -126,7 +135,7 @@ export default function MilestoneEditor({ type="submit" variant="container" size="S" - disabled={!isReadyToSubmit}> + disabled={!isReadyToSubmit[variant]}> 완료 완료 diff --git a/fe/src/components/Table/MilestonesTable/MilestonesTableItem.tsx b/fe/src/components/Table/MilestonesTable/MilestonesTableItem.tsx index 827b8ccfb..91681ae80 100644 --- a/fe/src/components/Table/MilestonesTable/MilestonesTableItem.tsx +++ b/fe/src/components/Table/MilestonesTable/MilestonesTableItem.tsx @@ -1,6 +1,18 @@ +import alertIcon from "@assets/icon/alertCircle.svg"; +import archiveIcon from "@assets/icon/archive.svg"; import calendarIcon from "@assets/icon/calendar.svg"; +import editIcon from "@assets/icon/edit.svg"; import milestoneIcon from "@assets/icon/milestone.svg"; +import trashIcon from "@assets/icon/trash.svg"; +import MilestoneEditor, { + MilestoneInfo, +} from "@components/Milestone/MilestoneEditor"; +import Button from "@components/common/Button"; +import ProgressBar from "@components/common/ProgressBar"; import { Milestone } from "@customTypes/index"; +import { deleteMilestone, putMilestoneState } from "api"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { TableBodyItem } from "../Table.style"; @@ -9,25 +21,126 @@ export default function MilestonesTableItem({ }: { milestone: Milestone; }) { - const { milestoneName, dueDate, description } = milestone; + const navigate = useNavigate(); + + const [isEditing, setIsEditing] = useState(false); + + const openEditor = () => setIsEditing(true); + const closeEditor = () => setIsEditing(false); + + const { + milestoneId, + milestoneName, + dueDate, + description, + openIssueCount, + closedIssueCount, + isOpen, + } = milestone; + + const onMilestoneDelete = async () => { + try { + const res = await deleteMilestone(milestoneId); + if (res.status === 204) { + navigate(0); + return; + } + } catch (error) { + // TODO: error handling + console.error(error); + } + }; + + const onMilestoneClose = async () => { + try { + const res = await putMilestoneState(milestoneId, "close"); + if (res.status === 200) { + navigate(0); + return; + } + } catch (error) { + // TODO: error handling + console.error(error); + } + }; + + const onMilestoneOpen = async () => { + try { + const res = await putMilestoneState(milestoneId, "open"); + if (res.status === 200) { + navigate(0); + return; + } + } catch (error) { + // TODO: error handling + console.error(error); + } + }; + + const milestoneInfo: MilestoneInfo = { + milestoneName: milestoneName, + dueDate, + description, + }; return ( - -
- 마일스톤 아이콘 - {milestoneName} - - 캘린더 아이콘 - {dueDate} - -
-
{description}
-
- - -
오른쪽
-
+ {isEditing ? ( + + ) : ( + <> + +
+ 마일스톤 아이콘 + {milestoneName} + + 캘린더 아이콘 + {dueDate} + +
+
{description}
+
+ + + + {isOpen ? ( + + ) : ( + + )} + + + + + + + )}
); } @@ -41,6 +154,7 @@ const LeftWrapper = styled.div` display: flex; flex-direction: column; gap: 8px; + flex-grow: 1; .milestone-info { display: flex; @@ -60,21 +174,22 @@ const LeftWrapper = styled.div` const RightWrapper = styled.div` display: flex; - gap: 24px; + flex-direction: column; + gap: 8px; + min-width: 244px; + align-items: flex-end; +`; - .tab-button-icon { - filter: ${({ theme: { filter } }) => filter.neutralTextDefault}; +const ButtonsContainer = styled.div` + display: flex; + align-items: center; + gap: 24px; - &.delete { + .delete { + img { filter: ${({ theme: { filter } }) => filter.dangerTextDefault}; } - } - - .tab-button-text { - font: ${({ theme: { font } }) => font.availableMD12}; - color: ${({ theme: { neutral } }) => neutral.text.default}; - - &.delete { + span { color: ${({ theme: { danger } }) => danger.text.default}; } } diff --git a/fe/src/components/common/ProgressBar.tsx b/fe/src/components/common/ProgressBar.tsx index e9cd1c756..8ed75f3c7 100644 --- a/fe/src/components/common/ProgressBar.tsx +++ b/fe/src/components/common/ProgressBar.tsx @@ -1,10 +1,12 @@ import styled from "styled-components"; export default function ProgressBar({ + variant = "label", name, openCount, closeCount, }: { + variant: "label" | "percent"; name: string; openCount: number; closeCount: number; @@ -14,13 +16,36 @@ export default function ProgressBar({ return ( - + {variant === "label" && } + {variant === "percent" && ( + + {percentage}% + {`열린 이슈 ${openCount}`} + {`닫힌 이슈 ${closeCount}`} + + )} ); } +const IssueCount = styled.span` + font: ${({ theme: { font } }) => font.displayMD12}; + color: ${({ theme: { neutral } }) => neutral.text.weak}; +`; + +const Info = styled.div` + display: flex; + justify-content: space-between; +`; + +const PercentText = styled.span` + font: ${({ theme: { font } }) => font.displayMD12}; + color: ${({ theme: { neutral } }) => neutral.text.weak}; +`; + const Wrapper = styled.div` display: flex; + width: 100%; flex-direction: column; gap: 8px; diff --git a/fe/src/mocks/data.ts b/fe/src/mocks/data.ts index 37dfb04be..93524137c 100644 --- a/fe/src/mocks/data.ts +++ b/fe/src/mocks/data.ts @@ -186,8 +186,8 @@ export const openMilestoneList = Array.from({ length: 10 }, (_, i) => { return { milestoneId: i + 1, milestoneName: faker.lorem.words(), - openIssueCount: faker.number.int(), - closedIssueCount: faker.number.int(), + openIssueCount: faker.number.int({ min: 0, max: 10 }), + closedIssueCount: faker.number.int({ min: 0, max: 10 }), description: faker.lorem.sentence(), dueDate: faker.date.future().toISOString().slice(0, 10), isOpen: true, @@ -198,8 +198,8 @@ export const closedMilestoneList = Array.from({ length: 3 }, (_, i) => { return { milestoneId: i + 10, milestoneName: faker.lorem.words(), - openIssueCount: faker.number.int(), - closedIssueCount: faker.number.int(), + openIssueCount: faker.number.int({ min: 0, max: 10 }), + closedIssueCount: faker.number.int({ min: 0, max: 10 }), description: faker.lorem.sentence(), dueDate: faker.date.future().toISOString().slice(0, 10), isOpen: false, diff --git a/fe/src/mocks/handlers.ts b/fe/src/mocks/handlers.ts index 331e978b3..44614da86 100644 --- a/fe/src/mocks/handlers.ts +++ b/fe/src/mocks/handlers.ts @@ -209,4 +209,8 @@ export const handlers = [ return res(ctx.status(200)); } }), + + rest.delete("/api/milestones/:milestoneId", async (_, res, ctx) => { + return res(ctx.status(204)); + }), ];