Skip to content

Commit

Permalink
Feature/#294 qa 반영 (#303)
Browse files Browse the repository at this point in the history
* feat: 팀페이지에서 스터디 목록 라우팅 등록

#294

* feat: 스터디 생성 실패 시, 에러 문구 띄워주기

#294

* feat: 자신이 속한 팀이 아닐 때는 초대 코드 및 스터디/파일 생성 버튼이 안 보이도록 수정

- 전역변수로 myTeam이라는 자신이 속한 teamId를 담는 배열로 자신이 팀인지 검증했습니다.
#294

* feat: 학습자료 혹은 스터디가 없을 때 생성해달라는 문구 추가

#294

* feat: 스터디 전체 보기 구현

#294

* feat: team 수정후 자동으로 다시 팀 정보 가져오도록 로직 수정

#294

* feat: 스터디 갤러리의 PageNavigator 의 pages 추가

#294

* feat: 수정된 팀 내 스터디 호출 api 반환 값에 따른 변화

#294

* feat: 팀 수정 시 바로 반영되도록 수정

#294

* refeactor: 사용하지 않는 mock data(studyCard) 삭제

#294

* feat: teamInfo 관련 querykey 수정

#294
  • Loading branch information
llddang authored Sep 8, 2024
1 parent cedeaec commit 4b05cc9
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 169 deletions.
9 changes: 2 additions & 7 deletions src/app/api/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ const defaultOptions: FetcherOptions = {
},
interceptors: {
request: async (config) => config,
response: async (response) => {
if (response.ok) {
return response;
}
throw new Error(response.statusText);
},
response: async (response) => response,
},
};

Expand Down Expand Up @@ -70,7 +65,7 @@ export const fetcher = (options?: FetcherOptions) => {
const bodyText = await response.text();
const body = bodyText ? JSON.parse(bodyText) : null;

return { ok: true, body };
return { ok: response.ok, body };
} catch (error) {
let message = '';
if (error instanceof Error) message = error.message;
Expand Down
12 changes: 12 additions & 0 deletions src/app/api/team.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { useQuery } from '@tanstack/react-query';

import useGetUser from '@/hooks/useGetUser';
import { Team } from '@/types';

import { fetcher } from './fetcher';
Expand All @@ -21,6 +24,14 @@ const getTeamInfo = (token: string, teamId: number) =>
},
});

const useGetTeamInfoQuery = (teamId: number) => {
const user = useGetUser();
return useQuery({
queryFn: () => getTeamInfo(user?.token || '', teamId),
queryKey: ['teamInfo', teamId.toString()],
});
};

const putEditTeam = (token: string, teamId: number, teamInfo: Pick<Team, 'name' | 'description'>) => {
return teamFetcher(`/teams/${teamId}`, {
method: 'PUT',
Expand Down Expand Up @@ -95,6 +106,7 @@ const getMyTeams = (memberId: number) => teamFetcher(`/teams/members/${memberId}
export {
postCreateTeam,
getTeamInfo,
useGetTeamInfoQuery,
putEditTeam,
patchEditTeamImage,
deleteTeam,
Expand Down
74 changes: 48 additions & 26 deletions src/app/team/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
'use client';

import { Box, Button, Flex, useBreakpointValue } from '@chakra-ui/react';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
import { BsLink45Deg } from 'react-icons/bs';

import { getDocumentList } from '@/app/api/document';
import { getGarden } from '@/app/api/garden';
import { getStudies } from '@/app/api/study';
import { getTeamInfo, postInviteTeam } from '@/app/api/team';
import { postInviteTeam, useGetTeamInfoQuery } from '@/app/api/team';
import { myTeamAtom } from '@/atom';
import Garden3D from '@/components/Garden3D';
import TabButton from '@/components/TabButton';
import Title from '@/components/Title';
Expand All @@ -21,13 +23,14 @@ import AttendanceRate from '@/containers/team/AttendanceRate';
import DocumentGridView from '@/containers/team/DocumentGridView';
import NavigationButton from '@/containers/team/NavigationButton';
import StudyGridView from '@/containers/team/StudyGridView';
import SuggestionCreate from '@/containers/team/SuggestionCreate';
import TeamControlPanel from '@/containers/team/TeamControlPanel';
import TeamMember from '@/containers/team/teamMember';
import { useGetFetchWithToken, useMutateWithToken } from '@/hooks/useFetchWithToken';
import { useMutateWithToken } from '@/hooks/useFetchWithToken';
import { DocumentList, Garden, StudyRank } from '@/types';

const Page = ({ params }: { params: { teamId: number } }) => {
const teamInfo = useGetFetchWithToken(getTeamInfo, [params.teamId]);
const { data: teamInfo } = useGetTeamInfoQuery(params.teamId);
const [garden, setGarden] = useState<Garden[]>([]);
const [category, setCategory] = useState<string>(TEAM_CATEGORY_INFOS[0].name);
const [cardIdx, setCardIdx] = useState<number>(0);
Expand All @@ -47,7 +50,7 @@ const Page = ({ params }: { params: { teamId: number } }) => {

getStudies(params.teamId, page, size).then((res) => {
if (res.ok) {
setStudyArray(res.body);
setStudyArray(res.body.content);
}
});
} else if (category === '학습자료') {
Expand All @@ -68,6 +71,7 @@ const Page = ({ params }: { params: { teamId: number } }) => {
getGarden(params.teamId).then((res) => {
setGarden(res.body);
});
TEAM_CATEGORY_INFOS[0].page = `/team/${params.teamId}/study-gallery`;
TEAM_CATEGORY_INFOS[1].page = `/team/${params.teamId}/document`;
}, []);

Expand All @@ -92,7 +96,8 @@ const Page = ({ params }: { params: { teamId: number } }) => {

getStudies(params.teamId, nextPage, size).then((res) => {
if (res.ok) {
if (res.body.length > 0) {
if (res.body.content.length > 0) {
setStudyArray(res.body.content);
setCardIdx((idx) => idx + CARD_PER_PAGE);
}
}
Expand Down Expand Up @@ -137,27 +142,43 @@ const Page = ({ params }: { params: { teamId: number } }) => {
});
};

const myTeam = useAtomValue(myTeamAtom);
const [isMyTeam, setIsMyTeam] = useState<boolean>(false);
useEffect(() => {
if (myTeam.teams !== undefined) {
const res = myTeam.teams.filter((teamId) => teamId === params.teamId);
setIsMyTeam(res.length === 1);
}
}, [myTeam, params.teamId]);

return (
<>
<Flex direction="column" gap="8" w="100%" p="8">
<Flex justify="space-between">
<Title isTeam imageUrl={teamInfo?.imageUrl} name={teamInfo?.name} description={teamInfo?.description} />
{/* TODO 팀원 목록, 초대링크 버튼 */}
<Flex align="center" gap={{ base: '2', lg: '8' }}>
<TeamMember teamId={params.teamId} teamName={teamInfo?.name} />
<Button
color="white"
bg="orange_dark"
onClick={handleInviteClick}
rightIcon={<BsLink45Deg size="24px" />}
rounded="full"
size="sm"
>
초대
</Button>
</Flex>
<Title
isTeam
imageUrl={teamInfo?.body.imageUrl}
name={teamInfo?.body.name}
description={teamInfo?.body.description}
/>
{/* TODO 자신의 팀일때만 보이도록 수정 */}
{isMyTeam && (
<Flex align="center" gap={{ base: '2', lg: '8' }}>
<TeamMember teamId={params.teamId} teamName={teamInfo?.body.name} />
<Button
color="white"
bg="orange_dark"
onClick={handleInviteClick}
rightIcon={<BsLink45Deg size="24px" />}
rounded="full"
size="sm"
>
초대
</Button>
</Flex>
)}
</Flex>
<TeamControlPanel teamInfo={teamInfo} />
<TeamControlPanel teamInfo={teamInfo?.body} />

<Flex pos="relative" align="center" flex="1" gap="8">
<Box pos="relative" overflow="hidden" w="100%" h={{ base: '250px', md: '300px', xl: '320px' }}>
Expand All @@ -173,29 +194,30 @@ const Page = ({ params }: { params: { teamId: number } }) => {
</Box>

{/* TODO 진행도 */}
<AttendanceRate attendanceRate={teamInfo?.attendanceRatio} />
<AttendanceRate attendanceRate={teamInfo?.body.attendanceRatio} />
</Flex>

<Flex direction="column" flex="1" gap="4">
{/* TODO 스터디, 학습자료, 작물창고 버튼 */}
<TabButton currentTab={category} changeTab={handleCategoryChange} categoryInfos={TEAM_CATEGORY_INFOS} />
{category !== '작물창고' && (
<NavigationButton
handlePrevClick={handlePrevClick}
handleNextClick={handleNextClick}
handlePlusClick={handlePlusClick}
isMyTeam={isMyTeam}
/>
)}
{/* TODO 전체보기, 네비게이션 이동 버튼 */}
{/* TODO 스터디 카드 */}
{category === '스터디' && (
{category === '스터디' && studyArray.length === 0 && <SuggestionCreate category="스터디" />}
{category === '스터디' && studyArray.length !== 0 && (
<StudyGridView
studyArray={studyArray.map((study, index) => ({
...study.studyReferenceResponse,
rank: cardIdx + index + 1,
}))}
teamId={params.teamId}
/>
)}
{category === '학습자료' && documentArray.length === 0 && <SuggestionCreate category="학습자료" />}
{category === '학습자료' && <DocumentGridView documentArray={documentArray} />}
</Flex>
</Flex>
Expand Down
28 changes: 28 additions & 0 deletions src/app/team/[teamId]/study-gallery/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { Button, Flex, Text } from '@chakra-ui/react';
import { useState } from 'react';

import StudyModal from '@/containers/study/Modal/StudyModal';
import StudyGallery from '@/containers/study-gallery/StudyGallery';

const Page = ({ params }: { params: { teamId: number } }) => {
const [isOpenModal, setIsOpenModal] = useState(false);

return (
<Flex align="center" direction="column" gap="9" w="100%" p="8">
<Flex justify="space-between" w="100%">
<Flex direction="row" gap="2">
<Text textStyle="bold_2xl">스터디 갤러리</Text>
</Flex>
<Button color="white" bg="orange_dark" onClick={() => setIsOpenModal(true)} rounded="full">
스터디 추가
</Button>
</Flex>
<StudyGallery teamId={params.teamId} />
<StudyModal teamId={params.teamId} isOpen={isOpenModal} setIsModalOpen={setIsOpenModal} studyInfo={null} />
</Flex>
);
};

export default Page;
4 changes: 4 additions & 0 deletions src/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ export const defaultUserAtom = {

export const userAtom = atomWithStorage('user', defaultUserAtom as UserAtomType);

export const myTeamAtom = atomWithStorage<{ teams: number[] }>('myTeam', {
teams: [],
});

export const loginBackPathAtom = atomWithStorage('loginBackPath', '/');
54 changes: 33 additions & 21 deletions src/components/StudyCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { Card, CardHeader, CardBody, CardFooter, Text, Image, Progress } from '@chakra-ui/react';
import { Card, CardHeader, CardBody, CardFooter, Text, Image, Progress, Link } from '@chakra-ui/react';

import CROP from '@/constants/crop';

import { StudyCardProps } from './types';

const StudyCard = ({ name, description, startDate, endDate, cropId, studyProgressRatio, rank }: StudyCardProps) => {
const StudyCard = ({
name,
id,
teamId,
description,
startDate,
endDate,
cropId,
studyProgressRatio,
rank,
}: StudyCardProps) => {
return (
<Card
alignItems="center"
Expand All @@ -17,25 +27,27 @@ const StudyCard = ({ name, description, startDate, endDate, cropId, studyProgres
_hover={{ bg: 'gray.100', transition: '0.5s ease-in-out' }}
rounded="2xl"
>
<CardHeader py="2">
<Text textStyle="bold_md">{name}</Text>
</CardHeader>
<CardBody py="0" textAlign="center" id={cropId.toString()}>
{CROP.filter((crop) => crop.id === cropId).map((crop) => (
<Image key={crop.id} w="16" mx="auto" py="4" alt="crops" src={crop.imageUrl} />
))}
<Text textStyle="sm">{description}</Text>
<Text textStyle="sm">
{startDate} ~ {endDate}
</Text>
</CardBody>
<CardFooter alignItems="center" justifyContent="center" gap="4" display="flex" w="100%" pt="0">
<Card textStyle="bold_md" alignItems="center" w="8" h="8" textAlign="center" shadow="md">
{rank}
</Card>
<Progress flex="1" h="1.5" colorScheme="blackAlpha" rounded="md" value={studyProgressRatio} />
<Text textStyle="sm">{studyProgressRatio}%</Text>
</CardFooter>
<Link href={`/team/${teamId}/study/${id}`}>
<CardHeader py="2">
<Text textStyle="bold_md">{name}</Text>
</CardHeader>
<CardBody py="0" textAlign="center" id={cropId.toString()}>
{CROP.filter((crop) => crop.id === cropId).map((crop) => (
<Image key={crop.id} w="16" mx="auto" py="4" alt="crops" src={crop.imageUrl} />
))}
<Text textStyle="sm">{description}</Text>
<Text textStyle="sm">
{startDate} ~ {endDate}
</Text>
</CardBody>
<CardFooter alignItems="center" justifyContent="center" gap="4" display="flex" w="100%" pt="0">
<Card textStyle="bold_md" alignItems="center" w="8" h="8" textAlign="center" shadow="md">
{rank}
</Card>
<Progress flex="1" h="1.5" colorScheme="blackAlpha" rounded="md" value={studyProgressRatio} />
<Text textStyle="sm">{studyProgressRatio}%</Text>
</CardFooter>
</Link>
</Card>
);
};
Expand Down
68 changes: 68 additions & 0 deletions src/containers/study-gallery/StudyGallery/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Flex, Grid, useBreakpointValue } from '@chakra-ui/react';
import { useEffect, useState } from 'react';

import { getStudies } from '@/app/api/study';
import PageNavigator from '@/components/PageNavigator';
import StudyCard from '@/components/StudyCard';
import SuggestionCreate from '@/containers/team/SuggestionCreate';
import { StudyRank } from '@/types';

const StudyGallery = ({ teamId }: { teamId: number }) => {
const [currentPage, setCurrentPage] = useState<number>(1);
const [studyArray, setStudyArray] = useState<StudyRank[]>([]);
const [cardIdx, setCardIdx] = useState<number>(0);

const [studyLength, setStudyLength] = useState<number>(0);

const itemsPerPage = useBreakpointValue({ base: 4, md: 8, xl: 10 })!;

useEffect(() => {
getStudies(teamId, currentPage - 1, itemsPerPage).then((res) => {
if (res.ok) {
setStudyArray(res.body.content);
setStudyLength(res.body.totalElements);
}
});
setCardIdx((currentPage - 1) * itemsPerPage);
}, [currentPage, itemsPerPage]);

if (studyArray.length === 0) {
return <SuggestionCreate category="스터디" />;
}

return (
<Flex direction="column">
<Grid gap={{ sm: '2', md: '4', xl: '8' }} templateColumns={`repeat(${itemsPerPage / 2}, 1fr)`} w="100%">
{studyArray
.map((study, index) => ({
...study.studyReferenceResponse,
rank: cardIdx + index + 1,
}))
.map((study) => (
<StudyCard
key={study.id}
teamId={teamId}
id={study.id}
name={study.name}
description={study.description}
startDate={study.startDate}
endDate={study.endDate}
status={study.status}
cropId={study.cropId}
studyProgressRatio={study.studyProgressRatio}
rank={study.rank}
/>
))}
</Grid>
<PageNavigator
currentPage={currentPage}
setCurrentPage={setCurrentPage}
componentLength={studyLength}
itemsPerPage={itemsPerPage}
/>
</Flex>
);
};

export default StudyGallery;
Loading

0 comments on commit 4b05cc9

Please sign in to comment.