Skip to content

Commit

Permalink
Merge pull request #136 from codesquad-team4-issue-tracker/feat/#121-…
Browse files Browse the repository at this point in the history
…crud

[FE] 레이블, 마일스톤 CRUD 및 이슈 코멘트 삭제 기능 구현
  • Loading branch information
aaaz425 authored Aug 18, 2023
2 parents 58d0e1b + a768782 commit c47dcd4
Show file tree
Hide file tree
Showing 29 changed files with 1,873 additions and 154 deletions.
6 changes: 4 additions & 2 deletions fe/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import LabelList from './components/Label/LabelList';
import MilestoneList from './components/Milestone/MilestoneList';
import UserProvider from './components/Context/UserContext';
import ApiTest from './components/ApiTest';
import Footer from './components/Footer/Footer';

export default function App() {
const location = useLocation();
const hiddenHeaderRoutes = ['/sign-in', '/sign-up'];
const shouldShowHeader = !hiddenHeaderRoutes.includes(location.pathname);
const withHeaderAndFooter = !hiddenHeaderRoutes.includes(location.pathname);

return (
<UserProvider>
{shouldShowHeader && <Header />}
{withHeaderAndFooter && <Header />}
<Routes>
<Route path="/" element={<IssueList />} />
<Route path="/sign-in" element={<SignIn />} />
Expand All @@ -29,6 +30,7 @@ export default function App() {
<Route path="/milestone" element={<MilestoneList />} />
<Route path="/test" element={<ApiTest />} />
</Routes>
{withHeaderAndFooter && <Footer />}
</UserProvider>
);
}
36 changes: 36 additions & 0 deletions fe/src/Hook/useDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MutableRefObject, useEffect, useRef, useState } from 'react';

type LabelDropdownReturnType = [
boolean,
MutableRefObject<HTMLDivElement | null>,
() => void,
];

export default function useDropdown(
initialState: boolean
): LabelDropdownReturnType {
const [isOpen, setIsOpen] = useState(initialState);
const ref = useRef<HTMLDivElement | null>(null);

const onSetOpenState = () => {
setIsOpen((prev) => !prev);
};

useEffect(() => {
const onClick = (e: MouseEvent) => {
if (ref.current !== null && !ref.current.contains(e.target as Node)) {
setIsOpen(false);
}
};

if (isOpen) {
window.addEventListener('click', onClick);
}

return () => {
window.removeEventListener('click', onClick);
};
}, [isOpen]);

return [isOpen, ref, onSetOpenState];
}
File renamed without changes.
122 changes: 90 additions & 32 deletions fe/src/components/ApiTest.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,111 @@
import { css } from '@emotion/react';
import { customFetch } from '../util/customFetch';
import {
GetAssigneesRes,
GetLabelsRes,
GetMilestonesRes,
GetWritersRes,
OnlySuccessRes,
} from '../type/Response.type';

export default function ApiTest() {
const GetAssignee = () => {};
const getAssignees = async () => {
const response = await customFetch<GetAssigneesRes>({
subUrl: 'api/members/show-content',
});

console.log(response.success);
};

const getLabels = async () => {
const response = await customFetch<GetLabelsRes>({
subUrl: 'api/labels/show-content',
});

console.log(response.success);
};

const getMilestones = async () => {
const response = await customFetch<GetMilestonesRes>({
subUrl: 'api/milestones/show-content',
});

console.log(response.success);
};

const getWriters = async () => {
const response = await customFetch<GetWritersRes>({
subUrl: 'api/members/show-content',
});

console.log(response.success);
};

const patchAssignees = async () => {
const issueId = 6;
const assigneeId = 1;
const response = await customFetch<OnlySuccessRes>({
method: 'PATCH',
subUrl: `api/issues/${issueId}/assignees`,
body: JSON.stringify({
ids: [assigneeId],
}),
});

console.log(response.success);
};

const patchLabels = async () => {
const issueId = 6;
const labelId = 1;
const response = await customFetch<OnlySuccessRes>({
method: 'PATCH',
subUrl: `api/issues/${issueId}/labels`,
body: JSON.stringify({
ids: [labelId],
}),
});

console.log(response.success);
};

const patchMilestones = async () => {
const issueId = 6;
const milestoneId = 1;
const response = await customFetch<OnlySuccessRes>({
method: 'PATCH',
subUrl: `api/issues/${issueId}/milestones/${milestoneId}`,
});

console.log(response.success);
};

const on = () => {};
return (
<div css={test}>
<button type="button" onClick={GetAssignee}>
<button type="button" onClick={getAssignees}>
GET 담당자를 불러온다
</button>
<button type="button" onClick={on}>
<button type="button" onClick={getLabels}>
GET 레이블을 불러온다
</button>
<button type="button" onClick={on}>
<button type="button" onClick={getMilestones}>
GET 마일스톤을 불러온다
</button>
<button type="button" onClick={on}>
<button type="button" onClick={getWriters}>
GET 작성자를 불러온다
</button>
<button type="button" onClick={on}>
POST 이슈의 담당자를 수정한다
<button type="button" onClick={patchAssignees}>
PATCH 이슈의 담당자를 수정한다
</button>
<button type="button" onClick={on}>
POST 이슈 레이블을 수정한다
<button type="button" onClick={patchLabels}>
PATCH 이슈 레이블을 수정한다
</button>
<button type="button" onClick={on}>
POST 이슈 마일스톤을 수정한다
<button type="button" onClick={patchMilestones}>
PATCH 이슈 마일스톤을 수정한다
</button>
<button type="button" onClick={on}>
{/* <button type="button" onClick={deleteComments}>
DELETE 이슈 상세페이지의 코멘트를 삭제한다
</button>
<button type="button" onClick={on}>
POST 레이블을 추가한다
</button>
<button type="button" onClick={on}>
PUT 레이블을 수정한다
</button>
<button type="button" onClick={on}>
DELETE 레이블을 삭제한다
</button>
<button type="button" onClick={on}>
POST 마일스톤을 추가한다
</button>
<button type="button" onClick={on}>
PUT 마일스톤을 수정한다
</button>
<button type="button" onClick={on}>
DELETE 마일스톤을 삭제한다
</button>
</button> */}
</div>
);
}
Expand Down
59 changes: 59 additions & 0 deletions fe/src/components/Context/LabelContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useState,
} from 'react';

type LabelProviderProps = {
children: ReactNode;
};

export const LabelContext = createContext<{
title: string;
description: string;
backgroundColor: string;
textColor: string;
setTitle: Dispatch<SetStateAction<string>>;
setDescription: Dispatch<SetStateAction<string>>;
setBackgroundColor: Dispatch<SetStateAction<string>>;
onSelectTextColor: (color: string) => void;
} | null>(null);

export function LabelProvider({ children }: LabelProviderProps) {
const [title, setTitle] = useState('');
const [textColor, setTextColor] = useState('white');
const [backgroundColor, setBackgroundColor] = useState('#000000');
const [description, setDescription] = useState('');

const onSelectTextColor = (color: string) => {
switch (color) {
case 'white':
setTextColor('white');
break;
case 'black':
setTextColor('black');
break;
default:
break;
}
};

return (
<LabelContext.Provider
value={{
title,
description,
backgroundColor,
textColor,
setTitle,
setDescription,
setBackgroundColor,
onSelectTextColor,
}}
>
{children}
</LabelContext.Provider>
);
}
9 changes: 9 additions & 0 deletions fe/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { css } from '@emotion/react';

export default function Footer() {
return <footer css={footer} />;
}

const footer = css`
height: 94px;
`;
2 changes: 1 addition & 1 deletion fe/src/components/Issue/IssueCreate/IssueCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function IssueCreate() {
}
} catch (error) {
//Memo: 에러 핸들링 필요
console.log(error);
console.error(error);
}
};

Expand Down
38 changes: 31 additions & 7 deletions fe/src/components/Issue/IssueDetail/Comment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useId, useState } from 'react';
import { useContext, useState } from 'react';
import { Theme, css, useTheme } from '@emotion/react';
import { border, font, radius } from '../../../styles/styles';
import { ReactComponent as EditIcon } from '../../../assets/icon/edit.svg';
Expand All @@ -19,11 +19,11 @@ import { OnlySuccessRes } from '../../../type/Response.type';
type Props = {
commentData: CommentType;
issueId: number;
onDelete: (commentId: number) => void;
};

export default function Comment({ commentData, issueId }: Props) {
export default function Comment({ commentData, issueId, onDelete }: Props) {
const theme = useTheme();
const id = useId();
const [isEditing, setIsEditing] = useState(false);
const [editedComment, setEditedComment] = useState(commentData.content);
const { ...context } = useContext(UserContext);
Expand Down Expand Up @@ -54,13 +54,33 @@ export default function Comment({ commentData, issueId }: Props) {
}
};

const onDeleteComment = async () => {
try {
const response = await customFetch<OnlySuccessRes>({
method: 'DELETE',
subUrl: `api/issues/${issueId}/comments/${commentData.id}`,
});

if (response.success) {
onDelete(commentData.id);
}
} catch (error) {
console.error(error);
}
};

const onAddImg = (imgMarkdown: string) => {
setEditedComment((prev) => `${prev}\n${imgMarkdown}\n`);
};

const userIdString = context.user?.member.id;
const isWriter = commentData.writer.id === Number(userIdString)!;
const isEdited = commentData.content !== editedComment;
const timeLine = getTimeLine(
new Date(
getKoreanTime(commentData.createdAt).getTime() + 9 * 60 * 60 * 1000
)
);

return (
<div css={comment(theme)}>
Expand All @@ -69,19 +89,23 @@ export default function Comment({ commentData, issueId }: Props) {
<div className="writer-info">
<UserImageIcon size="M" url={commentData.writer.imageUrl} />
<div className="writer">{commentData.writer.name}</div>
<div className="time-line">
{getTimeLine(getKoreanTime(commentData.createdAt))}
</div>
<div className="time-line">{timeLine}</div>
</div>
<div className="buttons">
{isWriter && (
<>
<Label
id={id}
textColor={theme.neutral.textWeak}
backgroundColor="inherit"
title="작성자"
/>
<Button
icon={<EditIcon />}
size="XS"
color={theme.danger.textDefault}
value="삭제"
onClick={onDeleteComment}
/>
<Button
icon={<EditIcon />}
size="XS"
Expand Down
12 changes: 12 additions & 0 deletions fe/src/components/Issue/IssueDetail/IssueDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ export default function IssueDetail() {
}
};

const onDeleteComment = (commentId: number) => {
setDetailIssue((prev) => {
if (prev) {
return {
...prev,
comments: prev.comments.filter((comment) => comment.id !== commentId),
};
}
});
};

const onAddImg = (imgMarkdown: string) => {
setContent((prev) => `${prev}\n${imgMarkdown}\n`);
};
Expand All @@ -123,6 +134,7 @@ export default function IssueDetail() {
key={comment.id}
issueId={detailIssue.id}
commentData={comment}
onDelete={onDeleteComment}
/>
))}
<IssueContent
Expand Down
Loading

0 comments on commit c47dcd4

Please sign in to comment.