diff --git a/src/api/task/get-task-detail.ts b/src/api/task/get-task-detail.ts index f6bfe2fe..fb5cb2c4 100644 --- a/src/api/task/get-task-detail.ts +++ b/src/api/task/get-task-detail.ts @@ -1,11 +1,38 @@ import instance from "@/utils/axios"; +import { notFound } from "next/navigation"; + +export const fetchTaskDetail = async ( + groupId: number, + taskListId: number, + taskId: number, + token: string +) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/groups/${groupId}/task-lists/${taskListId}/tasks/${taskId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response) return notFound(); + + return response.json(); + } catch (error) { + console.error(error); + } +}; /** * @author hwitae * @description 할 일 상세 정보를 조회합니다. * @param taskId 할 일 ID */ -const getTaskDetail = async ( +export const getTaskDetail = async ( groupId: number, taskListId: number, taskId: number @@ -22,5 +49,3 @@ const getTaskDetail = async ( console.error(error); } }; - -export default getTaskDetail; diff --git a/src/api/task/get-task-list.ts b/src/api/task/get-task-list.ts index 43bfd266..6f20036c 100644 --- a/src/api/task/get-task-list.ts +++ b/src/api/task/get-task-list.ts @@ -1,7 +1,31 @@ import { TaskList } from "@/types/taskList"; import instance from "@/utils/axios"; -const getTaskList = async ( +export const fetchTaskList = async ( + groupId: number, + taskListId: number, + token: string +) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/groups/${groupId}/task-lists/${taskListId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + return response.json(); + } catch (error) { + console.error(error); + throw error; + } +}; + +export const getTaskList = async ( groupId: number, taskListId: number, date?: string @@ -15,5 +39,3 @@ const getTaskList = async ( throw error; } }; - -export default getTaskList; diff --git a/src/app/[groupId]/tasklist/@task/_components/task-detail-contents.tsx b/src/app/[groupId]/tasklist/@task/_components/task-detail-contents.tsx index 44c7df1b..9fe4cb21 100644 --- a/src/app/[groupId]/tasklist/@task/_components/task-detail-contents.tsx +++ b/src/app/[groupId]/tasklist/@task/_components/task-detail-contents.tsx @@ -4,7 +4,7 @@ import { Icon, Profile, TaskDetailContentSkeleton } from "@/components"; import ICONS_MAP from "@/components/icon/icons-map"; import usePatchTaskDetail from "@/hooks/api/task/use-patch-task-detail"; import { Writer } from "@/types/user"; -import { ChangeEvent, useEffect, useRef, useState } from "react"; +import { ChangeEvent, useEffect, useRef } from "react"; import TextareaAutosize from "react-textarea-autosize"; import TaskDetailToggleBtn from "./task-detail-complete-btn"; import { toKoreanDateWithTimeString } from "@/utils/date-util"; @@ -46,7 +46,6 @@ const TaskDetailContents = ({ frequency, isPending, }: TaskDetailContentsProps) => { - const [text, setText] = useState(); const timer = useRef(null); const newDescription = useRef(description); const newName = useRef(name); @@ -67,12 +66,10 @@ const TaskDetailContents = ({ const handleDescriptionChange = (e: ChangeEvent) => { newDescription.current = e.target.value.trim(); - setText(e.target.value); }; const handleNameChange = (e: ChangeEvent) => { newName.current = e.target.value.trim(); - setText(e.target.value); }; const handleToggleBtnClick = () => { diff --git a/src/app/[groupId]/tasklist/@task/_components/task-detail.tsx b/src/app/[groupId]/tasklist/@task/_components/task-detail.tsx new file mode 100644 index 00000000..6da8b892 --- /dev/null +++ b/src/app/[groupId]/tasklist/@task/_components/task-detail.tsx @@ -0,0 +1,116 @@ +"use client"; + +import TaskDetailComment from "../_components/task-detail-comment"; +import TaskDetailContents from "../_components/task-detail-contents"; +import { Icon } from "@/components"; +import TaskDetailWrapper from "../_components/task-detail-wrapper"; +import { + notFound, + useParams, + usePathname, + useRouter, + useSearchParams, +} from "next/navigation"; +import useGetTaskDetail from "@/hooks/api/task/use-get-task-detail"; +import { AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; +import cn from "@/utils/clsx"; +import TaskDetailInputReply from "../_components/task-detail-input-reply"; + +const pageVariants = { + initial: { + x: "100%", + opacity: 0, + }, + visible: { + x: 0, + opacity: 1, + transition: { + type: "tween", + ease: "easeOut", + duration: 0.2, + } as const, + }, + exit: { + x: "100%", + opacity: 0, + transition: { + type: "tween", + ease: "easeIn", + duration: 0.2, + } as const, + }, +}; + +const TaskDetail = () => { + const router = useRouter(); + const param = useParams(); + const pathName = usePathname(); + const searchParam = useSearchParams(); + const taskId = Number(searchParam.get("task")); + const taskListId = Number(searchParam.get("list")); + const groupId = Number(param.groupId); + + const handleClose = () => { + router.push(`${pathName}?list=${taskListId}`); + }; + + const { data: taskDetailData, isPending } = useGetTaskDetail( + groupId, + taskListId, + taskId + ); + + if (taskId) { + if (!taskDetailData) notFound(); + } + + return ( + + {taskId ? ( + + +
+
+ +
+
+ + + {taskDetailData?.commentCount || 0} + +
+ +
+
+ +
+
+
+ ) : null} +
+ ); +}; + +export default TaskDetail; diff --git a/src/app/[groupId]/tasklist/@task/page.tsx b/src/app/[groupId]/tasklist/@task/page.tsx index 8365ff7e..28d379ce 100644 --- a/src/app/[groupId]/tasklist/@task/page.tsx +++ b/src/app/[groupId]/tasklist/@task/page.tsx @@ -1,114 +1,7 @@ -"use client"; - -import TaskDetailComment from "./_components/task-detail-comment"; -import TaskDetailContents from "./_components/task-detail-contents"; -import { Icon, InputReply } from "@/components"; -import TaskDetailWrapper from "./_components/task-detail-wrapper"; -import { - useParams, - usePathname, - useRouter, - useSearchParams, -} from "next/navigation"; -import useGetTaskDetail from "@/hooks/api/task/use-get-task-detail"; -import { AnimatePresence } from "framer-motion"; -import { useCreateComment } from "@/hooks/api/comments/use-create-comment"; -import { motion } from "framer-motion"; -import cn from "@/utils/clsx"; -import TaskDetailInputReply from "./_components/task-detail-input-reply"; - -const pageVariants = { - initial: { - x: "100%", - opacity: 0, - }, - visible: { - x: 0, - opacity: 1, - transition: { - type: "tween", - ease: "easeOut", - duration: 0.2, - } as const, - }, - exit: { - x: "100%", - opacity: 0, - transition: { - type: "tween", - ease: "easeIn", - duration: 0.2, - } as const, - }, -}; +import TaskDetail from "./_components/task-detail"; const Page = () => { - const router = useRouter(); - const param = useParams(); - const pathName = usePathname(); - const searchParam = useSearchParams(); - const taskId = Number(searchParam.get("task")); - const taskListId = Number(searchParam.get("list")); - const groupId = Number(param.groupId); - - const handleClose = () => { - router.push(`${pathName}?list=${taskListId}`); - }; - - const { data: taskDetailData, isPending } = useGetTaskDetail( - groupId, - taskListId, - taskId - ); - - const { mutate: postTaskDetailComment } = useCreateComment(taskId); - - return ( - - {taskId ? ( - - -
-
- -
-
- - - {taskDetailData?.commentCount || 0} - -
- -
-
- -
-
-
- ) : null} -
- ); + return ; }; export default Page; diff --git a/src/app/[groupId]/tasklist/_components/task-list-item.tsx b/src/app/[groupId]/tasklist/_components/task-list-item.tsx index 8eb71c80..d51f736d 100644 --- a/src/app/[groupId]/tasklist/_components/task-list-item.tsx +++ b/src/app/[groupId]/tasklist/_components/task-list-item.tsx @@ -43,6 +43,7 @@ const TaskListItem = ({ alt="empty_task" quality={100} draggable={false} + priority />

할 일이 없네요 diff --git a/src/app/[groupId]/tasklist/_components/task-list.tsx b/src/app/[groupId]/tasklist/_components/task-list.tsx new file mode 100644 index 00000000..13d4e18c --- /dev/null +++ b/src/app/[groupId]/tasklist/_components/task-list.tsx @@ -0,0 +1,91 @@ +"use client"; + +import TaskListContainer from "../_components/task-list-container"; +import { useEffect, useState } from "react"; +import TaskListDatePicker from "../_components/task-list-date-picker"; +import TaskListItem from "../_components/task-list-item"; +import cn from "@/utils/clsx"; +import { Button, Icon, TaskModal, TeamBannerMember } from "@/components"; +import BannerMemberSkeleton from "@/components/skeleton/team-skeleton/banner-member-skeleton"; +import useGetGroupInfo from "@/hooks/api/group/use-get-group-info"; +import useGetTaskItems from "@/hooks/api/task/use-get-task-items"; +import { notFound, useParams, useSearchParams } from "next/navigation"; +import usePrompt from "@/hooks/use-prompt"; + +const TaskList = () => { + const param = useParams(); + const query = useSearchParams().get("list"); + const groupId = Number(param.groupId); + const taskListId = Number(query); + const [selectedDate, setSelectedDate] = useState(null); + const { data: groupData, isPending } = useGetGroupInfo(groupId); + const { data: taskItems, isPending: taskItemsPending } = useGetTaskItems( + groupId, + taskListId, + selectedDate?.toLocaleDateString("sv-SE") || "" + ); + + const { Modal, openPrompt, closePrompt } = usePrompt(); + + useEffect(() => { + setSelectedDate(new Date()); + }, []); + + return ( +

+ {isPending ? ( + + ) : ( + {}} + className="py-3 tablet:mt-[69px] tablet:py-4" + /> + )} +
+ +
+ + + +
+
+ + + +
+ ); +}; + +export default TaskList; diff --git a/src/app/[groupId]/tasklist/page.tsx b/src/app/[groupId]/tasklist/page.tsx index 3f338018..830780ef 100644 --- a/src/app/[groupId]/tasklist/page.tsx +++ b/src/app/[groupId]/tasklist/page.tsx @@ -1,91 +1,84 @@ -"use client"; +import { Metadata } from "next"; +import TaskList from "./_components/task-list"; +import { cookies } from "next/headers"; +import { fetchTaskList } from "@/api/task/get-task-list"; +import { fetchTaskDetail } from "@/api/task/get-task-detail"; -import TaskListContainer from "./_components/task-list-container"; -import { useEffect, useState } from "react"; -import TaskListDatePicker from "./_components/task-list-date-picker"; -import TaskListItem from "./_components/task-list-item"; -import cn from "@/utils/clsx"; -import { Button, Icon, TaskModal, TeamBannerMember } from "@/components"; -import BannerMemberSkeleton from "@/components/skeleton/team-skeleton/banner-member-skeleton"; -import useGetGroupInfo from "@/hooks/api/group/use-get-group-info"; -import useGetTaskItems from "@/hooks/api/task/use-get-task-items"; -import { useParams, useSearchParams } from "next/navigation"; -import usePrompt from "@/hooks/use-prompt"; +export const generateMetadata = async ({ + params, + searchParams, +}: { + params: Promise<{ groupId: string }>; + searchParams: Promise<{ list: string; task: string | undefined }>; +}): Promise => { + const { groupId } = await params; + const { list, task } = await searchParams; + const cookieStore = await cookies(); + const token = cookieStore.get("accessToken")?.value; -const Page = () => { - const param = useParams(); - const query = useSearchParams().get("list"); - const groupId = Number(param.groupId); - const taskListId = Number(query); - const [selectedDate, setSelectedDate] = useState(null); - const { data: groupData, isPending } = useGetGroupInfo(groupId); - const { data: taskItems, isPending: taskItemsPending } = useGetTaskItems( - groupId, - taskListId, - selectedDate?.toLocaleDateString("sv-SE") || "" - ); + if (!token) { + return { + title: "할 일 목록", + description: "할 일을 확인할 수 있습니다.", + }; + } + + if (task) { + const taskDetail = await fetchTaskDetail( + Number(groupId), + Number(list), + Number(task), + token + ); - const { Modal, openPrompt, closePrompt } = usePrompt(); + return { + title: `${taskDetail.name ? taskDetail.name : "페이지를 찾을 수 없습니다."} - 할 일 상세`, + description: `${taskDetail.name ? `${taskDetail.name}의 상세 정보를 확인할 수 있습니다.` : "페이지를 찾을 수 없습니다."}`, + openGraph: { + title: `${taskDetail.name ? taskDetail.name : "페이지를 찾을 수 없습니다."} - 할 일 상세 | Coworkers`, + description: `${taskDetail.name ? `${taskDetail.name}의 상세 정보를 확인할 수 있습니다.` : "페이지를 찾을 수 없습니다."}`, + type: "website", + url: `https://coworkers-pied.vercel.app/${groupId}/tasklist?list=${list}&task=${task}`, + locale: "ko_KR", + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "Coworkers", + }, + ], + }, + }; + } - useEffect(() => { - setSelectedDate(new Date()); - }, []); + const response = await fetchTaskList(Number(groupId), Number(list), token); - return ( -
- {isPending ? ( - - ) : ( - {}} - className="py-3 tablet:mt-[69px] tablet:py-4" - /> - )} -
- -
- - - -
-
- - - -
- ); + return { + title: `${response.name ? response.name : "페이지를 찾을 수 없습니다."} - 할 일 목록`, + description: `${response.name ? `${response.name}의 할 일을 확인할 수 있습니다.` : "페이지를 찾을 수 없습니다."}`, + openGraph: { + title: `${response.name ? response.name : "페이지를 찾을 수 없습니다."} - 할 일 목록 | Coworkers`, + description: `${response.name ? `${response.name}의 할 일을 확인할 수 있습니다.` : "페이지를 찾을 수 없습니다."}`, + type: "website", + url: `https://coworkers-pied.vercel.app/${groupId}/tasklist?list=${list}`, + locale: "ko_KR", + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "Coworkers", + }, + ], + }, + }; +}; + +const Page = () => { + return ; }; export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d9cc44cc..8045eb31 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,10 +7,26 @@ import { ToastProvider } from "@/toast-provider"; import GnbWrapper from "@/components/gnb/gnb-wrapper"; export const metadata: Metadata = { - title: "Coworkers", + title: { default: "Coworkers", template: "%s | Coworkers" }, icons: { icon: "/ic-coworkers-logo.svg", }, + openGraph: { + title: "Coworkers", + description: "랜딩 페이지", + type: "website", + url: "https://coworkers-pied.vercel.app/", + locale: "ko_KR", + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "Coworkers", + }, + ], + }, }; export default function RootLayout({ diff --git a/src/app/myhistory/_components/history-list.tsx b/src/app/myhistory/_components/history-list.tsx index 269338c7..44846971 100644 --- a/src/app/myhistory/_components/history-list.tsx +++ b/src/app/myhistory/_components/history-list.tsx @@ -8,7 +8,7 @@ const HistoryList = ({ monthlyTaskList: MonthlyTaskList[]; }) => { return ( -
    +
      {[...monthlyTaskList].reverse().map((taskList) => { return (
    • diff --git a/src/app/myhistory/layout.tsx b/src/app/myhistory/layout.tsx index 9113aad3..3d119956 100644 --- a/src/app/myhistory/layout.tsx +++ b/src/app/myhistory/layout.tsx @@ -1,10 +1,14 @@ -"use client"; - +import { Metadata } from "next"; import { ReactNode } from "react"; +export const metadata: Metadata = { + title: "마이 히스토리", + description: "마이 히스토리", +}; + const Layout = ({ children }: { children: ReactNode }) => { return ( -
      +
      {children}
      ); diff --git a/src/app/myhistory/page.tsx b/src/app/myhistory/page.tsx index 950e0582..dffa94fc 100644 --- a/src/app/myhistory/page.tsx +++ b/src/app/myhistory/page.tsx @@ -3,23 +3,22 @@ import useGetUserHistory from "@/hooks/api/user/use-get-user-history"; import { getMonthlyTaskList } from "@/utils/util"; import HistoryList from "./_components/history-list"; +import { HistorySkeleton } from "@/components"; const Page = () => { const { data: userHistory, isPending: userHistoryPending } = useGetUserHistory(); + if (userHistoryPending) return ; + return ( -
      -
      -
      +
      +
      +

      마이 히스토리

      - {!userHistoryPending ? ( - - ) : ( -

      로딩 중

      - )} +
      diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 19915547..e3cdbbdc 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,30 +1,20 @@ -"use client"; +import NotFoundView from "@/components/not-found/not-found"; +import { Metadata } from "next"; -import Link from "next/link"; -import { Button } from "@/components/index"; -import LottieAnimation from "@/components/lottie/LottieAnimation"; -import notfound from "@/../public/animations/404-not-found.json"; +export const metadata: Metadata = { + title: "페이지를 찾을 수 없습니다", + description: "요청하신 페이지가 존재하지 않습니다.", + robots: { + index: false, + follow: false, + }, +}; const NotFound = () => { return ( -
      - -

      - 요청하신 페이지를 찾을 수 없습니다. -

      -

      - 경로가 변경되었거나 존재하지 않는 주소입니다. -

      - - - -
      +
      + +
      ); }; diff --git a/src/components/index.ts b/src/components/index.ts index 8e1d61a3..feec6a8c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -42,3 +42,4 @@ export { default as ArticlesListSkeleton } from "./skeleton/boards-skeleton/arti export { default as ArticleDetailSkeleton } from "./skeleton/boards-skeleton/article-detail-skeleton"; export { default as ArticleEditSkeleton } from "./skeleton/boards-skeleton/article-edit-skeleton"; export { default as UserSettingsSkeleton } from "./skeleton/user-skeleton/user-settings-skeleton"; +export { default as HistorySkeleton } from "./skeleton/history-skeleton/history-skeleton"; diff --git a/src/components/not-found/not-found.tsx b/src/components/not-found/not-found.tsx new file mode 100644 index 00000000..5b8be40b --- /dev/null +++ b/src/components/not-found/not-found.tsx @@ -0,0 +1,31 @@ +"use client"; + +import Link from "next/link"; +import { Button } from "@/components/index"; +import LottieAnimation from "@/components/lottie/LottieAnimation"; +import notfound from "@/../public/animations/404-not-found.json"; + +const NotFoundView = () => { + return ( +
      + +

      + 요청하신 페이지를 찾을 수 없습니다. +

      +

      + 경로가 변경되었거나 존재하지 않는 주소입니다. +

      + + + +
      + ); +}; + +export default NotFoundView; diff --git a/src/components/skeleton/history-skeleton/history-skeleton.tsx b/src/components/skeleton/history-skeleton/history-skeleton.tsx new file mode 100644 index 00000000..63d16bad --- /dev/null +++ b/src/components/skeleton/history-skeleton/history-skeleton.tsx @@ -0,0 +1,26 @@ +import Skeleton from "react-loading-skeleton"; + +const HistorySkeleton = () => { + return ( +
      +
      +
      +

      마이 히스토리

      +
      + {Array.from({ length: 3 }).map((_, idx) => { + return ( + + ); + })} +
      +
      +
      +
      + ); +}; + +export default HistorySkeleton; diff --git a/src/hooks/api/task/use-get-task-detail.ts b/src/hooks/api/task/use-get-task-detail.ts index b63a3ec0..96b928f6 100644 --- a/src/hooks/api/task/use-get-task-detail.ts +++ b/src/hooks/api/task/use-get-task-detail.ts @@ -1,4 +1,4 @@ -import getTaskDetail from "@/api/task/get-task-detail"; +import { getTaskDetail } from "@/api/task/get-task-detail"; import { useQuery } from "@tanstack/react-query"; const useGetTaskDetail = ( diff --git a/src/hooks/api/task/use-get-task-list.ts b/src/hooks/api/task/use-get-task-list.ts index 1c91dfe6..33e47baf 100644 --- a/src/hooks/api/task/use-get-task-list.ts +++ b/src/hooks/api/task/use-get-task-list.ts @@ -1,4 +1,4 @@ -import getTaskList from "@/api/task/get-task-list"; +import { getTaskList } from "@/api/task/get-task-list"; import { useQuery } from "@tanstack/react-query"; const useGetTaskList = (groupId: number, taskListId: number, date: string) => { diff --git a/src/hooks/use-prompt.tsx b/src/hooks/use-prompt.tsx index 81e83cda..a75f5aa7 100644 --- a/src/hooks/use-prompt.tsx +++ b/src/hooks/use-prompt.tsx @@ -26,16 +26,17 @@ const usePrompt = (showCloseBtn = false) => { const lockingScroll = useCallback(() => { const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + const isScrollable = document.body.scrollHeight > window.innerHeight; document.documentElement.style.overflow = "hidden"; - if (scrollbarWidth > 0) { + if (scrollbarWidth && isScrollable) { document.documentElement.style.paddingRight = `${scrollbarWidth}px`; } }, []); const allowScroll = useCallback(() => { document.documentElement.style.overflow = "auto"; - document.documentElement.style.paddingRight = "0"; + document.documentElement.style.removeProperty("padding-right"); }, []); const openPrompt = useCallback(() => setIsOpen(true), []); const closePrompt = useCallback(() => setIsOpen(false), []); diff --git a/src/utils/util.ts b/src/utils/util.ts index 1fb5f0f1..8f80abd2 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -48,9 +48,13 @@ export const getMonthlyTaskList = ( ): MonthlyTaskList[] => { if (!tasks) return []; + const sortedTasks = tasks.sort((a, b) => { + return new Date(b.doneAt).getTime() - new Date(a.doneAt).getTime(); + }); + let prevDoneDate = ""; - const monthlyTaskList = tasks.map((task) => { + const monthlyTaskList = sortedTasks.map((task) => { let doneDate = task.doneAt.slice(0, 10); if (prevDoneDate !== doneDate) { @@ -63,7 +67,7 @@ export const getMonthlyTaskList = ( } }); - return monthlyTaskList.filter( - (item): item is MonthlyTaskList => item !== undefined - ); + return monthlyTaskList + .filter((item): item is MonthlyTaskList => item !== undefined) + .reverse(); };