Skip to content

Commit 366ddbe

Browse files
authored
Merge pull request #306 from boostcampwm2023/feature/backlogPage
feat: 스토리별 백로그 조회, 스토리 추가 기능 구현
2 parents 9536305 + c0af411 commit 366ddbe

16 files changed

+400
-72
lines changed

frontend/src/components/backlog/EpicDropdown.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import EpicDropdownOption from "./EpicDropdownOption";
1212
interface EpicDropdownProps {
1313
selectedEpic?: EpicCategoryDTO;
1414
epicList: EpicCategoryDTO[];
15+
onEpicSelect: (epicId: number) => void;
1516
}
1617

17-
const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
18+
const EpicDropdown = ({
19+
selectedEpic,
20+
epicList,
21+
onEpicSelect,
22+
}: EpicDropdownProps) => {
1823
const { socket }: { socket: Socket } = useOutletContext();
1924
const { emitEpicCreateEvent } = useEpicEmitEvent(socket);
2025
const [value, setValue] = useState("");
@@ -40,8 +45,12 @@ const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
4045
}
4146
};
4247

48+
const handleEpicSelect = (epicId: number) => {
49+
onEpicSelect(epicId);
50+
};
51+
4352
return (
44-
<div className="relative p-1 rounded-md w-72 shadow-box">
53+
<div className="absolute p-1 bg-white rounded-md w-72 shadow-box">
4554
<div className="flex p-1 border-b-2">
4655
{selectedEpic && (
4756
<div className="min-w-[5rem]">
@@ -62,7 +71,9 @@ const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
6271
</div>
6372
<ul className="pt-1">
6473
{...epicList.map((epic) => (
65-
<EpicDropdownOption key={epic.id} epic={epic} />
74+
<li key={epic.id} onClick={() => handleEpicSelect(epic.id)}>
75+
<EpicDropdownOption key={epic.id} epic={epic} />
76+
</li>
6677
))}
6778
</ul>
6879
</div>

frontend/src/components/backlog/EpicDropdownOption.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ const EpicDropdownOption = ({ epic }: EpicDropdownOptionProps) => {
1919

2020
return (
2121
<>
22-
<li
23-
className="flex justify-between px-1 py-1 rounded-md group hover:cursor-pointer hover:bg-gray-100"
24-
key={epic.id}
25-
>
22+
<div className="flex justify-between px-1 py-1 rounded-md group hover:cursor-pointer hover:bg-gray-100">
2623
<CategoryChip content={epic.name} bgColor={epic.color} />
2724
<button
2825
className="invisible px-1 rounded-md group-hover:visible hover:bg-gray-300"
@@ -32,7 +29,7 @@ const EpicDropdownOption = ({ epic }: EpicDropdownOptionProps) => {
3229
>
3330
<MenuKebab width={20} height={20} stroke="#696969" />
3431
</button>
35-
</li>
32+
</div>
3633
{open && (
3734
<EpicUpdateBox
3835
epic={epic}
+62-27
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
1+
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
22
import { BacklogStatusType } from "../../types/DTO/backlogDTO";
33
import BacklogStatusChip from "./BacklogStatusChip";
44
import CategoryChip from "./CategoryChip";
5+
import TaskCreateButton from "./TaskCreateButton";
6+
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
7+
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
8+
import TaskContainer from "./TaskContainer";
9+
import TaskHeader from "./TaskHeader";
510

611
interface StoryBlockProps {
712
epic: string;
813
title: string;
9-
point: number;
14+
point: number | null;
1015
progress: number;
1116
status: BacklogStatusType;
17+
children: React.ReactNode;
18+
taskExist: boolean;
1219
}
1320

1421
const StoryBlock = ({
@@ -17,30 +24,58 @@ const StoryBlock = ({
1724
point,
1825
progress,
1926
status,
20-
}: StoryBlockProps) => (
21-
<div className="flex items-center gap-5 py-1 border-t border-b">
22-
<div className="w-[5rem]">
23-
<CategoryChip content={epic} bgColor="green" />
24-
</div>
25-
<div className="flex items-center gap-1 w-[38.75rem]">
26-
<button
27-
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
28-
type="button"
29-
>
30-
<ChevronDown width={16} height={16} fill="black" />
31-
</button>
32-
<p>{title}</p>
33-
</div>
34-
<div className="w-[4rem] text-right">
35-
<p className="">{point} POINT</p>
36-
</div>
37-
<div className="w-[4rem] text-right">
38-
<span>{progress}%</span>
39-
</div>
40-
<div className="w-[4rem]">
41-
<BacklogStatusChip status={status} />
42-
</div>
43-
</div>
44-
);
27+
taskExist,
28+
children,
29+
}: StoryBlockProps) => {
30+
const { showDetail, handleShowDetail } = useShowDetail();
31+
32+
return (
33+
<>
34+
<div className="flex items-center py-1 border-t border-b">
35+
<div className="w-[5rem] mr-5">
36+
<CategoryChip content={epic} bgColor="green" />
37+
</div>
38+
<div className="flex items-center gap-1 w-[40.9rem] mr-4">
39+
<button
40+
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
41+
type="button"
42+
onClick={() => handleShowDetail(!showDetail)}
43+
>
44+
{showDetail ? (
45+
<ChevronDown
46+
width={16}
47+
height={16}
48+
fill={taskExist ? "black" : "#C5C5C5"}
49+
/>
50+
) : (
51+
<ChevronRight
52+
width={16}
53+
height={16}
54+
fill={taskExist ? "black" : "#C5C5C5"}
55+
/>
56+
)}
57+
</button>
58+
<p>{title}</p>
59+
</div>
60+
<div className="w-[4rem] mr-[2.76rem] text-right">
61+
<p className="">{point} POINT</p>
62+
</div>
63+
<div className="w-[4rem] mr-[2.76rem] text-right">
64+
<span>{progress}%</span>
65+
</div>
66+
<div className="w-[6.25rem]">
67+
<BacklogStatusChip status={status} />
68+
</div>
69+
</div>
70+
{showDetail && (
71+
<TaskContainer>
72+
<TaskHeader />
73+
{children}
74+
<TaskCreateButton />
75+
</TaskContainer>
76+
)}
77+
</>
78+
);
79+
};
4580

4681
export default StoryBlock;
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,142 @@
1+
import { ChangeEvent, FormEvent, useMemo, useState } from "react";
12
import Check from "../../assets/icons/check.svg?react";
23
import Closed from "../../assets/icons/closed.svg?react";
34
import CategoryChip from "./CategoryChip";
5+
import { StoryForm } from "../../types/common/backlog";
6+
import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
7+
import { Socket } from "socket.io-client";
8+
import { useOutletContext } from "react-router-dom";
9+
// import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
10+
import EpicDropdown from "./EpicDropdown";
11+
import { EpicCategoryDTO } from "../../types/DTO/backlogDTO";
12+
import useDropdownState from "../../hooks/common/dropdown/useDropdownState";
413

5-
const StoryCreateForm = () => (
6-
<div className="flex items-center gap-5 py-1 border-t border-b">
7-
<div className="w-[5rem]">
8-
<CategoryChip content="프로젝트" bgColor="green" />
9-
</div>
10-
<input className="w-[38.75rem]" type="text" />
11-
<div className="flex items-center ">
12-
<input className="w-14" type="number" id="point-number" />
13-
<label htmlFor="point-number" className="">
14-
POINT
15-
</label>
16-
</div>
17-
<div className="flex items-center gap-2">
18-
<button
19-
className="flex items-center justify-center w-6 h-6 rounded-md bg-confirm-green"
20-
type="button"
21-
>
22-
<Check width={20} height={20} stroke="white" />
23-
</button>
24-
<button
25-
className="flex items-center justify-center w-6 h-6 rounded-md bg-error-red"
26-
type="button"
14+
interface StoryCreateFormProps {
15+
onCloseClick: () => void;
16+
epicList: EpicCategoryDTO[];
17+
}
18+
19+
const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
20+
const { socket }: { socket: Socket } = useOutletContext();
21+
const [{ title, point, epicId, status }, setStoryFormData] =
22+
useState<StoryForm>({
23+
title: "",
24+
point: undefined,
25+
status: "시작전",
26+
epicId: undefined,
27+
});
28+
const { open, handleClose, handleOpen, dropdownRef } = useDropdownState();
29+
const { emitStoryCreateEvent } = useStoryEmitEvent(socket);
30+
31+
const handleTitleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
32+
const { value } = target;
33+
setStoryFormData({ title: value, point, epicId, status });
34+
};
35+
36+
const handlePointChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
37+
const { value } = target;
38+
setStoryFormData({ title, point: Number(value), epicId, status });
39+
};
40+
41+
const handleSubmit = (event: FormEvent) => {
42+
event.preventDefault();
43+
if (epicId === undefined) {
44+
alert("에픽을 지정해주세요.");
45+
return;
46+
}
47+
48+
if (!title) {
49+
alert("제목을 입력해주세요.");
50+
return;
51+
}
52+
53+
if (point === undefined) {
54+
alert("포인트를 입력해주세요.");
55+
return;
56+
}
57+
58+
emitStoryCreateEvent({ title, status, epicId, point });
59+
onCloseClick();
60+
};
61+
62+
const handleEpicChange = (selectedEpicId: number) => {
63+
setStoryFormData({ title, status, point, epicId: selectedEpicId });
64+
handleClose();
65+
};
66+
67+
const handleEpicColumnClick = () => {
68+
if (!open) {
69+
handleOpen();
70+
}
71+
};
72+
73+
const selectedEpic = useMemo(
74+
() => epicList.filter(({ id }) => id === epicId)[0],
75+
[epicId]
76+
);
77+
return (
78+
<form
79+
className="flex items-center w-full py-1 border-t border-b"
80+
onSubmit={handleSubmit}
81+
>
82+
<div
83+
className="w-[5rem] min-h-[1.75rem] bg-light-gray rounded-md mr-7 hover:cursor-pointer relative"
84+
onClick={handleEpicColumnClick}
85+
ref={dropdownRef}
2786
>
28-
<Closed stroke="white" />
29-
</button>
30-
</div>
31-
</div>
32-
);
87+
{epicId && (
88+
<CategoryChip
89+
content={selectedEpic.name}
90+
bgColor={selectedEpic.color}
91+
/>
92+
)}
93+
{open && (
94+
<EpicDropdown
95+
selectedEpic={selectedEpic}
96+
epicList={epicList}
97+
onEpicSelect={handleEpicChange}
98+
/>
99+
)}
100+
</div>
101+
<input
102+
className="w-[34.7rem] h-[1.75rem] mr-[1.5rem] bg-light-gray rounded-md focus:outline-none"
103+
type="text"
104+
value={title}
105+
onChange={handleTitleChange}
106+
/>
107+
<div className="flex items-center mr-[2.8rem] ">
108+
<input
109+
className="w-24 h-[1.75rem] mr-1 text-right rounded-md bg-light-gray no-arrows focus:outline-none"
110+
type="number"
111+
id="point-number"
112+
value={point}
113+
onChange={handlePointChange}
114+
/>
115+
<label htmlFor="point-number" className="">
116+
POINT
117+
</label>
118+
</div>
119+
<div className="w-[4rem] mr-[2.76rem] text-right">
120+
<span>0%</span>
121+
</div>
122+
<div className="flex items-center gap-2">
123+
<button
124+
className="flex items-center justify-center w-6 h-6 rounded-md bg-confirm-green"
125+
type="button"
126+
onClick={handleSubmit}
127+
>
128+
<Check width={20} height={20} stroke="white" />
129+
</button>
130+
<button
131+
className="flex items-center justify-center w-6 h-6 rounded-md bg-error-red"
132+
type="button"
133+
onClick={onCloseClick}
134+
>
135+
<Closed stroke="white" />
136+
</button>
137+
</div>
138+
</form>
139+
);
140+
};
33141

34142
export default StoryCreateForm;

frontend/src/components/backlog/TaskBlock.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const TaskBlock = ({
1111
status,
1212
}: TaskDTO) => (
1313
<div className="flex items-center justify-between py-1 border-b">
14-
<p className="w-12">Task-{displayId}</p>
14+
<p className="w-[4rem]">Task-{displayId}</p>
1515
<p className="w-[25rem]">{title}</p>
1616
<div className="w-12">
1717
{assignedMemberId && (

frontend/src/components/backlog/TaskCreateButton.tsx

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

33
const TaskCreateButton = () => (
4-
<div className="py-1 border-b text-dark-gray">
4+
<div className="py-1 text-dark-gray">
55
<button
66
className="flex items-center justify-center w-full gap-1"
77
type="button"

frontend/src/components/backlog/TaskHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const TaskHeader = () => (
22
<div className="flex items-center justify-between py-1 border-b text-dark-gray">
3-
<p className="w-12">식별자</p>
3+
<p className="w-[4rem]">식별자</p>
44
<p className="w-[25rem]">태스크 이름</p>
55
<p className="w-12">담당자</p>
66
<p className="w-16">예상 시간</p>

0 commit comments

Comments
 (0)