Skip to content

Commit

Permalink
Merge pull request #311 from boostcampwm2023/feature/backlogPage
Browse files Browse the repository at this point in the history
feat: 태스크 생성 API 연동, 스토리 피드백 반영
  • Loading branch information
surinkwon authored Jul 18, 2024
2 parents 0b3bf49 + a04db17 commit fc983e6
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 7 deletions.
16 changes: 12 additions & 4 deletions frontend/src/components/backlog/StoryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
import BacklogStatusChip from "./BacklogStatusChip";
import CategoryChip from "./CategoryChip";
import TaskCreateButton from "./TaskCreateButton";
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
import TaskContainer from "./TaskContainer";
Expand All @@ -19,6 +18,7 @@ import TrashCan from "../../assets/icons/trash-can.svg?react";
import { useModal } from "../../hooks/common/modal/useModal";
import ConfirmModal from "../common/ConfirmModal";
import EpicDropdown from "./EpicDropdown";
import TaskCreateBlock from "./TaskCreateBlock";

interface StoryBlockProps {
id: number;
Expand Down Expand Up @@ -182,7 +182,10 @@ const StoryBlock = ({
<button
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
type="button"
onClick={() => handleShowDetail(!showDetail)}
onClick={(event) => {
event.stopPropagation();
handleShowDetail(!showDetail);
}}
>
{showDetail ? (
<ChevronDown
Expand All @@ -206,7 +209,12 @@ const StoryBlock = ({
defaultValue={title}
/>
) : (
<span className="w-full hover:cursor-pointer">{title}</span>
<span
title={title}
className="w-full overflow-hidden hover:cursor-pointer text-ellipsis whitespace-nowrap"
>
{title}
</span>
)}
</div>
<div
Expand Down Expand Up @@ -258,7 +266,7 @@ const StoryBlock = ({
<TaskContainer>
<TaskHeader />
{children}
<TaskCreateButton />
<TaskCreateBlock storyId={id} />
</TaskContainer>
)}
</>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/backlog/StoryCreateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {

const handleSubmit = (event: FormEvent) => {
event.preventDefault();

if (epicId === undefined) {
alert("에픽을 지정해주세요.");
return;
Expand All @@ -54,6 +55,21 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
return;
}

if (title.length > 100) {
alert("스토리 타이틀은 100자 이하여야 합니다.");
return;
}

if (point < 0 || point > 100) {
alert("포인트는 0이상 100이하여야 합니다.");
return;
}

if (!Number.isInteger(point)) {
alert("포인트는 정수여야 합니다.");
return;
}

emitStoryCreateEvent({ title, status, epicId, point });
onCloseClick();
};
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/backlog/TaskCreateBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import TaskCreateButton from "./TaskCreateButton";
import TaskCreateForm from "./TaskCreateForm";

interface TaskCreateBlockProps {
storyId: number;
}

const TaskCreateBlock = ({ storyId }: TaskCreateBlockProps) => {
const { showDetail, handleShowDetail } = useShowDetail();
return (
<>
{showDetail ? (
<TaskCreateForm
{...{ storyId }}
onCloseClick={() => handleShowDetail(false)}
/>
) : (
<TaskCreateButton onClick={() => handleShowDetail(true)} />
)}
</>
);
};

export default TaskCreateBlock;
7 changes: 6 additions & 1 deletion frontend/src/components/backlog/TaskCreateButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Plus from "../../assets/icons/plus.svg?react";

const TaskCreateButton = () => (
interface TaskCreateButtonProps {
onClick: () => void;
}

const TaskCreateButton = ({ onClick }: TaskCreateButtonProps) => (
<div className="py-1 text-dark-gray">
<button
className="flex items-center justify-center w-full gap-1"
type="button"
onClick={onClick}
>
<Plus width={24} height={24} stroke="#696969" />
<p>Task 생성하기</p>
Expand Down
130 changes: 130 additions & 0 deletions frontend/src/components/backlog/TaskCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ChangeEvent, FormEvent, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { Socket } from "socket.io-client";
import Check from "../../assets/icons/check.svg?react";
import Closed from "../../assets/icons/closed.svg?react";
import useTaskEmitEvent from "../../hooks/pages/backlog/useTaskEmitEvent";
import { TaskForm } from "../../types/common/backlog";

interface TaskCreateFormProps {
onCloseClick: () => void;
storyId: number;
}

const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => {
const [taskFormData, setTaskFormData] = useState<TaskForm>({
title: "",
expectedTime: null,
actualTime: null,
status: "시작전",
assignedMemberId: null,
storyId,
});
const { socket }: { socket: Socket } = useOutletContext();
const { emitTaskCreateEvent } = useTaskEmitEvent(socket);

const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setTaskFormData({ ...taskFormData, title: value });
};

const handleExpectedTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setTaskFormData({ ...taskFormData, expectedTime: Number(value) });
};

const handleActualTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setTaskFormData({ ...taskFormData, actualTime: Number(value) });
};

const handleSubmit = (event: FormEvent) => {
event.preventDefault();
let { title, actualTime, expectedTime } = taskFormData;
console.log(taskFormData);

if (title.length > 100) {
alert("제목은 100자 이내여야 합니다.");
return;
}

if (
typeof expectedTime === "number" &&
(expectedTime < 0 || expectedTime >= 100)
) {
alert("예상 시간은 0이상, 100 미만이어야 합니다.");
return;
}

if (
typeof actualTime === "number" &&
(actualTime < 0 || actualTime >= 100)
) {
alert("실제 시간은 0이상, 100 미만이어야 합니다.");
return;
}

if (actualTime === "") {
actualTime = null;
}

if (expectedTime === "") {
expectedTime = null;
}

emitTaskCreateEvent({ ...taskFormData, actualTime, expectedTime });
onCloseClick();
};

return (
<form className="flex items-center justify-between px-1 py-1 border-b">
<div className="w-[4rem]" />
<input
type="text"
className="w-[25rem] bg-gray-200 rounded-sm focus:outline-none px-1"
onChange={handleTitleChange}
/>
<div className="w-12"></div>
<div className="w-16 ">
<input
type="number"
className="max-w-full px-1 text-right bg-gray-200 rounded-sm no-arrows focus:outline-none"
onChange={handleExpectedTimeChange}
value={
taskFormData.expectedTime === null ? "" : taskFormData.expectedTime
}
/>
</div>
<div className="w-16 ">
<input
type="number"
className="max-w-full px-1 text-right bg-gray-200 rounded-sm no-arrows focus:outline-none"
onChange={handleActualTimeChange}
value={
taskFormData.actualTime === null ? "" : taskFormData.actualTime
}
/>
</div>
<div className="w-[6.25rem]">
<div className="flex items-center gap-2">
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-confirm-green"
type="button"
onClick={handleSubmit}
>
<Check width={20} height={20} stroke="white" />
</button>
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-error-red"
type="button"
onClick={onCloseClick}
>
<Closed stroke="white" />
</button>
</div>
</div>
</form>
);
};

export default TaskCreateForm;
39 changes: 38 additions & 1 deletion frontend/src/hooks/pages/backlog/useBacklogSocket.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { useEffect, useState } from "react";
import { Socket } from "socket.io-client";
import { BacklogDTO, EpicDTO, StoryDTO } from "../../../types/DTO/backlogDTO";
import {
BacklogDTO,
EpicDTO,
StoryDTO,
TaskDTO,
} from "../../../types/DTO/backlogDTO";
import {
BacklogSocketData,
BacklogSocketDomain,
BacklogSocketEpicAction,
BacklogSocketStoryAction,
BacklogSocketTaskAction,
} from "../../../types/common/backlog";

const useBacklogSocket = (socket: Socket) => {
Expand Down Expand Up @@ -96,6 +102,34 @@ const useBacklogSocket = (socket: Socket) => {
}
};

const handleTaskEvent = (
action: BacklogSocketTaskAction,
content: TaskDTO
) => {
switch (action) {
case BacklogSocketTaskAction.CREATE:
setBacklog((prevBacklog) => {
const newEpicList = prevBacklog.epicList.map((epic) => {
if (
epic.storyList.filter(({ id }) => id === content.storyId).length
) {
const newStoryList = epic.storyList.map((story) => {
if (story.id === content.storyId) {
return { ...story, taskList: [...story.taskList, content] };
}
return story;
});

return { ...epic, storyList: newStoryList };
}
return epic;
});
return { epicList: newEpicList };
});
break;
}
};

const handleOnBacklog = ({ domain, action, content }: BacklogSocketData) => {
switch (domain) {
case BacklogSocketDomain.BACKLOG:
Expand All @@ -107,6 +141,9 @@ const useBacklogSocket = (socket: Socket) => {
case BacklogSocketDomain.STORY:
handleStoryEvent(action, content);
break;
case BacklogSocketDomain.TASK:
handleTaskEvent(action, content);
break;
}
};

Expand Down
12 changes: 12 additions & 0 deletions frontend/src/hooks/pages/backlog/useTaskEmitEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Socket } from "socket.io-client";
import { TaskForm } from "../../../types/common/backlog";

const useTaskEmitEvent = (socket: Socket) => {
const emitTaskCreateEvent = (content: TaskForm) => {
socket.emit("task", { action: "create", content });
};

return { emitTaskCreateEvent };
};

export default useTaskEmitEvent;
1 change: 1 addition & 0 deletions frontend/src/types/DTO/backlogDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface TaskDTO {
actualTime: number | null;
status: BacklogStatusType;
assignedMemberId: number | null;
storyId: number;
}

export interface StoryDTO {
Expand Down
25 changes: 24 additions & 1 deletion frontend/src/types/common/backlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EpicCategoryDTO,
EpicDTO,
StoryDTO,
TaskDTO,
} from "../DTO/backlogDTO";

export type BacklogPath = "backlog" | "epic" | "completed";
Expand All @@ -29,6 +30,15 @@ export interface StoryForm {
status: "시작전";
}

export interface TaskForm {
storyId: number;
title: string;
expectedTime: number | null | "";
actualTime: number | null | "";
status: "시작전";
assignedMemberId: null;
}

export enum BacklogSocketDomain {
BACKLOG = "backlog",
EPIC = "epic",
Expand All @@ -48,6 +58,12 @@ export enum BacklogSocketStoryAction {
UPDATE = "update",
}

export enum BacklogSocketTaskAction {
CREATE = "create",
DELETE = "delete",
UPDATE = "update",
}

export interface BacklogSocketInitData {
domain: BacklogSocketDomain.BACKLOG;
action: "init";
Expand All @@ -66,7 +82,14 @@ export interface BacklogSocketStoryData {
content: StoryDTO;
}

export interface BacklogSocketTaskData {
domain: BacklogSocketDomain.TASK;
action: BacklogSocketTaskAction;
content: TaskDTO;
}

export type BacklogSocketData =
| BacklogSocketInitData
| BacklogSocketEpicData
| BacklogSocketStoryData;
| BacklogSocketStoryData
| BacklogSocketTaskData;

0 comments on commit fc983e6

Please sign in to comment.