Skip to content

Commit d9d44e9

Browse files
authored
Merge pull request #318 from boostcampwm2023/feature/backlogPage
feat: 애픽 생성, 수정 추가 기능 구현
2 parents 8dd6600 + d09cada commit d9d44e9

File tree

8 files changed

+146
-47
lines changed

8 files changed

+146
-47
lines changed

frontend/src/components/backlog/EpicDropdown.tsx

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { ChangeEvent, useState } from "react";
1+
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
22
import { useOutletContext } from "react-router-dom";
33
import { Socket } from "socket.io-client";
44
import { EpicCategoryDTO } from "../../types/DTO/backlogDTO";
55
import CategoryChip from "./CategoryChip";
66
import useEpicEmitEvent from "../../hooks/pages/backlog/useEpicEmitEvent";
77
import { CATEGORY_COLOR } from "../../constants/backlog";
88
import getRandomNumber from "../../utils/getRandomNumber";
9-
import { BacklogCategoryColor } from "../../types/common/backlog";
9+
import {
10+
BacklogCategoryColor,
11+
BacklogSocketData,
12+
BacklogSocketDomain,
13+
BacklogSocketEpicAction,
14+
} from "../../types/common/backlog";
1015
import EpicDropdownOption from "./EpicDropdownOption";
1116

1217
interface EpicDropdownProps {
@@ -23,6 +28,13 @@ const EpicDropdown = ({
2328
const { socket }: { socket: Socket } = useOutletContext();
2429
const { emitEpicCreateEvent } = useEpicEmitEvent(socket);
2530
const [value, setValue] = useState("");
31+
const inputElementRef = useRef<HTMLInputElement | null>(null);
32+
const epicColor = useMemo(() => {
33+
const colors = Object.keys(CATEGORY_COLOR);
34+
return colors[
35+
getRandomNumber(0, colors.length - 1)
36+
] as BacklogCategoryColor;
37+
}, []);
2638

2739
const handleInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
2840
const { value } = target;
@@ -42,20 +54,35 @@ const EpicDropdown = ({
4254
}
4355

4456
setValue("");
45-
const colors = Object.keys(CATEGORY_COLOR);
46-
const color = colors[
47-
getRandomNumber(0, colors.length - 1)
48-
] as BacklogCategoryColor;
49-
emitEpicCreateEvent({ name: value, color });
57+
emitEpicCreateEvent({ name: value, color: epicColor });
5058
}
5159
};
5260

5361
const handleEpicChange = (epicId: number | undefined) => {
5462
onEpicChange(epicId);
5563
};
5664

65+
const handleEpicEvent = ({ domain, action, content }: BacklogSocketData) => {
66+
if (
67+
domain === BacklogSocketDomain.EPIC &&
68+
action === BacklogSocketEpicAction.CREATE &&
69+
!selectedEpic
70+
) {
71+
onEpicChange(content.id);
72+
}
73+
};
74+
75+
useEffect(() => {
76+
socket.on("backlog", handleEpicEvent);
77+
inputElementRef.current?.focus();
78+
79+
return () => {
80+
socket.off("backlog", handleEpicEvent);
81+
};
82+
}, []);
83+
5784
return (
58-
<div className="absolute p-1 bg-white rounded-md w-72 shadow-box">
85+
<div className="absolute z-10 p-1 bg-white rounded-md w-72 shadow-box">
5986
<div className="flex p-1 border-b-2">
6087
{selectedEpic && (
6188
<div className="min-w-[5rem]">
@@ -72,6 +99,7 @@ const EpicDropdown = ({
7299
value={value}
73100
onChange={handleInputChange}
74101
onKeyDown={handleEnterKeydown}
102+
ref={inputElementRef}
75103
/>
76104
</div>
77105
<ul className="pt-1">
@@ -90,6 +118,12 @@ const EpicDropdown = ({
90118
</li>
91119
))}
92120
</ul>
121+
{value && (
122+
<div className="flex items-center gap-2 p-1">
123+
<span>생성</span>
124+
<CategoryChip content={value} bgColor={epicColor} />
125+
</div>
126+
)}
93127
</div>
94128
);
95129
};

frontend/src/components/backlog/StoryBlock.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ const StoryBlock = ({
5050
handleUpdating: handleTitleUpdatingOpen,
5151
inputContainerRef: titleRef,
5252
inputElementRef: titleInputRef,
53+
handleEnterKeyup: handleTitleEnterKeyup,
5354
} = useBacklogInputChange(updateTitle);
5455
const {
5556
updating: pointUpdating,
5657
handleUpdating: handlePointUpdatingOpen,
5758
inputContainerRef: pointRef,
5859
inputElementRef: pointInputRef,
60+
handleEnterKeyup: handlePointEnterKeyup,
5961
} = useBacklogInputChange(updatePoint);
6062
const {
6163
open: statusUpdating,
@@ -91,16 +93,22 @@ const StoryBlock = ({
9193
emitStoryUpdateEvent({ id, title: data as string });
9294
}
9395
function updatePoint<T>(data: T) {
94-
if ((!data && data !== 0) || data === point) {
96+
const newPoint = Number(data);
97+
if ((!data && data !== 0) || newPoint === point) {
9598
return;
9699
}
97100

98-
if ((data as number) < 0 || (data as number) > 100) {
101+
if (newPoint < 0 || newPoint > 100) {
99102
alert("스토리 포인트는 0이상 100이하여야 합니다.");
100103
return;
101104
}
102105

103-
emitStoryUpdateEvent({ id, point: Number(data) });
106+
if (!Number.isInteger(newPoint)) {
107+
alert("포인트는 정수여야 합니다.");
108+
return;
109+
}
110+
111+
emitStoryUpdateEvent({ id, point: newPoint });
104112
}
105113

106114
function updateStatus(data: BacklogStatusType) {
@@ -208,6 +216,7 @@ const StoryBlock = ({
208216
type="text"
209217
ref={titleInputRef}
210218
defaultValue={title}
219+
onKeyUp={handleTitleEnterKeyup}
211220
/>
212221
) : (
213222
<span
@@ -225,13 +234,14 @@ const StoryBlock = ({
225234
>
226235
{pointUpdating ? (
227236
<input
228-
className={`w-fit min-w-[1rem] max-w-[3.5rem] no-arrows text-right focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer`}
237+
className={`min-w-[1.75rem] no-arrows text-right focus:outline-none rounded-sm bg-gray-200 hover:cursor-pointer`}
229238
type="number"
230239
ref={pointInputRef}
231240
defaultValue={point !== 0 && !point ? 0 : point}
241+
onKeyUp={handlePointEnterKeyup}
232242
/>
233243
) : (
234-
<span>{point}</span>
244+
<span className="min-w-[1.75rem] text-right">{point}</span>
235245
)}
236246

237247
<span> POINT</span>

frontend/src/components/backlog/StoryCreateForm.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeEvent, FormEvent, useMemo, useState } from "react";
1+
import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react";
22
import Check from "../../assets/icons/check.svg?react";
33
import Closed from "../../assets/icons/closed.svg?react";
44
import CategoryChip from "./CategoryChip";
@@ -34,7 +34,8 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
3434

3535
const handlePointChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
3636
const { value } = target;
37-
setStoryFormData({ title, point: Number(value), epicId, status });
37+
const newPoint = value === "" ? undefined : Number(value);
38+
setStoryFormData({ title, point: newPoint, epicId, status });
3839
};
3940

4041
const handleSubmit = (event: FormEvent) => {
@@ -90,6 +91,12 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
9091
[epicId, epicList]
9192
);
9293

94+
useEffect(() => {
95+
if (!epicList.filter(({ id }) => id === epicId).length) {
96+
setStoryFormData({ title, point, status, epicId: undefined });
97+
}
98+
}, [epicList]);
99+
93100
return (
94101
<form
95102
className="flex items-center w-full py-1 border-t border-b"
@@ -102,8 +109,8 @@ const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
102109
>
103110
{epicId && (
104111
<CategoryChip
105-
content={selectedEpic.name}
106-
bgColor={selectedEpic.color}
112+
content={selectedEpic?.name}
113+
bgColor={selectedEpic?.color}
107114
/>
108115
)}
109116
{open && (

frontend/src/components/backlog/TaskBlock.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ const TaskBlock = ({
3030
inputElementRef: titleInputRef,
3131
updating: titleUpdating,
3232
handleUpdating: handleTitleUpdating,
33+
handleEnterKeyup: handleTitleKeyup,
3334
} = useBacklogInputChange(updateTitle);
3435
const {
3536
inputContainerRef: expectedTimeRef,
3637
inputElementRef: expectedTimeInputRef,
3738
updating: expectedTimeUpdating,
3839
handleUpdating: handleExpectedTimeUpdating,
40+
handleEnterKeyup: handleExpectedTimeKeyup,
3941
} = useBacklogInputChange(updateExpectedTime);
4042
const {
4143
inputContainerRef: actualTimeRef,
4244
inputElementRef: actualTimeInputRef,
4345
updating: actualTimeUpdating,
4446
handleUpdating: handleActualTimeUpdating,
47+
handleEnterKeyup: handleActualTimeKeyup,
4548
} = useBacklogInputChange(updateActualTime);
4649
const {
4750
open: assignedMemberUpdating,
@@ -78,7 +81,12 @@ const TaskBlock = ({
7881
}, [assignedMemberId, partialMemberList]);
7982

8083
function updateTitle<T>(data: T) {
81-
if (!data || data === title) {
84+
if (data === title) {
85+
return;
86+
}
87+
88+
if (!data) {
89+
alert("태스크 타이틀을 입력해주세요");
8290
return;
8391
}
8492

@@ -189,7 +197,7 @@ const TaskBlock = ({
189197
onContextMenu={(event) => event.preventDefault()}
190198
ref={blockRef}
191199
>
192-
<p className="w-[4rem]">Task-{displayId}</p>
200+
<p className="w-[4rem] truncate">Task-{displayId}</p>
193201
<div
194202
className="w-[25rem] min-h-[1.5rem] hover:cursor-pointer truncate"
195203
ref={titleRef}
@@ -201,6 +209,7 @@ const TaskBlock = ({
201209
ref={titleInputRef}
202210
defaultValue={title}
203211
type="text"
212+
onKeyUp={handleTitleKeyup}
204213
/>
205214
) : (
206215
<span title={title}>{title}</span>
@@ -232,6 +241,7 @@ const TaskBlock = ({
232241
ref={expectedTimeInputRef}
233242
defaultValue={expectedTime === null ? "" : expectedTime}
234243
type="number"
244+
onKeyUp={handleExpectedTimeKeyup}
235245
/>
236246
) : (
237247
<p className="max-w-full text-right">{expectedTime}</p>
@@ -248,6 +258,7 @@ const TaskBlock = ({
248258
ref={actualTimeInputRef}
249259
defaultValue={actualTime === null ? "" : actualTime}
250260
type="number"
261+
onKeyUp={handleActualTimeKeyup}
251262
/>
252263
) : (
253264
<p className="min-w-full text-right">{actualTime}</p>

frontend/src/components/backlog/TaskCreateForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const TaskCreateForm = ({ onCloseClick, storyId }: TaskCreateFormProps) => {
7676
};
7777

7878
return (
79-
<form className="flex items-center justify-between px-1 py-1 border-b">
79+
<form className="flex items-center justify-between py-1 border-b">
8080
<div className="w-[4rem]" />
8181
<input
8282
type="text"

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ const useBacklogInputChange = (update: <T>(data: T) => void) => {
2626
}
2727
};
2828

29+
const handleEnterKeyup = (event: React.KeyboardEvent) => {
30+
if (event.nativeEvent.isComposing) {
31+
return;
32+
}
33+
34+
if (!updating) {
35+
return;
36+
}
37+
38+
if (event.code === "Enter" && inputElementRef.current) {
39+
update(inputElementRef.current.value);
40+
setUpdating(false);
41+
}
42+
};
43+
2944
useEffect(() => {
3045
window.addEventListener("mouseup", handleOutsideClick);
3146

@@ -34,7 +49,19 @@ const useBacklogInputChange = (update: <T>(data: T) => void) => {
3449
};
3550
}, [updating]);
3651

37-
return { updating, inputContainerRef, inputElementRef, handleUpdating };
52+
useEffect(() => {
53+
if (updating) {
54+
inputElementRef.current?.focus();
55+
}
56+
}, [updating]);
57+
58+
return {
59+
updating,
60+
inputContainerRef,
61+
inputElementRef,
62+
handleUpdating,
63+
handleEnterKeyup,
64+
};
3865
};
3966

4067
export default useBacklogInputChange;

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,23 @@ const useBacklogSocket = (socket: Socket) => {
7676
break;
7777
case BacklogSocketStoryAction.UPDATE:
7878
if (content.epicId) {
79-
let targetStory: StoryDTO | null = null;
80-
backlog.epicList.some((epic) => {
81-
const foundStory = epic.storyList.find(
82-
(story) => story.id === content.id
83-
);
84-
if (foundStory) {
85-
targetStory = { ...foundStory };
86-
return true;
87-
}
88-
return false;
89-
});
79+
setBacklog((prevBacklog) => {
80+
let targetStory: StoryDTO | null = null;
81+
backlog.epicList.some((epic) => {
82+
const foundStory = epic.storyList.find(
83+
(story) => story.id === content.id
84+
);
85+
if (foundStory) {
86+
targetStory = { ...foundStory };
87+
return true;
88+
}
89+
return false;
90+
});
9091

91-
if (!targetStory) {
92-
break;
93-
}
92+
if (!targetStory) {
93+
return prevBacklog;
94+
}
9495

95-
setBacklog((prevBacklog) => {
9696
const newEpicList = prevBacklog.epicList.map((epic) => {
9797
const newStoryList = epic.storyList.filter((story) => {
9898
if (story.id === content.id) {

0 commit comments

Comments
 (0)