Skip to content
21 changes: 20 additions & 1 deletion src/components/common/modal/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { useState } from "react";
import { deleteCard } from "@/lib/apis/cardsApi";
import { TOKEN_1 } from "@/lib/constants/tokens";
import { useModalStore } from "@/lib/store/useModalStore";
import Image from "next/image";
import MenuButtonIcon from "../../../../public/icon/menu_icon.svg";
import { useTaskStore } from "@/lib/store/useTaskStore";

export default function MenuButton() {
const [isOpen, setIsOpen] = useState(false);
const { openModal, closeModal } = useModalStore();
const { selectedTaskId } = useTaskStore();

const openModifyModal = () => {
closeModal();
openModal("editTask");
};

const handleDelete = async () => {
if (!selectedTaskId) return;

await deleteCard({
token: TOKEN_1,
cardId: selectedTaskId,
});

closeModal();
window.location.reload();
};

return (
<div
onClick={() => setIsOpen((prev) => !prev)}
Expand All @@ -30,7 +46,10 @@ export default function MenuButton() {
>
수정하기
</button>
<button className="w-[81px] h-8 rounded font-normal text-md text-gray-800 hover:text-violet hover:bg-violet-8">
<button
onClick={handleDelete}
className="w-[81px] h-8 rounded font-normal text-md text-gray-800 hover:text-violet hover:bg-violet-8"
>
삭제하기
</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Modal({
<div className="flex justify-center items-center fixed top-0 left-0 w-full h-full p-6 bg-black/70 z-50">
<div
className={clsx(
"flex flex-col max-h-[80vh] px-4 rounded border-none bg-white",
"flex flex-col max-h-[92vh] px-4 rounded border-none bg-white",
isPage
? "gap-2 max-w-[327px] py-4 tablet:px-8 tablet:gap-6 tablet:max-w-[1200px] tablet:py-6"
: "gap-8 max-w-[327px] py-6 tablet:max-w-[1200px] tablet:p-8"
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/textarea/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface TextareaProps extends ComponentPropsWithoutRef<"textarea"> {
containerClassName: string;
labelClassName: string;
textareaClassName: string;
spanClassName: string;
spanClassName?: string;
}

const Textarea = ({
Expand Down
2 changes: 1 addition & 1 deletion src/components/modal/add-column/AddColumnModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function CreateDashboardModal() {
const buttonClick = async () => {
if (!dashboardId) return;

postColumn({
await postColumn({
token: TOKEN_1,
title: inputValue,
dashboardId: Number(dashboardId),
Expand Down
2 changes: 1 addition & 1 deletion src/components/modal/editColumn/EditColumnModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function CreateDashboardModal() {
const handleEditClick = async () => {
if (!selectedColumnId) return;

putColumn({
await putColumn({
token: TOKEN_1,
title: inputValue,
columnId: Number(selectedColumnId),
Expand Down
124 changes: 124 additions & 0 deletions src/components/modal/task-detail/CommentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useEffect, useState } from "react";
import { Comment } from "@/lib/types";
import { putComment, deleteComment } from "@/lib/apis/commentsApi";
import { TOKEN_1 } from "@/lib/constants/tokens";
import { useIsMobile } from "@/lib/hooks/useCheckViewport";
import { formatDate } from "@/lib/utils/dateUtils";
import UserIcon from "@/components/common/user-icon/UserIcon";
import Button from "@/components/common/button/Button";
import Textarea from "@/components/common/textarea/Textarea";

type CommentCardProps = Comment & {
onChange: () => void;
};

export default function CommentCard({
id,
content,
author,
createdAt,
onChange,
}: CommentCardProps) {
const [isEditMode, setIsEditMode] = useState(false);
const [inputValue, setInputValue] = useState(content);
const [isFormValid, setIsFormValid] = useState(false);
const isMobile = useIsMobile();
const date = formatDate(createdAt, true);

useEffect(() => {
const trimmedValue = inputValue.trim();
setIsFormValid(trimmedValue !== "");
}, [inputValue]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value);
};

const handleEditMode = () => {
setIsEditMode(!isEditMode);
};

const handleEditComment = async () => {
await putComment({
token: TOKEN_1,
content: inputValue.trim(),
commentId: id,
});

setInputValue("");
onChange();
};

const handleDeleteComment = () => {
deleteComment({
token: TOKEN_1,
commentId: id,
});

onChange();
};

return (
<div className=" flex gap-2 pb-2 border-b border-gray-400 tablet:gap-3 tablet:pb-3">
<div className="shrink-0">
<UserIcon
name={author.nickname}
img={author.profileImageUrl}
size={isMobile ? "sm" : "md"}
/>
</div>
<div className="flex flex-col gap-2 w-full tablet:gap-[10px] pc:gap-[6px]">
<div>
<div className="flex gap-2 items-center">
<div className="font-semibold text-xs text-gray-800 tablet:text-md">
{author.nickname}
</div>
<div className="font-normal text-[10px] text-gray-500 tablet:text-xs">
{date}
</div>
</div>
{isEditMode ? (
<div className="relative">
<Textarea
label=""
value={inputValue}
containerClassName="gap-1"
labelClassName="font-medium text-md tablet:text-lg"
textareaClassName="h-[70px] p-4 rounded-md text-xs tablet:h-[110px] tablet:text-md"
placeholder="댓글 작성하기"
onChange={handleChange}
/>
<Button
variant="whiteViolet"
radius="sm"
onClick={handleEditComment}
className="w-[84px] max-h-[28px] absolute bottom-3 right-3 font-medium text-xs leading-[18px] tablet:w-[78px] tablet:h-[32px]"
disabled={!isFormValid}
>
등록
</Button>
</div>
) : (
<div className="font-normal text-xs text-gray-800 tablet:text-md">
{content}
</div>
)}
</div>
<div className="flex gap-2 tablet:gap-3 pc:gap-[14px]">
<div
onClick={handleEditMode}
className="font-normal text-[10px] text-gray-500 underline cursor-pointer tablet:text-xs"
>
수정
</div>
<div
onClick={handleDeleteComment}
className="font-normal text-[10px] text-gray-500 underline cursor-pointer tablet:text-xs"
>
삭제
</div>
</div>
</div>
</div>
);
}
69 changes: 69 additions & 0 deletions src/components/modal/task-detail/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useEffect, useState, useRef } from "react";
import { useIntersection } from "@/lib/hooks/useIntersection";
import { Comment } from "@/lib/types";
import { fetchCommentList } from "@/lib/apis/commentsApi";
import { TOKEN_1 } from "@/lib/constants/tokens";
import CommentCard from "./CommentCard";

const PAGE_SIZE = 3;

export default function CommentList({
id,
onChange,
}: {
id: number;
onChange: () => void;
}) {
const [items, setItems] = useState<Comment[]>([]);
const [cursorId, setCursorId] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLast, setIsLast] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);

const handleLoad = async () => {
if (isLoading || isLast) return;
setIsLoading(true);

try {
const { comments: newComments, cursorId: nextCursorId } =
await fetchCommentList({
token: TOKEN_1,
size: PAGE_SIZE,
cursorId,
cardId: id,
});

setItems((prev) => [...prev, ...newComments]);
setCursorId(nextCursorId);

if (newComments.length < PAGE_SIZE || nextCursorId === null) {
setIsLast(true);
}
} finally {
setIsLoading(false);
}
};

useEffect(() => {
handleLoad();
}, []);

useIntersection({
target: observerRef,
onIntersect: handleLoad,
disabled: isLast,
});

return (
<div className="flex flex-col gap-2">
{items.map((item, index) => (
<div
key={item.id}
ref={index === items.length - 1 ? observerRef : null}
>
<CommentCard {...item} onChange={onChange} />
</div>
))}
</div>
);
}
76 changes: 76 additions & 0 deletions src/components/modal/task-detail/TaskCommentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useState } from "react";
import { useDashboardStore } from "@/lib/store/useDashboardStore";
import { postComment } from "@/lib/apis/commentsApi";
import { TOKEN_1 } from "@/lib/constants/tokens";
import Button from "@/components/common/button/Button";
import Textarea from "@/components/common/textarea/Textarea";
import CommentList from "./CommentList";

export default function TaskCommentSection({
cardId,
columnId,
}: {
cardId: number;
columnId: number;
}) {
const [inputValue, setInputValue] = useState("");
const [isFormValid, setIsFormValid] = useState(false);
const [commentListKey, setCommentListKey] = useState(0);
const { dashboardId } = useDashboardStore();

useEffect(() => {
const trimmedValue = inputValue.trim();
setIsFormValid(trimmedValue !== "");
}, [inputValue]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value);
};

const buttonClick = async () => {
if (!dashboardId) return;

await postComment({
token: TOKEN_1,
content: inputValue.trim(),
cardId: cardId,
columnId: columnId,
dashboardId: Number(dashboardId),
});

setInputValue("");
setCommentListKey((prev) => prev + 1); // key 값 변경해서 CommentList 다시 마운트
};

if (!dashboardId) return;

return (
<div className="flex flex-col gap-4 w-[290px] tablet:gap-6 tablet:w-[420px] pc:w-[445px]">
<div className="relative">
<Textarea
label="댓글"
value={inputValue}
containerClassName="gap-1"
labelClassName="font-medium text-md tablet:text-lg"
textareaClassName="h-[70px] p-4 rounded-md text-xs tablet:h-[110px] tablet:text-md"
placeholder="댓글 작성하기"
onChange={handleChange}
/>
<Button
variant="whiteViolet"
radius="sm"
onClick={buttonClick}
className="w-[84px] max-h-[28px] absolute bottom-3 right-3 font-medium text-xs leading-[18px] tablet:w-[78px] tablet:h-[32px]"
disabled={!isFormValid}
>
등록
</Button>
</div>
<CommentList
id={cardId}
key={commentListKey}
onChange={() => setCommentListKey((prev) => prev + 1)}
/>
</div>
);
}
18 changes: 12 additions & 6 deletions src/components/modal/task-detail/TaskDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TOKEN_1 } from "@/lib/constants/tokens";
import Modal from "@/components/common/modal/Modal";
import TaskInfoSection from "./TaskInfoSection";
import TaskContentSection from "./TaskContentSection";
import TaskCommentSection from "./TaskCommentSection";

export default function TaskDetailModal() {
const { selectedTaskId } = useTaskStore();
Expand All @@ -28,10 +29,16 @@ export default function TaskDetailModal() {
if (!selectedTaskId) return;
if (!data) return;

const { id, title, description, tags, dueDate, assignee, imageUrl } = data;

// vercel 배포를 위해 임시로 작성한 코드
console.log(`${id} 값은 코멘트 api 요청할 때 사용하시면 됩니다!`);
const {
id,
title,
description,
tags,
dueDate,
assignee,
imageUrl,
columnId,
} = data;

return (
<Modal taskTitle={title}>
Expand All @@ -44,8 +51,7 @@ export default function TaskDetailModal() {
tags={tags}
imageUrl={imageUrl}
/>
{/* 코멘트 부분 여기에 추가해주시면 됩니다 */}
<div></div>
<TaskCommentSection cardId={id} columnId={columnId} />
</div>
</div>
</Modal>
Expand Down
Loading