Skip to content
37 changes: 30 additions & 7 deletions src/app/taskmate/team/[teamId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
"use client";

Comment on lines +1 to +2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use client가 붙어 있는데, AsyncBoundary가 ssr: false로 dynamic import 되어있어서 제거해도 괜찮지않나요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorFallback에서 화살표 함수 prop을 전달하는게, 서버 컴포넌트에서는 함수를 전달 못해서 에러가 발생하더라고요

import AsyncBoundary from "@/components/common/AsyncBoundary";
import { GoalList } from "@/components/team/GoalList";
import { MemberList } from "@/components/team/MemberList";
import { Summary } from "@/components/team/Summary";

export default function Page() {
return (
<div className="flex flex-col gap-10">
{/* @TODO: Loading & Error 처리 */}
<AsyncBoundary>
<div className="mobile:py-8 flex flex-col gap-10 px-6 pt-[88px]">
<AsyncBoundary
loadingFallback={<Summary.Loading />}
errorFallback={(error, onReset) => (
<Summary.Error
error={error}
onReset={onReset}
/>
)}
>
<Summary />
</AsyncBoundary>

{/* @TODO: Loading & Error 처리 */}
<AsyncBoundary>
<AsyncBoundary
loadingFallback={<GoalList.Loading />}
errorFallback={(error, onReset) => (
<GoalList.Error
error={error}
onReset={onReset}
/>
)}
>
<GoalList />
</AsyncBoundary>

{/* @TODO: Loading & Error 처리 */}
<AsyncBoundary>
<AsyncBoundary
loadingFallback={<MemberList.Loading />}
errorFallback={(error, onReset) => (
<MemberList.Error
error={error}
onReset={onReset}
/>
)}
>
<MemberList />
</AsyncBoundary>
</div>
Expand Down
42 changes: 42 additions & 0 deletions src/components/team/GoalList/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import Button from "@/components/common/Button/Button";
import { Icon } from "@/components/common/Icon";

interface Props {
error: Error;
onReset: () => void;
}

export default function GoalListError({ error, onReset }: Props) {
return (
<div className="flex w-full flex-col items-start gap-5 rounded-3xl bg-white p-6">
<div className="flex w-full items-center justify-start gap-3">
<Icon
name="FlagGreen"
size={40}
/>
<h2 className="typography-body-1 text-label-neutral font-medium">
목표
</h2>
</div>

<div className="flex w-full flex-col items-center gap-2 rounded-2xl bg-gray-50 px-6 py-8">
<p className="typography-body-2 text-label-strong font-semibold">
목표를 불러오지 못했어요
</p>
<p className="typography-label-2 text-center text-gray-400">
{error.message || "잠시 후 다시 시도해주세요."}
</p>
<Button
variant="secondary"
size="md"
className="mt-3"
onClick={onReset}
>
다시 시도
</Button>
</div>
</div>
);
}
79 changes: 79 additions & 0 deletions src/components/team/GoalList/GoalList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { useMemo, useState } from "react";

import { Icon } from "@/components/common/Icon";
import { MainSecondaryProgressCard } from "@/components/team/MainSecondaryProgressCard";
import { Order } from "@/components/todo/List/Order";
import { goalQueries } from "@/features/goal/query/goal.queryKey";
import { SortType } from "@/features/goal/types";
import { useTeamId } from "@/features/team/hooks/useTeamId";
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll/useInfiniteScroll";

const sortTypeByLabel: Record<string, SortType> = {
최신순: "LATEST",
오래된순: "OLDEST",
};

export default function GoalList() {
const teamId = useTeamId();
const sortOptions = ["최신순", "오래된순"];
const [selectedSort, setSelectedSort] = useState("최신순");
const sort = sortTypeByLabel[selectedSort] ?? "LATEST";

const { ref, data } = useInfiniteScroll(
goalQueries.getTeamGoalListInfinite(teamId, sort),
);

const size = data.pages[0].size;
const goalList = useMemo(
() => data.pages.flatMap((page) => page.items),
[data.pages],
);

return (
<div className="flex w-full flex-col items-start gap-5">
<div className="flex w-full items-center justify-between">
<div className="flex items-center justify-start gap-3">
<Icon
name="FlagGreen"
size={40}
/>
<h2 className="typography-body-1 text-label-neutral font-medium">
목표
</h2>
<span className="typography-body-1 ml-[-8px] font-medium text-gray-400">
{size}개
</span>
</div>

<Order
options={sortOptions}
selected={selectedSort}
onSelect={setSelectedSort}
/>
</div>

<div className="max-h-[300px] w-full overflow-y-auto pr-1">
<div className="tablet:grid-cols-2 desktop:grid-cols-3 grid w-full grid-cols-1 gap-4">
{goalList.map((goal) => (
<MainSecondaryProgressCard
teamId={teamId}
key={goal.goalId}
goalId={goal.goalId}
title={goal.name}
progress={goal.progressPercent}
color="green"
isFavorite={goal.isFavorite}
/>
))}
</div>

<div
ref={ref}
className="h-1 w-full"
/>
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions src/components/team/GoalList/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { Icon } from "@/components/common/Icon";
import Spinner from "@/components/common/Spinner";

export default function GoalListLoading() {
return (
<div className="flex w-full flex-col items-start gap-5">
<div className="flex w-full items-center justify-between">
<div className="flex items-center justify-start gap-3">
<Icon
name="FlagGreen"
size={40}
/>
<h2 className="typography-body-1 text-label-neutral font-medium">
목표
</h2>
</div>
</div>

<div className="flex h-[300px] w-full items-center justify-center">
<Spinner size={40} />
</div>
</div>
);
}
79 changes: 8 additions & 71 deletions src/components/team/GoalList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,8 @@
"use client";

import { useSuspenseQuery } from "@tanstack/react-query";
import { useState } from "react";

import { Icon } from "@/components/common/Icon";
import { MainSecondaryProgressCard } from "@/components/team/MainSecondaryProgressCard";
import { Order } from "@/components/todo/List/Order";
import { goalQueries } from "@/features/goal/query/goal.queryKey";
import { SortType } from "@/features/goal/types";
import { useTeamId } from "@/features/team/hooks/useTeamId";

const sortTypeByLabel: Record<string, SortType> = {
최신순: "LATEST",
"마감일 순": "OLDEST",
};

// @TODO: 목표 목록 조회 시 무한 스크롤 처리 (useSuspenseInfiniteQuery )
export const GoalList = () => {
const teamId = useTeamId();
const sortOptions = ["최신순", "마감일 순"];
const [selectedSort, setSelectedSort] = useState("최신순");

const {
data: { items: goalList },
} = useSuspenseQuery(
goalQueries.getTeamGoalList(
teamId,
sortTypeByLabel[selectedSort] ?? "LATEST",
),
);

return (
<div className="flex w-full flex-col items-start gap-5">
<div className="flex w-full items-center justify-between">
<div className="flex items-center justify-start gap-3">
<Icon
name="FlagGreen"
size={40}
/>
<h2 className="typography-body-1 text-label-neutral font-medium">
목표
</h2>
<span className="typography-body-1 ml-[-8px] font-medium text-gray-400">
{goalList.length}개
</span>
</div>

<Order
options={sortOptions}
selected={selectedSort}
onSelect={setSelectedSort}
/>
</div>

<div className="grid w-full grid-cols-3 gap-4">
{goalList.map((goal) => (
<MainSecondaryProgressCard
teamId={teamId}
key={goal.goalId}
goalId={goal.goalId}
title={goal.name}
progress={goal.progressPercent}
color="green"
isFavorite={goal.isFavorite}
/>
))}
</div>
</div>
);
};
import GoalListError from "./Error";
import GoalListComponent from "./GoalList";
import GoalListLoading from "./Loading";

export const GoalList = Object.assign(GoalListComponent, {
Error: GoalListError,
Loading: GoalListLoading,
});
26 changes: 16 additions & 10 deletions src/components/team/MainHeroProgressCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ function StatItem({
suffix: string;
}) {
return (
<div className="min-w-0 shrink-0">
<p className="typography-body-1 mb-2 font-bold text-green-400">{label}</p>
<p className="flex items-end gap-1 text-white">
<span className="text-[56px] leading-none font-bold">{value}</span>
<span className="typography-title-3 pb-0.5 font-semibold">
<div className="min-w-0 md:shrink-0">
<p className="typography-body-2 mb-1 truncate font-bold text-green-400 md:mb-2 md:text-[15px]">
{label}
</p>
<p className="flex min-w-0 items-end gap-1 text-white">
<span className="truncate text-[40px] leading-none font-bold md:text-[48px]">
{value}
</span>
<span className="typography-body-1 truncate pb-1 font-semibold md:pb-0.5 md:text-[24px]">
{suffix}
</span>
</p>
Expand All @@ -53,7 +57,7 @@ function StatItem({
const Character = () => {
return (
<svg
className="mobile:w-[100px] absolute right-0 bottom-0 opacity-50"
className="mobile:w-[200px] absolute right-0 bottom-0 w-[100px] opacity-50"
xmlns="http://www.w3.org/2000/svg"
width="196"
height="215"
Expand Down Expand Up @@ -178,7 +182,9 @@ export const MainHeroProgressCard = ({
)}
>
<div className="mb-10 flex items-center gap-2">
<h2 className="typography-title-2 font-bold text-white">{title}</h2>
<h2 className="typography-title-2 truncate font-bold text-white">
{title}
</h2>

{isAdmin && (
<button>
Expand All @@ -194,7 +200,7 @@ export const MainHeroProgressCard = ({
)}
</div>

<div className="mb-8 flex gap-16 md:max-w-[520px]">
<div className="mb-8 grid grid-cols-2 gap-x-6 gap-y-5 md:flex md:max-w-[520px] md:gap-16">
<StatItem
label="오늘의 진행 상황"
value={progress}
Expand All @@ -221,7 +227,7 @@ export const MainHeroProgressCard = ({
{/* 말풍선 */}
{progress >= 80 && progress < 100 && (
<div
className="absolute top-full z-20 -translate-x-1/2 translate-y-7 transition-[left] duration-500 ease-out md:top-auto md:bottom-auto md:bottom-full md:-translate-y-7"
className="absolute top-full z-20 -translate-x-1/2 translate-y-7 transition-[left] duration-500 ease-out md:top-auto md:bottom-full md:-translate-y-7"
style={{ left: `${progress}%` }}
>
<div className="relative flex items-center gap-2 rounded-full bg-gray-800 px-4.5 py-4.5 text-xs font-semibold text-white shadow-lg">
Expand All @@ -232,7 +238,7 @@ export const MainHeroProgressCard = ({
height={24}
className="shrink-0 object-contain"
/>
<span className="typography-body-2 whitespace-nowrap text-green-100">
<span className="typography-body-2 truncate whitespace-nowrap text-green-100">
{statusLabel}
</span>
<span className="absolute bottom-full left-1/2 h-4 w-4 -translate-x-1/2 translate-y-1/2 rotate-45 bg-gray-800 md:top-full md:-translate-y-1/2" />
Expand Down
4 changes: 3 additions & 1 deletion src/components/team/MainSecondaryProgressCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export const MainSecondaryProgressCard = ({
}}
>
<div className="mb-5 flex items-center justify-between gap-4">
<h3 className="text-lg font-semibold text-zinc-900">{title}</h3>
<h3 className="truncate text-lg font-semibold text-zinc-900">
{title}
</h3>
{iconSrc ? (
<Image
src={iconSrc}
Expand Down
Loading
Loading