Skip to content

Commit

Permalink
feat: 스토리 추가 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
surinkwon committed Jul 7, 2024
1 parent 99f4eb2 commit c0af411
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 48 deletions.
17 changes: 14 additions & 3 deletions frontend/src/components/backlog/EpicDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import EpicDropdownOption from "./EpicDropdownOption";
interface EpicDropdownProps {
selectedEpic?: EpicCategoryDTO;
epicList: EpicCategoryDTO[];
onEpicSelect: (epicId: number) => void;
}

const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
const EpicDropdown = ({
selectedEpic,
epicList,
onEpicSelect,
}: EpicDropdownProps) => {
const { socket }: { socket: Socket } = useOutletContext();
const { emitEpicCreateEvent } = useEpicEmitEvent(socket);
const [value, setValue] = useState("");
Expand All @@ -40,8 +45,12 @@ const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
}
};

const handleEpicSelect = (epicId: number) => {
onEpicSelect(epicId);
};

return (
<div className="relative p-1 rounded-md w-72 shadow-box">
<div className="absolute p-1 bg-white rounded-md w-72 shadow-box">
<div className="flex p-1 border-b-2">
{selectedEpic && (
<div className="min-w-[5rem]">
Expand All @@ -62,7 +71,9 @@ const EpicDropdown = ({ selectedEpic, epicList }: EpicDropdownProps) => {
</div>
<ul className="pt-1">
{...epicList.map((epic) => (
<EpicDropdownOption key={epic.id} epic={epic} />
<li key={epic.id} onClick={() => handleEpicSelect(epic.id)}>
<EpicDropdownOption key={epic.id} epic={epic} />
</li>
))}
</ul>
</div>
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/components/backlog/EpicDropdownOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ const EpicDropdownOption = ({ epic }: EpicDropdownOptionProps) => {

return (
<>
<li
className="flex justify-between px-1 py-1 rounded-md group hover:cursor-pointer hover:bg-gray-100"
key={epic.id}
>
<div className="flex justify-between px-1 py-1 rounded-md group hover:cursor-pointer hover:bg-gray-100">
<CategoryChip content={epic.name} bgColor={epic.color} />
<button
className="invisible px-1 rounded-md group-hover:visible hover:bg-gray-300"
Expand All @@ -32,7 +29,7 @@ const EpicDropdownOption = ({ epic }: EpicDropdownOptionProps) => {
>
<MenuKebab width={20} height={20} stroke="#696969" />
</button>
</li>
</div>
{open && (
<EpicUpdateBox
epic={epic}
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/components/backlog/StoryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface StoryBlockProps {
progress: number;
status: BacklogStatusType;
children: React.ReactNode;
taskExist: boolean;
}

const StoryBlock = ({
Expand All @@ -23,6 +24,7 @@ const StoryBlock = ({
point,
progress,
status,
taskExist,
children,
}: StoryBlockProps) => {
const { showDetail, handleShowDetail } = useShowDetail();
Expand All @@ -40,9 +42,17 @@ const StoryBlock = ({
onClick={() => handleShowDetail(!showDetail)}
>
{showDetail ? (
<ChevronDown width={16} height={16} fill="black" />
<ChevronDown
width={16}
height={16}
fill={taskExist ? "black" : "#C5C5C5"}
/>
) : (
<ChevronRight width={16} height={16} fill="black" />
<ChevronRight
width={16}
height={16}
fill={taskExist ? "black" : "#C5C5C5"}
/>
)}
</button>
<p>{title}</p>
Expand Down
162 changes: 135 additions & 27 deletions frontend/src/components/backlog/StoryCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,142 @@
import { ChangeEvent, FormEvent, useMemo, useState } from "react";
import Check from "../../assets/icons/check.svg?react";
import Closed from "../../assets/icons/closed.svg?react";
import CategoryChip from "./CategoryChip";
import { StoryForm } from "../../types/common/backlog";
import useStoryEmitEvent from "../../hooks/pages/backlog/useStoryEmitEvent";
import { Socket } from "socket.io-client";
import { useOutletContext } from "react-router-dom";
// import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import EpicDropdown from "./EpicDropdown";
import { EpicCategoryDTO } from "../../types/DTO/backlogDTO";
import useDropdownState from "../../hooks/common/dropdown/useDropdownState";

const StoryCreateForm = () => (
<div className="flex items-center gap-5 py-1 border-t border-b">
<div className="w-[5rem]">
<CategoryChip content="프로젝트" bgColor="green" />
</div>
<input className="w-[38.75rem]" type="text" />
<div className="flex items-center ">
<input className="w-14" type="number" id="point-number" />
<label htmlFor="point-number" className="">
POINT
</label>
</div>
<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"
>
<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"
interface StoryCreateFormProps {
onCloseClick: () => void;
epicList: EpicCategoryDTO[];
}

const StoryCreateForm = ({ onCloseClick, epicList }: StoryCreateFormProps) => {
const { socket }: { socket: Socket } = useOutletContext();
const [{ title, point, epicId, status }, setStoryFormData] =
useState<StoryForm>({
title: "",
point: undefined,
status: "시작전",
epicId: undefined,
});
const { open, handleClose, handleOpen, dropdownRef } = useDropdownState();
const { emitStoryCreateEvent } = useStoryEmitEvent(socket);

const handleTitleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const { value } = target;
setStoryFormData({ title: value, point, epicId, status });
};

const handlePointChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const { value } = target;
setStoryFormData({ title, point: Number(value), epicId, status });
};

const handleSubmit = (event: FormEvent) => {
event.preventDefault();
if (epicId === undefined) {
alert("에픽을 지정해주세요.");
return;
}

if (!title) {
alert("제목을 입력해주세요.");
return;
}

if (point === undefined) {
alert("포인트를 입력해주세요.");
return;
}

emitStoryCreateEvent({ title, status, epicId, point });
onCloseClick();
};

const handleEpicChange = (selectedEpicId: number) => {
setStoryFormData({ title, status, point, epicId: selectedEpicId });
handleClose();
};

const handleEpicColumnClick = () => {
if (!open) {
handleOpen();
}
};

const selectedEpic = useMemo(
() => epicList.filter(({ id }) => id === epicId)[0],
[epicId]
);
return (
<form
className="flex items-center w-full py-1 border-t border-b"
onSubmit={handleSubmit}
>
<div
className="w-[5rem] min-h-[1.75rem] bg-light-gray rounded-md mr-7 hover:cursor-pointer relative"
onClick={handleEpicColumnClick}
ref={dropdownRef}
>
<Closed stroke="white" />
</button>
</div>
</div>
);
{epicId && (
<CategoryChip
content={selectedEpic.name}
bgColor={selectedEpic.color}
/>
)}
{open && (
<EpicDropdown
selectedEpic={selectedEpic}
epicList={epicList}
onEpicSelect={handleEpicChange}
/>
)}
</div>
<input
className="w-[34.7rem] h-[1.75rem] mr-[1.5rem] bg-light-gray rounded-md focus:outline-none"
type="text"
value={title}
onChange={handleTitleChange}
/>
<div className="flex items-center mr-[2.8rem] ">
<input
className="w-24 h-[1.75rem] mr-1 text-right rounded-md bg-light-gray no-arrows focus:outline-none"
type="number"
id="point-number"
value={point}
onChange={handlePointChange}
/>
<label htmlFor="point-number" className="">
POINT
</label>
</div>
<div className="w-[4rem] mr-[2.76rem] text-right">
<span>0%</span>
</div>
<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>
</form>
);
};

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

const TaskCreateButton = () => (
<div className="py-1 border-b text-dark-gray">
<div className="py-1 text-dark-gray">
<button
className="flex items-center justify-center w-full gap-1"
type="button"
Expand Down
27 changes: 26 additions & 1 deletion frontend/src/hooks/pages/backlog/useBacklogSocket.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useEffect, useState } from "react";
import { Socket } from "socket.io-client";
import { BacklogDTO, EpicDTO } from "../../../types/DTO/backlogDTO";
import { BacklogDTO, EpicDTO, StoryDTO } from "../../../types/DTO/backlogDTO";
import {
BacklogSocketData,
BacklogSocketDomain,
BacklogSocketEpicAction,
BacklogSocketStoryAction,
} from "../../../types/common/backlog";

const useBacklogSocket = (socket: Socket) => {
Expand Down Expand Up @@ -46,6 +47,27 @@ const useBacklogSocket = (socket: Socket) => {
}
};

const handleStoryEvent = (
action: BacklogSocketStoryAction,
content: StoryDTO
) => {
switch (action) {
case BacklogSocketStoryAction.CREATE:
setBacklog((prevBacklog) => {
const newEpicList = prevBacklog.epicList.map((epic) => {
if (epic.id === content.epicId) {
const newStoryList = [...epic.storyList, content];
return { ...epic, storyList: newStoryList };
}

return epic;
});
return { epicList: newEpicList };
});
break;
}
};

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

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

const useStoryEmitEvent = (socket: Socket) => {
const emitStoryCreateEvent = (content: StoryForm) => {
socket.emit("story", { action: "create", content });
};

const emitStoryDeleteEvent = (content: { id: number }) => {
socket.emit("story", { action: "delete", content });
};

const emitStoryUpdateEvent = (content: {
id: number;
title?: string;
status?: BacklogStatusType;
epicId?: number;
point?: number;
}) => {
socket.emit("story", { action: "update", content });
};

return { emitStoryCreateEvent, emitStoryDeleteEvent, emitStoryUpdateEvent };
};

export default useStoryEmitEvent;
30 changes: 22 additions & 8 deletions frontend/src/pages/backlog/UnfinishedStoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,32 @@ const UnfinishedStoryPage = () => {
const { showDetail, handleShowDetail } = useShowDetail();
const storyList = useMemo(
() => changeEpicListToStoryList(backlog.epicList),
[]
[backlog.epicList]
);
const epicCategoryList = useMemo(
() => backlog.epicList.map(({ id, name, color }) => ({ id, name, color })),
[backlog.epicList]
);

return (
<div>
{...storyList.map(({ epic, title, point, status, taskList }) => (
<StoryBlock {...{ title, point, status }} epic={epic.name} progress={2}>
{...taskList.map((task) => <TaskBlock {...task} />)}
</StoryBlock>
))}
<div className="flex flex-col items-center gap-4">
<div className="w-full border-b">
{...storyList.map(({ epic, title, point, status, taskList }) => (
<StoryBlock
{...{ title, point, status }}
epic={epic.name}
progress={2}
taskExist={taskList.length > 0}
>
{...taskList.map((task) => <TaskBlock {...task} />)}
</StoryBlock>
))}
</div>
{showDetail ? (
<StoryCreateForm onCloseClick={() => handleShowDetail(false)} />
<StoryCreateForm
epicList={epicCategoryList}
onCloseClick={() => handleShowDetail(false)}
/>
) : (
<StoryCreateButton onClick={() => handleShowDetail(true)} />
)}
Expand Down
Loading

0 comments on commit c0af411

Please sign in to comment.