Skip to content

Commit

Permalink
issue-tracker-08#125 feat: 마일스톤 수정 & 삭제 기능
Browse files Browse the repository at this point in the history
  • Loading branch information
youzysu committed Aug 11, 2023
1 parent be24f4b commit e1e59eb
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 51 deletions.
4 changes: 4 additions & 0 deletions fe/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,7 @@ export const putMilestoneState = async (
`/milestones/${milestoneId}?state=${state}`
);
};

export const deleteMilestone = async (milestoneId: number) => {
return await fetcherWithBearer.delete(`/milestones/${milestoneId}`);
};
47 changes: 28 additions & 19 deletions fe/src/components/Milestone/MilestoneEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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<HTMLInputElement>) => {
setNewMilestone((prev) => {
return { ...prev, newName: e.target.value };
return { ...prev, milestoneName: e.target.value };
});
};

const onDueDateChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewMilestone((prev) => {
return { ...prev, newDueDate: e.target.value };
return { ...prev, dueDate: e.target.value };
});
};

const onDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
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
Expand All @@ -91,24 +100,24 @@ export default function MilestoneEditor({
<TextInput
name="제목"
variant="short"
value={newMilestone.newName}
value={newMilestone.milestoneName}
placeholder="마일스톤의 이름을 입력하세요"
onChange={onNameChange}
/>
<TextInput
name="완료일(선택)"
variant="short"
value={newMilestone.newDueDate}
value={newMilestone.dueDate}
placeholder="YYYY-MM-DD"
onChange={onDueDateChange}
hasError={!!newMilestone.newDueDate && !isValidDueDate}
hasError={!!newMilestone.dueDate && !isValidDueDate}
helpText='"YYYY-MM-DD" 형식만 가능해요.'
/>
</div>
<TextInput
name="설명(선택)"
variant="short"
value={newMilestone.newDescription}
value={newMilestone.description}
placeholder="마일스톤에 대한 설명을 입력하세요"
onChange={onDescriptionChange}
/>
Expand All @@ -126,7 +135,7 @@ export default function MilestoneEditor({
type="submit"
variant="container"
size="S"
disabled={!isReadyToSubmit}>
disabled={!isReadyToSubmit[variant]}>
<img src={plusIcon} alt="완료" />
<span>완료</span>
</Button>
Expand Down
169 changes: 142 additions & 27 deletions fe/src/components/Table/MilestonesTable/MilestonesTableItem.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 (
<StyledMilestoneItem>
<LeftWrapper>
<div className="milestone-info">
<img src={milestoneIcon} alt="마일스톤 아이콘" />
<MilestoneName>{milestoneName}</MilestoneName>
<MilestoneDueDate>
<img src={calendarIcon} alt="캘린더 아이콘" />
<span>{dueDate}</span>
</MilestoneDueDate>
</div>
<div className="milestone-description">{description}</div>
</LeftWrapper>

<RightWrapper>
<div>오른쪽</div>
</RightWrapper>
{isEditing ? (
<MilestoneEditor
variant="edit"
milestoneId={milestoneId}
closeEditor={closeEditor}
milestoneInfo={milestoneInfo}
/>
) : (
<>
<LeftWrapper>
<div className="milestone-info">
<img src={milestoneIcon} alt="마일스톤 아이콘" />
<MilestoneName>{milestoneName}</MilestoneName>
<MilestoneDueDate>
<img src={calendarIcon} alt="캘린더 아이콘" />
<span>{dueDate}</span>
</MilestoneDueDate>
</div>
<div className="milestone-description">{description}</div>
</LeftWrapper>

<RightWrapper>
<ButtonsContainer>
{isOpen ? (
<Button variant="ghost" size="S" onClick={onMilestoneClose}>
<img src={archiveIcon} alt="닫기" />
<span className="tab-button-text">닫기</span>
</Button>
) : (
<Button variant="ghost" size="S" onClick={onMilestoneOpen}>
<img src={alertIcon} alt="열기" />
<span className="tab-button-text">열기</span>
</Button>
)}
<Button variant="ghost" size="S" onClick={openEditor}>
<img src={editIcon} alt="편집" />
<span className="tab-button-text">편집</span>
</Button>
<Button
variant="ghost"
size="S"
className="delete"
onClick={onMilestoneDelete}>
<img src={trashIcon} alt="삭제" />
<span className="tab-button-text">삭제</span>
</Button>
</ButtonsContainer>
<ProgressBar
variant="percent"
name={milestoneName}
openCount={openIssueCount}
closeCount={closedIssueCount}
/>
</RightWrapper>
</>
)}
</StyledMilestoneItem>
);
}
Expand All @@ -41,6 +154,7 @@ const LeftWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
flex-grow: 1;
.milestone-info {
display: flex;
Expand All @@ -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};
}
}
Expand Down
27 changes: 26 additions & 1 deletion fe/src/components/common/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,13 +16,36 @@ export default function ProgressBar({
return (
<Wrapper>
<progress id={name} max={100} value={percentage} />
<label htmlFor={name}>{name}</label>
{variant === "label" && <label htmlFor={name}>{name}</label>}
{variant === "percent" && (
<Info>
<PercentText>{percentage}%</PercentText>
<IssueCount>{`열린 이슈 ${openCount}`}</IssueCount>
<IssueCount>{`닫힌 이슈 ${closeCount}`}</IssueCount>
</Info>
)}
</Wrapper>
);
}

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;
Expand Down
8 changes: 4 additions & 4 deletions fe/src/mocks/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit e1e59eb

Please sign in to comment.