diff --git a/next.config.ts b/next.config.ts
index 76fe1d3e..4d6e90fe 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,12 @@
const nextConfig = {
images: {
- domains: ["k.kakaocdn.net"], // 카카오 CDN 도메인 추가
+ domains: ["img1.kakaocdn.net", "k.kakaocdn.net"], // 카카오 CDN 도메인 추가
remotePatterns: [
+ {
+ protocol: "http",
+ hostname: "img1.kakaocdn.net",
+ pathname: "/**",
+ },
{
protocol: "http",
hostname: "k.kakaocdn.net",
diff --git a/public/icons/mypage/clap.svg b/public/icons/mypage/clap.svg
new file mode 100644
index 00000000..5028104a
--- /dev/null
+++ b/public/icons/mypage/clap.svg
@@ -0,0 +1,29 @@
+
diff --git a/public/icons/mypage/mypage-character.png b/public/icons/mypage/mypage-character.png
new file mode 100644
index 00000000..658e1526
Binary files /dev/null and b/public/icons/mypage/mypage-character.png differ
diff --git a/src/app/(protected)/my-page/_component/TaskContainer.tsx b/src/app/(protected)/my-page/_component/TaskContainer.tsx
index 312032f7..d247e3dc 100644
--- a/src/app/(protected)/my-page/_component/TaskContainer.tsx
+++ b/src/app/(protected)/my-page/_component/TaskContainer.tsx
@@ -1,17 +1,25 @@
+import { useExpiredTaskStore } from "@/store/useTaskStore";
import type { TaskOrigin } from "@/types/myPage";
import { format } from "date-fns";
import { ko } from "date-fns/locale";
+import { useRouter } from "next/navigation";
import React, { useState } from "react";
const TaskItem = ({ task }: { task: TaskOrigin }) => {
+ const router = useRouter();
const date = new Date(task.dueDatetime);
const formattedDate = format(date, "M월 d일 (eee)ㆍa hh:mm까지", {
locale: ko,
});
+ const { setCurrentTask } = useExpiredTaskStore();
+
+ const handleTaskClick = () => {
+ setCurrentTask(task);
+ router.push(`/my-page/task-detail/${task.id}`);
+ };
return (
- // TODO : 여기에 완료한 일 또는 미룬 일 눌렀을때 해당 화면 전환 함수 추가 필요
-
+
{task.name}
{formattedDate}
diff --git a/src/app/(protected)/my-page/task-detail/[taskId]/ExpiredTaskDetailPage.tsx b/src/app/(protected)/my-page/task-detail/[taskId]/ExpiredTaskDetailPage.tsx
new file mode 100644
index 00000000..52058730
--- /dev/null
+++ b/src/app/(protected)/my-page/task-detail/[taskId]/ExpiredTaskDetailPage.tsx
@@ -0,0 +1,299 @@
+"use client";
+
+import RetrospectItem from "@/app/(protected)/retrospection/[taskId]/_components/RetrospectItem";
+import { Badge } from "@/components/component/Badge";
+import CustomBackHeader from "@/components/customBackHeader/CustomBackHeader";
+import { useUserStore } from "@/store";
+import type { TaskWithRetrospection } from "@/types/myPage";
+import {
+ convertIsoToMonthDayTimeText,
+ formatTimeFromMinutes,
+} from "@/utils/dateFormat";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+
+type Props = {
+ task: TaskWithRetrospection;
+};
+
+const retrospectItems: RetrospectItems = {
+ result: {
+ title: "몰입한 결과에 얼마나 만족하시나요?",
+ required: true,
+ },
+ focus: {
+ title: "몰입하는 동안 나의 집중력은?",
+ required: true,
+ },
+ keepAndTry: {
+ title: "이번 몰입의 좋았던 점과 개선할 점은?",
+ required: false,
+ },
+};
+
+const RESULT_CONTENT = [0, 1, 2, 3, 4];
+const FOCUS_STEPS = [0, 1, 2, 3, 4, 5];
+const BAR = {
+ HEIGHT: 18,
+ SLIDER_RADIUS: 9,
+};
+
+export default function ExpiredTaskDetailPage({ task }: Props) {
+ const router = useRouter();
+ const { userData } = useUserStore();
+
+ const satisfaction = task.satisfaction / 20 - 1;
+
+ const taskStatus = task.status === "COMPLETE" ? "COMPLETE" : "FAIL";
+
+ const PHRASE = {
+ COMPLETE: {
+ topBar: "완료한 일",
+ main: "잘 완료하셨어요!",
+ },
+ FAIL: {
+ topBar: "미룬 일",
+ main: "완료를 안하셨네요. 다음엔 꼭 완료해요!",
+ },
+ };
+
+ const keys = [
+ {
+ name: "작은 행동",
+ onlyComplete: false,
+ content: task.triggerAction,
+ },
+ {
+ name: "예상 소요시간",
+ onlyComplete: false,
+ content: formatTimeFromMinutes(task.estimatedTime),
+ },
+ {
+ name: "마감일",
+ onlyComplete: false,
+ content: convertIsoToMonthDayTimeText(task.dueDateTime),
+ },
+ ];
+
+ const filtered = keys.filter((item) => {
+ if (!item.onlyComplete) {
+ return item.onlyComplete === false;
+ }
+ // onlyComplete === true 인 경우, 조건 함수까지 만족해야 함
+ return item.onlyComplete === true && taskStatus === "COMPLETE";
+ });
+
+ return (
+
+ {/* 헤더 부분 */}
+
+
+ {/* Contents 부분 */}
+
+ {/* Contents - 작업 개요 */}
+
+ {/* Contents - 작업 개요 - 문구*/}
+
+
+ {task.name}
{" "}
+ {/* TODO: 이 task.name 이 새로고침 해야 나옴.. 뭔가 고쳐야 함*/}
+ {PHRASE[taskStatus].main}
+
+
+
+ {/* Contents - 작업 개요 - 작업 정보 */}
+
+ {/* Contents - 작업 개요 - 작업 정보 - 페르소나 */}
+
+
+
+
+ {task.personaName} {userData.nickname}
+
+
+
+
+ {/* Contents - 작업 개요 - 작업 정보 - 페르소나 제외 작업 정보 */}
+
+ {keys.map((item, index) => (
+
+ {item.name}
+ {item.content}
+
+ ))}
+ {/* 완료일 정보는 따로 */}
+ {taskStatus === "COMPLETE" && (
+
+
완료 일
+
+
+
+ {" "}
+ {convertIsoToMonthDayTimeText(task.updatedAt)}
+
+
+
+
+ )}
+
+
+
+
+ {/* Contents - 작업 회고 내용 */}
+
+ {/* Contents - 작업 회고 내용 - 제목 문구 */}
+
+
+ {/* Contents - 작업 회고 내용 - 회고 내용 */}
+
+ {" "}
+ {/* 실제 유저 회고 부분 */}
+ {/* 몰입 결과 회고 */}
+
+
+ {RESULT_CONTENT.map((num, index) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+
+
+
+ ))}
+
+
+ {/* 몰입하는 동안 나의 집중력 */}
+
+
+
+ {/* 전체 바 배경 */}
+
+
+ {/* 선택된 채워진 부분 */}
+
+
+ {/* 점들 */}
+
+ {FOCUS_STEPS.map((step, i) => (
+
+ key={i}
+ className={`w-[6px] h-[6px] rounded-full transition-all duration-200 ${
+ task.concentration >= step
+ ? "bg-background-skyblue opacity-90"
+ : "bg-background-skyblue opacity-30"
+ }`}
+ />
+ ))}
+
+
+ {/* 슬라이더 핸들 */}
+
+
+
+ {/* 아래 숫자 레이블 */}
+
+ {FOCUS_STEPS.map((step, i) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+
+
+ key={i}
+ className={
+ task.concentration === step
+ ? "text-gray-alternative"
+ : ""
+ }
+ >
+ {`${step * 20}`}
+
+
+ ))}
+
+
+
+ {/* 몰입 회고 텍스트 */}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/my-page/task-detail/[taskId]/page.tsx b/src/app/(protected)/my-page/task-detail/[taskId]/page.tsx
new file mode 100644
index 00000000..78a03608
--- /dev/null
+++ b/src/app/(protected)/my-page/task-detail/[taskId]/page.tsx
@@ -0,0 +1,35 @@
+import { serverApi } from "@/lib/serverKy";
+import type { TaskWithRetrospection } from "@/types/myPage";
+import { HTTPError } from "ky";
+import ExpiredTaskDetailPage from "./ExpiredTaskDetailPage";
+
+const fetchServerTaskWithRetrospection = async (
+ taskId: string,
+): Promise
=> {
+ const response = await serverApi
+ .get(`v1/tasks/${taskId}/retrospection`)
+ .json()
+ .catch((error) => {
+ if (error instanceof HTTPError) {
+ console.error(error.response);
+ error.response.json().then((data) => {
+ console.error("Error response: ", data);
+ });
+ } else {
+ console.error("Error : ", error.message);
+ }
+ });
+ return response as TaskWithRetrospection;
+};
+
+export default async function RetrospectPage({
+ params,
+}: {
+ params: Promise<{ taskId: string }>;
+}) {
+ const { taskId } = await params;
+ const task: TaskWithRetrospection =
+ await fetchServerTaskWithRetrospection(taskId);
+
+ return ;
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 11b2b5ab..e5e99577 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -51,7 +51,7 @@ body {
/* 배경색 설정 */
html {
@apply bg-background-primary;
- }
+}
/* 기본 헤더 스타일 */
.h1 {
@@ -105,6 +105,10 @@ html {
@apply text-b3;
}
+.b3Bold {
+ @apply text-b3 font-semibold;
+}
+
/* 캡션 스타일 */
.c1 {
@apply text-c1;
diff --git a/src/store/useTaskStore.ts b/src/store/useTaskStore.ts
index 30fd5f57..55c2b75c 100644
--- a/src/store/useTaskStore.ts
+++ b/src/store/useTaskStore.ts
@@ -1,4 +1,5 @@
import { EditTaskInputType } from "@/app/(protected)/(create)/context";
+import { TaskOrigin } from "@/types/myPage";
import type { Task as TaskType } from "@/types/task";
import { create } from "zustand";
@@ -40,6 +41,11 @@ type TaskStore = {
setCurrentTask: (task: TaskType) => void;
};
+type ExpiredTaskStore = {
+ currentTask: TaskOrigin | null;
+ setCurrentTask: (task: TaskOrigin) => void;
+};
+
export const useTaskProgressStore = create((set) => ({
currentTask: null,
setCurrentTask: (task) => set({ currentTask: task }),
@@ -49,3 +55,8 @@ export const useThisWeekTaskStore = create((set) => ({
currentTask: null,
setCurrentTask: (task) => set({ currentTask: task }),
}));
+
+export const useExpiredTaskStore = create((set) => ({
+ currentTask: null,
+ setCurrentTask: (task) => set({ currentTask: task }),
+}));
\ No newline at end of file
diff --git a/src/types/myPage.ts b/src/types/myPage.ts
index a6da33ff..bdb2d4a9 100644
--- a/src/types/myPage.ts
+++ b/src/types/myPage.ts
@@ -57,3 +57,18 @@ export interface MyData {
completedTaskCount: number;
procrastinatedTaskCount: number;
}
+
+export interface TaskWithRetrospection {
+ id: number,
+ name: string,
+ personaId: number,
+ personaName: string,
+ triggerAction: string,
+ estimatedTime: number,
+ dueDateTime: string,
+ status: string,
+ updatedAt: string,
+ satisfaction: number,
+ concentration: number,
+ comment: string
+}
diff --git a/src/utils/dateFormat.ts b/src/utils/dateFormat.ts
index 57ef9e77..b88400ce 100644
--- a/src/utils/dateFormat.ts
+++ b/src/utils/dateFormat.ts
@@ -349,3 +349,30 @@ export const getTimeRemaining = (
return { days, hours, minutes };
};
+
+// 120분 => 2시간, 50분 => 50분, 90분 => 1시간 30분
+export const formatTimeFromMinutes = (minutes: number): string => {
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+
+ if (hours === 0 && mins === 0) return "0분";
+ if (hours === 0) return `${mins}분`;
+ if (mins === 0) return `${hours}시간`;
+ return `${hours}시간 ${mins}분`;
+}
+
+// 2025-03-05T16:00:00 -> "3월 5일, 오후 4:00"
+export const convertIsoToMonthDayTimeText = (input: string): string => {
+ const date = new Date(input);
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+
+ const time = date.toLocaleTimeString("ko-KR", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+
+ return `${month}월 ${day}일, ${time}`;
+ }
\ No newline at end of file
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 77d46f25..3adb488f 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -41,6 +41,7 @@ export default {
b1: ["18px", { lineHeight: "145%", fontWeight: "400" }],
b2: ["16px", { lineHeight: "145%", fontWeight: "400" }],
b3: ["14px", { lineHeight: "145%", fontWeight: "400" }],
+ b3Bold: ["14px", { lineHeight: "145%", fontWeight: "600" }],
/* Caption */
c1: ["13px", { lineHeight: "150%", fontWeight: "400" }],