Skip to content

Commit dd1fdf7

Browse files
authored
Merge pull request #313 from boostcampwm2023/feature/backlogPage
feat: 태스크 삭제, 수정 API 연동
2 parents 7ed3874 + b5ce397 commit dd1fdf7

File tree

5 files changed

+315
-26
lines changed

5 files changed

+315
-26
lines changed

frontend/src/components/backlog/AssignedMemberDropdown.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ const AssignedMemberDropdown = ({
1313
const memberList = [myInfo, ...partialMemberList];
1414

1515
return (
16-
<div className="rounded-md w-fit shadow-box">
16+
<div className="absolute top-0 bg-white rounded-md w-fit shadow-box">
1717
<ul>
1818
{...memberList.map((member: LandingMemberDTO) => (
1919
<li
20-
className="p-2 hover:cursor-pointer hover:bg-gray-100"
20+
className="p-3 overflow-hidden rounded-md hover:cursor-pointer hover:bg-gray-100 text-ellipsis whitespace-nowrap"
2121
key={member.id}
2222
onClick={() => onOptionClick(member.id)}
2323
>
+254-21
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,266 @@
1-
import { TaskDTO } from "../../types/DTO/backlogDTO";
1+
import { MouseEvent, useMemo } from "react";
2+
import { useOutletContext } from "react-router-dom";
3+
import { Socket } from "socket.io-client";
4+
import useDropdownState from "../../hooks/common/dropdown/useDropdownState";
5+
import useBacklogInputChange from "../../hooks/pages/backlog/useBacklogInputChange";
6+
import useTaskEmitEvent from "../../hooks/pages/backlog/useTaskEmitEvent";
7+
import AssignedMemberDropdown from "./AssignedMemberDropdown";
28
import BacklogStatusChip from "./BacklogStatusChip";
3-
import CategoryChip from "./CategoryChip";
9+
import BacklogStatusDropdown from "./BacklogStatusDropdown";
10+
import useMemberStore from "../../stores/useMemberStore";
11+
import { BacklogStatusType, TaskDTO } from "../../types/DTO/backlogDTO";
12+
import { useModal } from "../../hooks/common/modal/useModal";
13+
import { MOUSE_KEY } from "../../constants/event";
14+
import ConfirmModal from "../common/ConfirmModal";
15+
import TrashCan from "../../assets/icons/trash-can.svg?react";
416

517
const TaskBlock = ({
18+
id,
619
displayId,
720
title,
821
assignedMemberId,
922
expectedTime,
1023
actualTime,
1124
status,
12-
}: TaskDTO) => (
13-
<div className="flex items-center justify-between py-1 border-b">
14-
<p className="w-[4rem]">Task-{displayId}</p>
15-
<p className="w-[25rem]">{title}</p>
16-
<div className="w-12">
17-
{assignedMemberId && (
18-
<CategoryChip content={`${assignedMemberId}`} bgColor="green" />
25+
}: TaskDTO) => {
26+
const { socket }: { socket: Socket } = useOutletContext();
27+
const {
28+
inputContainerRef: titleRef,
29+
inputElementRef: titleInputRef,
30+
updating: titleUpdating,
31+
handleUpdating: handleTitleUpdating,
32+
} = useBacklogInputChange(updateTitle);
33+
const {
34+
inputContainerRef: expectedTimeRef,
35+
inputElementRef: expectedTimeInputRef,
36+
updating: expectedTimeUpdating,
37+
handleUpdating: handleExpectedTimeUpdating,
38+
} = useBacklogInputChange(updateExpectedTime);
39+
const {
40+
inputContainerRef: actualTimeRef,
41+
inputElementRef: actualTimeInputRef,
42+
updating: actualTimeUpdating,
43+
handleUpdating: handleActualTimeUpdating,
44+
} = useBacklogInputChange(updateActualTime);
45+
const {
46+
open: assignedMemberUpdating,
47+
handleOpen: handleAssignedMemberUpdateOpen,
48+
dropdownRef: assignedMemberRef,
49+
} = useDropdownState();
50+
const {
51+
open: statusUpdating,
52+
handleOpen: handleStatusUpdateOpen,
53+
dropdownRef: statusRef,
54+
} = useDropdownState();
55+
const myInfo = useMemberStore((state) => state.myInfo);
56+
const partialMemberList = useMemberStore((state) => state.memberList);
57+
const { emitTaskUpdateEvent, emitTaskDeleteEvent } = useTaskEmitEvent(socket);
58+
const {
59+
open: deleteMenuOpen,
60+
handleOpen: handleDeleteMenuOpen,
61+
62+
dropdownRef: blockRef,
63+
} = useDropdownState();
64+
const { open, close } = useModal();
65+
66+
const assignedMemberName = useMemo(() => {
67+
if (assignedMemberId === null) {
68+
return "";
69+
}
70+
71+
if (myInfo.id === assignedMemberId) {
72+
return myInfo.username;
73+
}
74+
75+
return partialMemberList.filter(({ id }) => id === assignedMemberId)[0]
76+
.username;
77+
}, [assignedMemberId, partialMemberList]);
78+
79+
function updateTitle<T>(data: T) {
80+
if (!data || data === title) {
81+
return;
82+
}
83+
84+
if ((data as string).length > 100) {
85+
alert("태스크 타이틀은 100자 이하여야 합니다.");
86+
return;
87+
}
88+
89+
emitTaskUpdateEvent({ id, title: data as string });
90+
}
91+
function updateExpectedTime<T>(data: T) {
92+
if (!data || data === String(expectedTime)) {
93+
return;
94+
}
95+
96+
if (data === "") {
97+
emitTaskUpdateEvent({ id, expectedTime: null });
98+
return;
99+
}
100+
101+
if (!isNaN(Number(data)) && (Number(data) >= 100 || Number(data) < 0)) {
102+
alert(
103+
"예상 시간은 0이상, 100미만의 정수 또는 소수점 한 자릿수여야 합니다."
104+
);
105+
return;
106+
}
107+
108+
emitTaskUpdateEvent({ id, expectedTime: Number(data) });
109+
}
110+
function updateActualTime<T>(data: T) {
111+
if (!data || data === String(actualTime)) {
112+
return;
113+
}
114+
115+
if (data === "") {
116+
emitTaskUpdateEvent({ id, actualTime: null });
117+
return;
118+
}
119+
120+
if (!isNaN(Number(data)) && (Number(data) >= 100 || Number(data) < 0)) {
121+
alert(
122+
"실제 시간은 0이상, 100미만의 정수 또는 소수점 한 자릿수여야 합니다."
123+
);
124+
return;
125+
}
126+
emitTaskUpdateEvent({ id, actualTime: Number(data) });
127+
}
128+
function updateAssignedMember(data: number) {
129+
if (data === assignedMemberId) {
130+
return;
131+
}
132+
133+
emitTaskUpdateEvent({ id, assignedMemberId: data });
134+
}
135+
136+
function updateStatus(data: BacklogStatusType) {
137+
if (data === status) {
138+
return;
139+
}
140+
141+
emitTaskUpdateEvent({ id, status: data });
142+
}
143+
144+
const handleRightButtonClick = (event: MouseEvent) => {
145+
if (event.button === MOUSE_KEY.RIGHT) {
146+
handleDeleteMenuOpen();
147+
}
148+
};
149+
150+
const handleTaskDelete = () => {
151+
emitTaskDeleteEvent({ id });
152+
close();
153+
};
154+
155+
const handleDeleteButtonClick = () => {
156+
open(
157+
<ConfirmModal
158+
title="태스크 삭제"
159+
body="태스크가 삭제됩니다."
160+
confirmText="삭제"
161+
cancelText="취소"
162+
confirmColor="#E33535"
163+
cancelColor="#C6C6C6"
164+
onCancelButtonClick={close}
165+
onConfirmButtonClick={handleTaskDelete}
166+
/>
167+
);
168+
};
169+
170+
return (
171+
<>
172+
<div
173+
className="flex items-center justify-between py-1 border-b"
174+
onMouseUp={handleRightButtonClick}
175+
onContextMenu={(event) => event.preventDefault()}
176+
ref={blockRef}
177+
>
178+
<p className="w-[4rem]">Task-{displayId}</p>
179+
<div
180+
className="w-[25rem] min-h-[1.5rem] hover:cursor-pointer"
181+
ref={titleRef}
182+
onClick={() => handleTitleUpdating(true)}
183+
>
184+
{titleUpdating ? (
185+
<input
186+
className="w-full min-w-[1rem] focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer"
187+
ref={titleInputRef}
188+
defaultValue={title}
189+
type="text"
190+
/>
191+
) : (
192+
<span>{title}</span>
193+
)}
194+
</div>
195+
<div
196+
className="w-12 min-h-[1.5rem] hover:cursor-pointer relative"
197+
onClick={handleAssignedMemberUpdateOpen}
198+
>
199+
<div className="w-full min-h-[1.5rem]" ref={assignedMemberRef}>
200+
{assignedMemberId && <p>{assignedMemberName}</p>}
201+
</div>
202+
{assignedMemberUpdating && (
203+
<AssignedMemberDropdown onOptionClick={updateAssignedMember} />
204+
)}
205+
</div>
206+
<div
207+
className="w-16 min-h-[1.5rem] hover:cursor-pointer"
208+
ref={expectedTimeRef}
209+
onClick={() => handleExpectedTimeUpdating(true)}
210+
>
211+
{expectedTimeUpdating ? (
212+
<input
213+
className="w-full min-w-[1rem] no-arrows text-right focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer"
214+
ref={expectedTimeInputRef}
215+
defaultValue={expectedTime === null ? "" : expectedTime}
216+
type="number"
217+
/>
218+
) : (
219+
<p className="max-w-full text-right">{expectedTime}</p>
220+
)}
221+
</div>
222+
<div
223+
className="w-16 min-h-[1.5rem] hover:cursor-pointer"
224+
ref={actualTimeRef}
225+
onClick={() => handleActualTimeUpdating(true)}
226+
>
227+
{actualTimeUpdating ? (
228+
<input
229+
className="w-full min-w-[1rem] no-arrows text-right focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer"
230+
ref={actualTimeInputRef}
231+
defaultValue={actualTime === null ? "" : actualTime}
232+
type="number"
233+
/>
234+
) : (
235+
<p className="min-w-full text-right">{actualTime}</p>
236+
)}
237+
</div>
238+
<div
239+
className="w-[6.25rem] hover:cursor-pointer relative"
240+
onClick={handleStatusUpdateOpen}
241+
>
242+
<div ref={statusRef}>
243+
<BacklogStatusChip status={status} />
244+
</div>
245+
{statusUpdating && (
246+
<BacklogStatusDropdown onOptionClick={updateStatus} />
247+
)}
248+
</div>
249+
</div>
250+
{deleteMenuOpen && (
251+
<div className="absolute px-2 py-1 bg-white rounded-md shadow-box">
252+
<button
253+
className="flex items-center w-full gap-3"
254+
type="button"
255+
onClick={handleDeleteButtonClick}
256+
>
257+
<TrashCan width={20} height={20} fill="red" />
258+
<span>삭제</span>
259+
</button>
260+
</div>
19261
)}
20-
</div>
21-
<div className="w-16 ">
22-
<p className="max-w-full text-right">{expectedTime}</p>
23-
</div>
24-
<div className="w-16 ">
25-
<p className="max-w-full text-right">{actualTime}</p>
26-
</div>
27-
<div className="w-[6.25rem]">
28-
<BacklogStatusChip status={status} />
29-
</div>
30-
</div>
31-
);
262+
</>
263+
);
264+
};
32265

33266
export default TaskBlock;

frontend/src/components/backlog/TaskCreateForm.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => {
4141
const handleSubmit = (event: FormEvent) => {
4242
event.preventDefault();
4343
let { title, actualTime, expectedTime } = taskFormData;
44-
console.log(taskFormData);
4544

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

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

+41-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ const useBacklogSocket = (socket: Socket) => {
6262
setBacklog((prevBacklog) => {
6363
const newEpicList = prevBacklog.epicList.map((epic) => {
6464
if (epic.id === content.epicId) {
65-
const newStoryList = [...epic.storyList, content];
65+
const newStoryList = [
66+
...epic.storyList,
67+
{ ...content, taskList: [] },
68+
];
6669
return { ...epic, storyList: newStoryList };
6770
}
6871

@@ -127,6 +130,43 @@ const useBacklogSocket = (socket: Socket) => {
127130
return { epicList: newEpicList };
128131
});
129132
break;
133+
case BacklogSocketTaskAction.UPDATE:
134+
setBacklog((prevBacklog) => {
135+
const newEpicList = prevBacklog.epicList.map((epic) => {
136+
const newStoryList = epic.storyList.map((story) => {
137+
const newTaskList = story.taskList.map((task) => {
138+
if (task.id === content.id) {
139+
return { ...task, ...content };
140+
}
141+
142+
return task;
143+
});
144+
145+
return { ...story, taskList: newTaskList };
146+
});
147+
return { ...epic, storyList: newStoryList };
148+
});
149+
150+
return { epicList: newEpicList };
151+
});
152+
153+
break;
154+
case BacklogSocketTaskAction.DELETE:
155+
setBacklog((prevBacklog) => {
156+
const newEpicList = prevBacklog.epicList.map((epic) => {
157+
const newStoryList = epic.storyList.map((story) => {
158+
const newTaskList = story.taskList.filter(
159+
({ id }) => id !== content.id
160+
);
161+
162+
return { ...story, taskList: newTaskList };
163+
});
164+
return { ...epic, storyList: newStoryList };
165+
});
166+
167+
return { epicList: newEpicList };
168+
});
169+
break;
130170
}
131171
};
132172

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import { Socket } from "socket.io-client";
22
import { TaskForm } from "../../../types/common/backlog";
3+
import { BacklogStatusType } from "../../../types/DTO/backlogDTO";
34

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

9-
return { emitTaskCreateEvent };
10+
const emitTaskUpdateEvent = (content: {
11+
id: number;
12+
title?: string;
13+
expectedTime?: number | null;
14+
actualTime?: number | null;
15+
assignedMemberId?: number;
16+
storyId?: number;
17+
status?: BacklogStatusType;
18+
}) => {
19+
socket.emit("task", { action: "update", content });
20+
};
21+
22+
const emitTaskDeleteEvent = (content: {id: number}) => {
23+
socket.emit("task", {action: "delete", content})
24+
}
25+
26+
return { emitTaskCreateEvent, emitTaskUpdateEvent, emitTaskDeleteEvent };
1027
};
1128

1229
export default useTaskEmitEvent;

0 commit comments

Comments
 (0)