Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 스토리별 백로그 조회, 스토리 추가 기능 구현 #306

Merged
merged 5 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
89 changes: 62 additions & 27 deletions frontend/src/components/backlog/StoryBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
import useShowDetail from "../../hooks/pages/backlog/useShowDetail";
import { BacklogStatusType } from "../../types/DTO/backlogDTO";
import BacklogStatusChip from "./BacklogStatusChip";
import CategoryChip from "./CategoryChip";
import TaskCreateButton from "./TaskCreateButton";
import ChevronDown from "../../assets/icons/chevron-down.svg?react";
import ChevronRight from "../../assets/icons/chevron-right.svg?react";
import TaskContainer from "./TaskContainer";
import TaskHeader from "./TaskHeader";

interface StoryBlockProps {
epic: string;
title: string;
point: number;
point: number | null;
progress: number;
status: BacklogStatusType;
children: React.ReactNode;
taskExist: boolean;
}

const StoryBlock = ({
Expand All @@ -17,30 +24,58 @@ const StoryBlock = ({
point,
progress,
status,
}: StoryBlockProps) => (
<div className="flex items-center gap-5 py-1 border-t border-b">
<div className="w-[5rem]">
<CategoryChip content={epic} bgColor="green" />
</div>
<div className="flex items-center gap-1 w-[38.75rem]">
<button
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
type="button"
>
<ChevronDown width={16} height={16} fill="black" />
</button>
<p>{title}</p>
</div>
<div className="w-[4rem] text-right">
<p className="">{point} POINT</p>
</div>
<div className="w-[4rem] text-right">
<span>{progress}%</span>
</div>
<div className="w-[4rem]">
<BacklogStatusChip status={status} />
</div>
</div>
);
taskExist,
children,
}: StoryBlockProps) => {
const { showDetail, handleShowDetail } = useShowDetail();

return (
<>
<div className="flex items-center py-1 border-t border-b">
<div className="w-[5rem] mr-5">
<CategoryChip content={epic} bgColor="green" />
</div>
<div className="flex items-center gap-1 w-[40.9rem] mr-4">
<button
className="flex items-center justify-center w-5 h-5 rounded-md hover:bg-dark-gray hover:bg-opacity-20"
type="button"
onClick={() => handleShowDetail(!showDetail)}
>
{showDetail ? (
<ChevronDown
width={16}
height={16}
fill={taskExist ? "black" : "#C5C5C5"}
/>
) : (
<ChevronRight
width={16}
height={16}
fill={taskExist ? "black" : "#C5C5C5"}
/>
)}
</button>
<p>{title}</p>
</div>
<div className="w-[4rem] mr-[2.76rem] text-right">
<p className="">{point} POINT</p>
</div>
<div className="w-[4rem] mr-[2.76rem] text-right">
<span>{progress}%</span>
</div>
<div className="w-[6.25rem]">
<BacklogStatusChip status={status} />
</div>
</div>
{showDetail && (
<TaskContainer>
<TaskHeader />
{children}
<TaskCreateButton />
</TaskContainer>
)}
</>
);
};

export default StoryBlock;
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/TaskBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const TaskBlock = ({
status,
}: TaskDTO) => (
<div className="flex items-center justify-between py-1 border-b">
<p className="w-12">Task-{displayId}</p>
<p className="w-[4rem]">Task-{displayId}</p>
<p className="w-[25rem]">{title}</p>
<div className="w-12">
{assignedMemberId && (
Expand Down
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
2 changes: 1 addition & 1 deletion frontend/src/components/backlog/TaskHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const TaskHeader = () => (
<div className="flex items-center justify-between py-1 border-b text-dark-gray">
<p className="w-12">식별자</p>
<p className="w-[4rem]">식별자</p>
<p className="w-[25rem]">태스크 이름</p>
<p className="w-12">담당자</p>
<p className="w-16">예상 시간</p>
Expand Down
Loading
Loading