Skip to content

Commit fc983e6

Browse files
authored
Merge pull request #311 from boostcampwm2023/feature/backlogPage
feat: 태스크 생성 API 연동, 스토리 피드백 반영
2 parents 0b3bf49 + a04db17 commit fc983e6

File tree

9 files changed

+264
-7
lines changed

9 files changed

+264
-7
lines changed

frontend/src/components/backlog/StoryBlock.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
44
import { BacklogStatusType, EpicCategoryDTO } from "../../types/DTO/backlogDTO";
55
import BacklogStatusChip from "./BacklogStatusChip";
66
import CategoryChip from "./CategoryChip";
7-
import TaskCreateButton from "./TaskCreateButton";
87
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
98
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
109
import TaskContainer from "./TaskContainer";
@@ -19,6 +18,7 @@ import TrashCan from "../../assets/icons/trash-can.svg?react";
1918
import { useModal } from "../../hooks/common/modal/useModal";
2019
import ConfirmModal from "../common/ConfirmModal";
2120
import EpicDropdown from "./EpicDropdown";
21+
import TaskCreateBlock from "./TaskCreateBlock";
2222

2323
interface StoryBlockProps {
2424
id: number;
@@ -182,7 +182,10 @@ const StoryBlock = ({
182182
<button
183183
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
184184
type="button"
185-
onClick={() => handleShowDetail(!showDetail)}
185+
onClick={(event) => {
186+
event.stopPropagation();
187+
handleShowDetail(!showDetail);
188+
}}
186189
>
187190
{showDetail ? (
188191
<ChevronDown
@@ -206,7 +209,12 @@ const StoryBlock = ({
206209
defaultValue={title}
207210
/>
208211
) : (
209-
<span className="w-full hover:cursor-pointer">{title}</span>
212+
<span
213+
title={title}
214+
className="w-full overflow-hidden hover:cursor-pointer text-ellipsis whitespace-nowrap"
215+
>
216+
{title}
217+
</span>
210218
)}
211219
</div>
212220
<div
@@ -258,7 +266,7 @@ const StoryBlock = ({
258266
<TaskContainer>
259267
<TaskHeader />
260268
{children}
261-
<TaskCreateButton />
269+
<TaskCreateBlock storyId={id} />
262270
</TaskContainer>
263271
)}
264272
</>

frontend/src/components/backlog/StoryCreateForm.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
3939

4040
const handleSubmit = (event: FormEvent) => {
4141
event.preventDefault();
42+
4243
if (epicId === undefined) {
4344
alert("에픽을 지정해주세요.");
4445
return;
@@ -54,6 +55,21 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
5455
return;
5556
}
5657

58+
if (title.length > 100) {
59+
alert("스토리 타이틀은 100자 이하여야 합니다.");
60+
return;
61+
}
62+
63+
if (point < 0 || point > 100) {
64+
alert("포인트는 0이상 100이하여야 합니다.");
65+
return;
66+
}
67+
68+
if (!Number.isInteger(point)) {
69+
alert("포인트는 정수여야 합니다.");
70+
return;
71+
}
72+
5773
emitStoryCreateEvent({ title, status, epicId, point });
5874
onCloseClick();
5975
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
2+
import TaskCreateButton from "./TaskCreateButton";
3+
import TaskCreateForm from "./TaskCreateForm";
4+
5+
interface TaskCreateBlockProps {
6+
storyId: number;
7+
}
8+
9+
const TaskCreateBlock = ({ storyId }: TaskCreateBlockProps) => {
10+
const { showDetail, handleShowDetail } = useShowDetail();
11+
return (
12+
<>
13+
{showDetail ? (
14+
<TaskCreateForm
15+
{...{ storyId }}
16+
onCloseClick={() => handleShowDetail(false)}
17+
/>
18+
) : (
19+
<TaskCreateButton onClick={() => handleShowDetail(true)} />
20+
)}
21+
</>
22+
);
23+
};
24+
25+
export default TaskCreateBlock;

frontend/src/components/backlog/TaskCreateButton.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import Plus from "../../assets/icons/plus.svg?react";
22

3-
const TaskCreateButton = () => (
3+
interface TaskCreateButtonProps {
4+
onClick: () => void;
5+
}
6+
7+
const TaskCreateButton = ({ onClick }: TaskCreateButtonProps) => (
48
<div className="py-1 text-dark-gray">
59
<button
610
className="flex items-center justify-center w-full gap-1"
711
type="button"
12+
onClick={onClick}
813
>
914
<Plus width={24} height={24} stroke="#696969" />
1015
<p>Task 생성하기</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { ChangeEvent, FormEvent, useState } from "react";
2+
import { useOutletContext } from "react-router-dom";
3+
import { Socket } from "socket.io-client";
4+
import Check from "../../assets/icons/check.svg?react";
5+
import Closed from "../../assets/icons/closed.svg?react";
6+
import useTaskEmitEvent from "../../hooks/pages/backlog/useTaskEmitEvent";
7+
import { TaskForm } from "../../types/common/backlog";
8+
9+
interface TaskCreateFormProps {
10+
onCloseClick: () => void;
11+
storyId: number;
12+
}
13+
14+
const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => {
15+
const [taskFormData, setTaskFormData] = useState<TaskForm>({
16+
title: "",
17+
expectedTime: null,
18+
actualTime: null,
19+
status: "시작전",
20+
assignedMemberId: null,
21+
storyId,
22+
});
23+
const { socket }: { socket: Socket } = useOutletContext();
24+
const { emitTaskCreateEvent } = useTaskEmitEvent(socket);
25+
26+
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
27+
const { value } = event.target;
28+
setTaskFormData({ ...taskFormData, title: value });
29+
};
30+
31+
const handleExpectedTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
32+
const { value } = event.target;
33+
setTaskFormData({ ...taskFormData, expectedTime: Number(value) });
34+
};
35+
36+
const handleActualTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
37+
const { value } = event.target;
38+
setTaskFormData({ ...taskFormData, actualTime: Number(value) });
39+
};
40+
41+
const handleSubmit = (event: FormEvent) => {
42+
event.preventDefault();
43+
let { title, actualTime, expectedTime } = taskFormData;
44+
console.log(taskFormData);
45+
46+
if (title.length > 100) {
47+
alert("제목은 100자 이내여야 합니다.");
48+
return;
49+
}
50+
51+
if (
52+
typeof expectedTime === "number" &&
53+
(expectedTime < 0 || expectedTime >= 100)
54+
) {
55+
alert("예상 시간은 0이상, 100 미만이어야 합니다.");
56+
return;
57+
}
58+
59+
if (
60+
typeof actualTime === "number" &&
61+
(actualTime < 0 || actualTime >= 100)
62+
) {
63+
alert("실제 시간은 0이상, 100 미만이어야 합니다.");
64+
return;
65+
}
66+
67+
if (actualTime === "") {
68+
actualTime = null;
69+
}
70+
71+
if (expectedTime === "") {
72+
expectedTime = null;
73+
}
74+
75+
emitTaskCreateEvent({ ...taskFormData, actualTime, expectedTime });
76+
onCloseClick();
77+
};
78+
79+
return (
80+
<form className="flex items-center justify-between px-1 py-1 border-b">
81+
<div className="w-[4rem]" />
82+
<input
83+
type="text"
84+
className="w-[25rem] bg-gray-200 rounded-sm focus:outline-none px-1"
85+
onChange={handleTitleChange}
86+
/>
87+
<div className="w-12"></div>
88+
<div className="w-16 ">
89+
<input
90+
type="number"
91+
className="max-w-full px-1 text-right bg-gray-200 rounded-sm no-arrows focus:outline-none"
92+
onChange={handleExpectedTimeChange}
93+
value={
94+
taskFormData.expectedTime === null ? "" : taskFormData.expectedTime
95+
}
96+
/>
97+
</div>
98+
<div className="w-16 ">
99+
<input
100+
type="number"
101+
className="max-w-full px-1 text-right bg-gray-200 rounded-sm no-arrows focus:outline-none"
102+
onChange={handleActualTimeChange}
103+
value={
104+
taskFormData.actualTime === null ? "" : taskFormData.actualTime
105+
}
106+
/>
107+
</div>
108+
<div className="w-[6.25rem]">
109+
<div className="flex items-center gap-2">
110+
<button
111+
className="flex items-center justify-center w-6 h-6 rounded-md bg-confirm-green"
112+
type="button"
113+
onClick={handleSubmit}
114+
>
115+
<Check width={20} height={20} stroke="white" />
116+
</button>
117+
<button
118+
className="flex items-center justify-center w-6 h-6 rounded-md bg-error-red"
119+
type="button"
120+
onClick={onCloseClick}
121+
>
122+
<Closed stroke="white" />
123+
</button>
124+
</div>
125+
</div>
126+
</form>
127+
);
128+
};
129+
130+
export default TaskCreateForm;

frontend/src/hooks/pages/backlog/useBacklogSocket.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { useEffect, useState } from "react";
22
import { Socket } from "socket.io-client";
3-
import { BacklogDTO, EpicDTO, StoryDTO } from "../../../types/DTO/backlogDTO";
3+
import {
4+
BacklogDTO,
5+
EpicDTO,
6+
StoryDTO,
7+
TaskDTO,
8+
} from "../../../types/DTO/backlogDTO";
49
import {
510
BacklogSocketData,
611
BacklogSocketDomain,
712
BacklogSocketEpicAction,
813
BacklogSocketStoryAction,
14+
BacklogSocketTaskAction,
915
} from "../../../types/common/backlog";
1016

1117
const useBacklogSocket = (socket: Socket) => {
@@ -96,6 +102,34 @@ const useBacklogSocket = (socket: Socket) => {
96102
}
97103
};
98104

105+
const handleTaskEvent = (
106+
action: BacklogSocketTaskAction,
107+
content: TaskDTO
108+
) => {
109+
switch (action) {
110+
case BacklogSocketTaskAction.CREATE:
111+
setBacklog((prevBacklog) => {
112+
const newEpicList = prevBacklog.epicList.map((epic) => {
113+
if (
114+
epic.storyList.filter(({ id }) => id === content.storyId).length
115+
) {
116+
const newStoryList = epic.storyList.map((story) => {
117+
if (story.id === content.storyId) {
118+
return { ...story, taskList: [...story.taskList, content] };
119+
}
120+
return story;
121+
});
122+
123+
return { ...epic, storyList: newStoryList };
124+
}
125+
return epic;
126+
});
127+
return { epicList: newEpicList };
128+
});
129+
break;
130+
}
131+
};
132+
99133
const handleOnBacklog = ({ domain, action, content }: BacklogSocketData) => {
100134
switch (domain) {
101135
case BacklogSocketDomain.BACKLOG:
@@ -107,6 +141,9 @@ const useBacklogSocket = (socket: Socket) => {
107141
case BacklogSocketDomain.STORY:
108142
handleStoryEvent(action, content);
109143
break;
144+
case BacklogSocketDomain.TASK:
145+
handleTaskEvent(action, content);
146+
break;
110147
}
111148
};
112149

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Socket } from "socket.io-client";
2+
import { TaskForm } from "../../../types/common/backlog";
3+
4+
const useTaskEmitEvent = (socket: Socket) => {
5+
const emitTaskCreateEvent = (content: TaskForm) => {
6+
socket.emit("task", { action: "create", content });
7+
};
8+
9+
return { emitTaskCreateEvent };
10+
};
11+
12+
export default useTaskEmitEvent;

frontend/src/types/DTO/backlogDTO.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface TaskDTO {
1717
actualTime: number | null;
1818
status: BacklogStatusType;
1919
assignedMemberId: number | null;
20+
storyId: number;
2021
}
2122

2223
export interface StoryDTO {

frontend/src/types/common/backlog.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
EpicCategoryDTO,
44
EpicDTO,
55
StoryDTO,
6+
TaskDTO,
67
} from "../DTO/backlogDTO";
78

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

33+
export interface TaskForm {
34+
storyId: number;
35+
title: string;
36+
expectedTime: number | null | "";
37+
actualTime: number | null | "";
38+
status: "시작전";
39+
assignedMemberId: null;
40+
}
41+
3242
export enum BacklogSocketDomain {
3343
BACKLOG = "backlog",
3444
EPIC = "epic",
@@ -48,6 +58,12 @@ export enum BacklogSocketStoryAction {
4858
UPDATE = "update",
4959
}
5060

61+
export enum BacklogSocketTaskAction {
62+
CREATE = "create",
63+
DELETE = "delete",
64+
UPDATE = "update",
65+
}
66+
5167
export interface BacklogSocketInitData {
5268
domain: BacklogSocketDomain.BACKLOG;
5369
action: "init";
@@ -66,7 +82,14 @@ export interface BacklogSocketStoryData {
6682
content: StoryDTO;
6783
}
6884

85+
export interface BacklogSocketTaskData {
86+
domain: BacklogSocketDomain.TASK;
87+
action: BacklogSocketTaskAction;
88+
content: TaskDTO;
89+
}
90+
6991
export type BacklogSocketData =
7092
| BacklogSocketInitData
7193
| BacklogSocketEpicData
72-
| BacklogSocketStoryData;
94+
| BacklogSocketStoryData
95+
| BacklogSocketTaskData;

0 commit comments

Comments
 (0)