From 0f456ecda61713390935c65018021e78c82426fc Mon Sep 17 00:00:00 2001
From: litae <109706689+qkdflrgs@users.noreply.github.com>
Date: Thu, 10 Aug 2023 20:24:52 +0900
Subject: [PATCH] =?UTF-8?q?[FE]=20refactor/#146:=203=EC=A3=BC=EC=B0=A8=20?=
=?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=ED=8C=A9?=
=?UTF-8?q?=ED=86=A0=EB=A7=81=20(#147)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat/#146: 코멘트 컴포넌트 구현
* feat/#146: 시간 차이를 구하는 함수 구현
* refactor/#146: Alert 컴포넌트 리팩토링
* refactor/#146: input 컴포넌트 리팩토링
* refactor/#146: TextInput 컴포넌트 리팩토링
* refactor/#146: 드롭다운 인디케이터 리팩토링
* refactor/#146: 드롭다운 아이템 컴포넌트 리팩토링
* refactor/#146: 드롭다운 패널 컴포넌트 리팩토링
* refactor/#146: 이슈 리스트 컴포넌트 리팩토링
* refactor/#146: 메인 페이지 리팩토링
* refactor/#146: 스타일 object 수정
* feat/#146: 마일스톤 상세 페이지(작업중)
---
FE/src/components/Alert/Alert.tsx | 32 +-
FE/src/components/Comment/Comment.tsx | 317 ++++++++++++++++++
.../DropdownIndicator/DropdownIndicator.tsx | 46 +--
.../components/DropdownPanel/DropdownItem.tsx | 27 +-
.../DropdownPanel/DropdownPanel.tsx | 8 +-
FE/src/components/IssueList/IssueList.tsx | 10 +-
FE/src/components/common/Input/Input.tsx | 3 +
.../components/common/TextInput/TextInput.tsx | 10 +-
FE/src/pages/MainPage.tsx | 53 ++-
FE/src/pages/MilestonesPage.tsx | 90 +++++
FE/src/styles/base/Object.ts | 2 +-
FE/src/utils/calculateTime.ts | 18 +
12 files changed, 524 insertions(+), 92 deletions(-)
create mode 100644 FE/src/components/Comment/Comment.tsx
create mode 100644 FE/src/pages/MilestonesPage.tsx
create mode 100644 FE/src/utils/calculateTime.ts
diff --git a/FE/src/components/Alert/Alert.tsx b/FE/src/components/Alert/Alert.tsx
index febddcdd0..13277aa95 100644
--- a/FE/src/components/Alert/Alert.tsx
+++ b/FE/src/components/Alert/Alert.tsx
@@ -1,10 +1,24 @@
import { styled } from "styled-components";
+import Button from "../common/Button/Button";
-export default function Alert() {
+type Props = {
+ onClickCancel(): void;
+ onClickActive(): void;
+};
+
+export default function Alert({ onClickCancel, onClickActive }: Props) {
return (
-
-
+ 정말 삭제하시겠습니까?
+
+
+
+
);
}
@@ -12,11 +26,21 @@ export default function Alert() {
const Container = styled.div`
display: flex;
flex-direction: column;
+ justify-content: space-between;
padding: 32px;
width: 424px;
height: 180px;
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ border: ${({ theme }) =>
+ `${theme.border.default} ${theme.colorSystem.neutral.border.default}`};
+ border-radius: ${({ theme }) => theme.radius.large};
+ box-shadow: ${({ theme }) => theme.dropShadow.lightMode};
`;
const AlertContent = styled.p``;
-const ButtonTap = styled.div``;
+const ButtonTap = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-around;
+`;
diff --git a/FE/src/components/Comment/Comment.tsx b/FE/src/components/Comment/Comment.tsx
new file mode 100644
index 000000000..a5bbd2a11
--- /dev/null
+++ b/FE/src/components/Comment/Comment.tsx
@@ -0,0 +1,317 @@
+import { styled } from "styled-components";
+import { AssigneesList } from "../../type";
+import UserProfileButton from "../UserProfileButton/UserProfileButton";
+import { calculateTime } from "../../utils/calculateTime";
+import Button from "../common/Button/Button";
+import { useEffect, useState } from "react";
+
+type Props = {
+ id: number;
+ commentId?: number;
+ type?: "content" | "comment";
+ author: AssigneesList;
+ createAt: string;
+ content: string;
+};
+
+export default function Comment({
+ id,
+ commentId,
+ type = "comment",
+ author,
+ createAt,
+ content,
+}: Props) {
+ const [isEdit, setIsEdit] = useState(false);
+ const [commentContent, setCommentContent] = useState(content);
+ const [showCounter, setShowCounter] = useState(false);
+
+ const editComment = () => {
+ setIsEdit(true);
+ };
+
+ const cancelEditComment = () => {
+ setCommentContent(content);
+ setIsEdit(false);
+ };
+
+ const handleCommentInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ const { target } = e;
+ target.style.height = "auto";
+ target.style.height = `${target.scrollHeight}px`;
+ setCommentContent(e.target.value);
+ };
+
+ const updateContent = async () => {
+ const URL = `http://3.34.141.196/api/issues/${id}/content`; // PATCH 요청을 보낼 리소스 URL로 변경
+
+ const patchData = {
+ content: commentContent,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ try {
+ const response = await fetch(URL, {
+ method: "PATCH",
+ headers: headers,
+ body: JSON.stringify(patchData),
+ });
+
+ if (response.status === 204) {
+ window.location.reload();
+ } else {
+ console.log("PATCH 요청에 실패하였습니다. 상태 코드:", response.status);
+ }
+ } catch (error) {
+ console.error("API 호출 오류:", error);
+ }
+ };
+
+ const updateComment = async () => {
+ const URL = `http://3.34.141.196/api/issues/${id}/comments/${commentId}`; // PATCH 요청을 보낼 리소스 URL로 변경
+
+ const patchData = {
+ content: commentContent,
+ };
+
+ const headers = {
+ "Content-Type": "application/json",
+ };
+
+ try {
+ const response = await fetch(URL, {
+ method: "PATCH",
+ headers: headers,
+ body: JSON.stringify(patchData),
+ });
+
+ if (response.status === 204) {
+ window.location.reload();
+ } else {
+ console.log("PATCH 요청에 실패하였습니다. 상태 코드:", response.status);
+ }
+ } catch (error) {
+ console.error("API 호출 오류:", error);
+ }
+ };
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout | null = null;
+
+ if (commentContent) {
+ setShowCounter(true);
+ timer = setTimeout(() => {
+ setShowCounter(false);
+ }, 2000);
+ }
+
+ return () => {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ };
+ }, [commentContent]);
+
+ return (
+
+
+
+
+
+ {author.nickname}
+
+
+
+
+
+
+ {!isEdit && (
+
+
+
+ )}
+ {isEdit && (
+
+
+
+
+
+ {showCounter && (
+ 띄어쓰기 포함 {commentContent.length}자
+ )}
+
+
+
+
+
+
+ )}
+
+ {isEdit && (
+
+
+
+
+ )}
+
+ );
+}
+
+const Container = styled.li`
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ width: 100%;
+ height: min-content;
+`;
+
+const Header = styled.div`
+ padding: 16px 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ height: 64px;
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+`;
+
+const Info = styled.div`
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ height: 32px;
+ font: ${({ theme }) => theme.font.displayMedium16};
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.default};
+`;
+
+const UserName = styled.span`
+ color: ${({ theme }) => theme.colorSystem.neutral.text.default};
+`;
+
+const Time = styled.span`
+ color: ${({ theme }) => theme.colorSystem.neutral.text.weak};
+`;
+
+const ButtonTap = styled.div`
+ display: flex;
+ gap: 16px;
+`;
+
+const ContentWrapper = styled.div<{ $isEdit: boolean }>`
+ position: relative;
+ padding: ${({ $isEdit }) => ($isEdit ? "16px" : "16px 24px 24px 24px")};
+ width: 100%;
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ border-bottom-left-radius: inherit;
+ border-bottom-right-radius: inherit;
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 1px;
+ background-color: ${({ theme }) =>
+ theme.colorSystem.neutral.border.default};
+ }
+`;
+
+const Content = styled.textarea`
+ padding: 0px;
+ width: 100%;
+ min-height: 24px;
+ resize: none;
+ border: none;
+ outline: none;
+ overflow: hidden;
+ font: ${({ theme }) => theme.font.displayMedium16};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.default};
+ background-color: transparent;
+`;
+
+const CommentWrapper = styled.div<{ $isEdit: boolean }>`
+ display: flex;
+ flex-direction: column;
+ border: ${({ $isEdit, theme }) =>
+ $isEdit
+ ? `${theme.border.default} ${theme.colorSystem.neutral.border.defaultActive}`
+ : `${theme.border.default} ${theme.colorSystem.neutral.border.default}`};
+ border-radius: ${({ theme }) => theme.radius.large};
+`;
+
+const EditButtons = styled.div`
+ display: flex;
+ gap: 16px;
+ justify-content: end;
+ width: 100%;
+`;
+
+const CountWrapper = styled.div`
+ display: flex;
+ justify-content: end;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ height: 52px;
+ padding: 16px;
+`;
+
+const Counter = styled.span`
+ font: ${({ theme }) => theme.font.displayMedium12};
+ color: ${({ theme }) => theme.colorSystem.neutral.text.weak};
+`;
+
+const IconImg = styled.img`
+ width: 20px;
+ height: 20px;
+`;
+
+const AddFileWrapper = styled.div`
+ padding: 10px 16px;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 52px;
+ border-top: ${({ theme }) =>
+ `${theme.border.dash} ${theme.colorSystem.neutral.border.default}`};
+`;
+
+const AddFileInput = styled.input``;
+
+const EditWrapper = styled.div`
+ background-color: ${({ theme }) => theme.colorSystem.neutral.surface.strong};
+ border-bottom-left-radius: inherit;
+ border-bottom-right-radius: inherit;
+`;
diff --git a/FE/src/components/DropdownIndicator/DropdownIndicator.tsx b/FE/src/components/DropdownIndicator/DropdownIndicator.tsx
index bdde9052e..1ef85a102 100644
--- a/FE/src/components/DropdownIndicator/DropdownIndicator.tsx
+++ b/FE/src/components/DropdownIndicator/DropdownIndicator.tsx
@@ -1,7 +1,4 @@
import { styled } from "styled-components";
-import DropdownPanel from "../DropdownPanel/DropdownPanel";
-import { useEffect, useState } from "react";
-import { AssigneesProps } from "../../type";
type Props = {
icon?: string;
@@ -9,7 +6,6 @@ type Props = {
padding?: string;
width?: string;
height?: string;
- hasDropdown?: boolean;
};
export default function DropdownIndicator({
@@ -18,51 +14,11 @@ export default function DropdownIndicator({
padding = "4px 0px",
width = "80px",
height = "32px",
- hasDropdown = false,
}: Props) {
- const [isOpen, setIsOpen] = useState(false);
- const [assigneesData, setAssigneesData] = useState();
-
- const openDropdown = () => {
- setIsOpen(true);
- };
-
- const closeDropdown = () => {
- setIsOpen(false);
- };
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- const response = await fetch(
- "http://3.34.141.196/api/issues/assignees",
- );
- const data = await response.json();
- setAssigneesData(data);
- } catch (error) {
- console.log("error");
- }
- };
-
- fetchData();
- }, []);
-
return (
-
+
{label}
- {hasDropdown && isOpen && (
-
- )}
);
}
diff --git a/FE/src/components/DropdownPanel/DropdownItem.tsx b/FE/src/components/DropdownPanel/DropdownItem.tsx
index 32a83cd47..49e48d061 100644
--- a/FE/src/components/DropdownPanel/DropdownItem.tsx
+++ b/FE/src/components/DropdownPanel/DropdownItem.tsx
@@ -1,38 +1,27 @@
import { styled } from "styled-components";
import UserProfileButton from "../UserProfileButton/UserProfileButton";
-import { useNavigate, useParams } from "react-router-dom";
type Props = {
id: number;
userImg?: string;
itemName: string;
- closeDropdown(): void;
+ onClick(): void;
};
export default function DropdownItem({
id,
userImg = "/logo/profile.jpg",
itemName,
- closeDropdown,
+ onClick,
}: Props) {
- const navigate = useNavigate();
- const { filter } = useParams();
- const checkAssignee = () => {
- navigate(`/issues/${filter}&assigneeIds=${id}`);
- closeDropdown();
- };
-
return (
-
+
- {userImg && (
-
- )}
-
+ {userImg && }
+
diff --git a/FE/src/components/DropdownPanel/DropdownPanel.tsx b/FE/src/components/DropdownPanel/DropdownPanel.tsx
index b50f0e460..8ab323740 100644
--- a/FE/src/components/DropdownPanel/DropdownPanel.tsx
+++ b/FE/src/components/DropdownPanel/DropdownPanel.tsx
@@ -19,13 +19,13 @@ export default function DropdownPanel({
{title}
{assigneesList &&
- assigneesList.assignees.map((assignee, key) => (
+ assigneesList.assignees.map((assignee) => (
))}
diff --git a/FE/src/components/IssueList/IssueList.tsx b/FE/src/components/IssueList/IssueList.tsx
index 6f791b025..e7ad782a9 100644
--- a/FE/src/components/IssueList/IssueList.tsx
+++ b/FE/src/components/IssueList/IssueList.tsx
@@ -2,6 +2,7 @@ import { styled } from "styled-components";
import Button from "../common/Button/Button";
import { IssueListProps } from "../../type";
import LabelItem from "../SideBar/LabelItem";
+import { calculateTime } from "../../utils/calculateTime";
type Props = {
issue: IssueListProps;
@@ -18,7 +19,9 @@ export default function IssueList({ issue }: Props) {
- {issue.title}
+
+ {issue.title}
+
{issue.labels &&
issue.labels.map((label, key) => (
#{issue.id}
- 이 이슈가 8분 전, {issue.author}님에 의해 작성되었습니다
+ 이 이슈가 {calculateTime(issue.createAt)}, {issue.author}
+ 님에 의해 작성되었습니다
{issue.milestone !== null && (