Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#247 팀 rud api 연결 #248

Merged
merged 8 commits into from
Jun 29, 2024
6 changes: 3 additions & 3 deletions src/app/api/team.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EditTeamDto } from '@/types';
import { Team } from '@/types';

import { fetcher } from './fetcher';

Expand All @@ -10,10 +10,10 @@ const postCreateTeam = (team: FormData) =>
body: team,
});

const putEditTeam = (teamId: number, team: EditTeamDto) =>
const putEditTeam = (teamId: number, teamInfo: Pick<Team, 'name' | 'description'>) =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

아하! 이런 식으로 Pick 해서 쓰는 거군요! 새로 배워갑니다🤩

teamFetcher(`/teams/${teamId}`, {
method: 'PUT',
body: team,
body: teamInfo,
});

const patchEditTeamImage = (teamId: number, file: FormData) =>
Expand Down
10 changes: 8 additions & 2 deletions src/app/team/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import AssetGridView from '@/containers/team/AssetGridView';
import AttendanceRate from '@/containers/team/AttendanceRate';
import NavigationButton from '@/containers/team/NavigationButton';
import StudyGridView from '@/containers/team/StudyGridView';
import TeamControlPanel from '@/containers/team/TeamControlPanel';
import TeamMember from '@/containers/team/teamMember';
import { gardenInfos1 } from '@/mocks/Garden3D';
import studyAssetCardData from '@/mocks/studyAssetCard';
import studyCardData from '@/mocks/studyCard';
import teamInfoData from '@/mocks/teamInfo';

const Page = ({ params }: { params: { teamId: number } }) => {
// TODO 팀 조회 연결
const teamInfo = teamInfoData;
Copy link
Member Author

Choose a reason for hiding this comment

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

teamInfoData는 현재 mocks데이터입니다! api 연결 필요합니다


const [category, setCategory] = useState<string>(TEAM_CATEGORY_INFOS[0].name);
const [cardIdx, setCardIdx] = useState<number>(0);

Expand Down Expand Up @@ -84,7 +89,7 @@ const Page = ({ params }: { params: { teamId: number } }) => {
<>
<Flex direction="column" gap="8" w="100%" p="8">
<Flex justify="space-between">
<Title isTeam name="열사모" description="팀입니다" />
<Title isTeam imageUrl={teamInfo.imageUrl} name={teamInfo.name} description={teamInfo.description} />
{/* TODO 팀원 목록, 초대링크 버튼 */}
<Flex align="center" gap={{ base: '2', lg: '8' }}>
<TeamMember />
Expand All @@ -93,6 +98,7 @@ const Page = ({ params }: { params: { teamId: number } }) => {
</Button>
</Flex>
</Flex>
<TeamControlPanel teamInfo={teamInfo} />

<Flex pos="relative" align="center" flex="1" gap="8">
{/* TODO 잔디 */}
Expand All @@ -109,7 +115,7 @@ const Page = ({ params }: { params: { teamId: number } }) => {
</Box>

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

<Flex direction="column" flex="1" gap="4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar/SidebarContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const SidebarContent = ({ isOpen, setIsOpen }: SidebarContentProps) => {
</>
)}
</Flex>
<TeamModal isOpen={isTeamModalOpen} setIsOpen={setIsTeamModalOpen} />
<TeamModal isOpen={isTeamModalOpen} onClose={() => setIsTeamModalOpen(false)} />
Copy link
Member Author

Choose a reason for hiding this comment

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

이 부분 제맘대로 슬쩍 바꿨는데요! 사실 모달창에서 모달을 닫는 작업만 필요하기 때문에, setter로 넘겨줘야하나 싶었습니다! 그래서 모달창 닫는 함수로 넘겨줬습니다!

</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/Title/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Text, Flex, Avatar, Box } from '@chakra-ui/react';

import { TitleProps } from './types';

const Title = ({ isTeam = false, teamImg, name, description }: TitleProps) => {
const Title = ({ isTeam, name, description, imageUrl }: TitleProps) => {
return (
<Flex pos="relative" align="center" gap="3">
{isTeam && (
Expand All @@ -11,7 +11,7 @@ const Title = ({ isTeam = false, teamImg, name, description }: TitleProps) => {
borderColor="gray.100"
shadow="none"
size="md"
src={teamImg || '/images/doore_logo.png'}
src={imageUrl ?? '/images/doore_logo.png'}
Copy link
Member Author

Choose a reason for hiding this comment

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

여기는 null 병합 연산자(??)로 넣어줬습니다! || 는 Falsy값이 다 들어가기 때문에, 확실하게 undefined/null만 판단하도록 ?? 로 바꿨습니다!

그런데 백엔드에 여쭤보니까, 이미지 아무것도 안넣으면, 백엔드에서 기본 디폴트 이미지 url을 던져준다고 하더라구요! 그래서 사실 ?? '/images/doore_logo.png' 로 프론트상에서 디폴트 이미지 지정해주는게 필요없어지긴 합니다!

그런데 현재는 이미지 아무것도 안넣었을때, null을 던져준다고 합니다!(추후 디폴트 url로 바꿀 예정이래요)

Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 api 연결 오류로 undefined 일 수 있으니 이후에도 계속 유지해도 될듯 합니다!

/>
)}
<Text textStyle="bold_3xl">{name}</Text>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Title/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface TitleProps {
isTeam?: boolean;
teamImg?: string;
name: string;
description: string;
imageUrl?: string;
}
32 changes: 32 additions & 0 deletions src/containers/team/DeleteTeamModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Text } from '@chakra-ui/react';

import { deleteTeam } from '@/app/api/team';
import ConfirmModal from '@/components/Modal/ConfirmModal';

import { DeleteTeamModalProps } from './type';

const DeleteTeamModal = ({ id, name, isOpen, onClose }: DeleteTeamModalProps) => {
const handleDeleteTeamButtonClick = () => {
deleteTeam(id).then(() => {
onClose();
});
};

return (
<ConfirmModal
isOpen={isOpen}
onClose={onClose}
title="팀 삭제"
confirmButtonText="삭제"
onConfirmButtonClick={() => handleDeleteTeamButtonClick()}
>
<Text align="center">
삭제된 팀은 되돌릴 수 없습니다.
<br />
{name} 팀을 삭제하시겠습니까?
</Text>
</ConfirmModal>
);
};

export default DeleteTeamModal;
6 changes: 6 additions & 0 deletions src/containers/team/DeleteTeamModal/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Team } from '@/types';

export interface DeleteTeamModalProps extends Pick<Team, 'id' | 'name'> {
isOpen: boolean;
onClose: () => void;
}
57 changes: 57 additions & 0 deletions src/containers/team/TeamControlPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button, Flex } from '@chakra-ui/react';
import { useState } from 'react';

import { TeamControlPanelProps } from './types';
import DeleteTeamModal from '../DeleteTeamModal';
import TeamModal from '../TeamModal';

const TeamControlPanel = ({ teamInfo }: TeamControlPanelProps) => {
const [isEditModalOpen, setIsEditModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);

return (
<Flex gap="2" mb="8">
<Button
w="fit-content"
px="4"
py="1"
color="white"
bg="orange"
shadow="md"
_hover={{ bg: 'orange' }}
aria-label=""
onClick={() => setIsEditModalOpen(true)}
size="xs"
>
수정
</Button>
<Button
w="fit-content"
px="4"
py="1"
color="black"
bg="white"
shadow="md"
_hover={{ bg: 'white' }}
aria-label="delete-team-button"
onClick={() => setIsDeleteModalOpen(true)}
size="xs"
>
Copy link
Member Author

Choose a reason for hiding this comment

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

요 부분은 스터디코드 슬쩍해와씁니다!

삭제
</Button>
{isEditModalOpen && (
<TeamModal teamInfo={teamInfo} isOpen={isEditModalOpen} onClose={() => setIsEditModalOpen(false)} />
)}
{isDeleteModalOpen && (
<DeleteTeamModal
id={teamInfo?.id}
name={teamInfo?.name}
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
/>
)}
</Flex>
);
};

export default TeamControlPanel;
5 changes: 5 additions & 0 deletions src/containers/team/TeamControlPanel/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TeamDetail } from '@/types';

export interface TeamControlPanelProps {
teamInfo: TeamDetail;
}
93 changes: 69 additions & 24 deletions src/containers/team/TeamModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';

import { Flex, Text, Textarea, Image } from '@chakra-ui/react';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { BiEdit, BiFile } from 'react-icons/bi';

import { postCreateTeam } from '@/app/api/team';
import { patchEditTeamImage, postCreateTeam, putEditTeam } from '@/app/api/team';
import IconBox from '@/components/IconBox';
import ActionModal from '@/components/Modal/ActionModal';

Expand All @@ -18,52 +18,92 @@ const AlertContent = ({ message }: { message: string }) => {
);
};

const TeamModal = ({ isOpen, setIsOpen }: TeamModalProps) => {
const TeamModal = ({ teamInfo, isOpen, onClose }: TeamModalProps) => {
const inputFileRef = useRef<HTMLInputElement>(null);
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [thumbnailPath, setThumbnailPath] = useState<string>('');
Copy link
Member Author

Choose a reason for hiding this comment

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

thumbnailPath는 서버에서 받는 이미지 주소입니다

const [thumbnail, setThumbnail] = useState<File | null>();
const [alertName, setAlertName] = useState<boolean>(false);
const [alertDescription, setAlertDescription] = useState<boolean>(false);

const onClose = () => {
const resetState = () => {
setName('');
setDescription('');
setThumbnailPath('');
setThumbnail(null);
setAlertName(false);
setAlertDescription(false);
setIsOpen(false);
};

const onSave = () => {
if (name === '') setAlertName(true);
else if (description === '') setAlertDescription(true);
else {
const teamForm = new FormData();
const request = {
const resetAndCloseModal = () => {
resetState();
onClose();
};

const isTeamInfoValid = () => {
const isValidName = name.trim() !== '';
const isValidDescription = description.trim() !== '';
setAlertName(!isValidName);
setAlertDescription(!isValidDescription);

return isValidName && isValidDescription;
};
Comment on lines +44 to +51
Copy link
Collaborator

Choose a reason for hiding this comment

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

함수 분리하니까 예쁘네요👍
스터디 모달도 참고해야겠어요~!


const handleEditTeamButtonClick = () => {
if (!isTeamInfoValid()) return;

if (teamInfo) {
putEditTeam(teamInfo.id, {
name,
description,
};
const requestBlob = new Blob([JSON.stringify(request)], { type: 'application/json' });

teamForm.append('request', requestBlob);
teamForm.append('file', thumbnail as Blob);
}).then(() => {
if (thumbnail) {
const teamForm = new FormData();
teamForm.append('file', thumbnail as Blob);

postCreateTeam(teamForm).then(() => {
onClose();
patchEditTeamImage(teamInfo.id, teamForm).then(() => {});
}
resetAndCloseModal();
});
}
};

const handleAddTeamButtonClick = () => {
if (!isTeamInfoValid) return;

const teamForm = new FormData();
const request = {
name,
description,
};
const requestBlob = new Blob([JSON.stringify(request)], { type: 'application/json' });

teamForm.append('request', requestBlob);
teamForm.append('file', thumbnail as Blob);

postCreateTeam(teamForm).then(() => {
resetAndCloseModal();
});
};
Copy link
Member Author

Choose a reason for hiding this comment

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

handleEditTeamButtonClick, handleAddTeamButtonClick을 잘하면 좀 더 깔끔하게 짤 수 있지 않을까.. 했지만,, 지금은 아무 생각도 나지 않기때문에,,, 배포이후로 미루겠습니다 핫핫!

혹시 좋은 방법있으시면 추천 부탁드립니다!


useEffect(() => {
if (teamInfo) {
setName(teamInfo.name);
setDescription(teamInfo.description);
setThumbnailPath(teamInfo.imageUrl ?? '');
}
}, [teamInfo]);

return (
<ActionModal
isOpen={isOpen}
onClose={onClose}
title="팀 생성"
onClose={resetAndCloseModal}
title={`팀 ${teamInfo ? '수정' : '생성'}`}
subButtonText="취소"
onSubButtonClick={onClose}
mainButtonText="저장"
onMainButtonClick={onSave}
onSubButtonClick={resetAndCloseModal}
Copy link
Member Author

Choose a reason for hiding this comment

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

resetAndCloseModal 이름보다는 사실 handleCloseModalButtonClick, handleCancleButtonClick 이런식으로 핸들러 이름으로 짓고 싶었는데요.. (101, 104 line)

state를 초기화하고 modal을 닫는 동작이, 해당 104 line 말고 101, 67, 87 line에 다 들어간단 말이죠..
그런데 101, 67, 87 line 에서 handleCloseModalButtonClick 을 호출한다는게 어색해보여서, 그냥 resetAndCloseModal 으로 지었습니다!

물론

const `handleCloseModalButtonClick`  = () => {resetAndCloseModal();}
const  `handleCancleButtonClick` = () =>  {resetAndCloseModal();}

이렇게 함수 2개 만들면 핸들러 네이밍 붙힐 수 있긴 한데용,, 뭔가 굳이라는 생각이 들었습니다....

mainButtonText={teamInfo ? '수정' : '생성'}
Copy link
Collaborator

Choose a reason for hiding this comment

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

피그마엔 '생성' 대신 '저장'으로 되어있어서 스터디 모달도 '저장'으로 되어있는데, 하나로 통일하는 게 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

음음 혹시 생성은 어떨까용?? 왜냐면 모달창 이름이 스터디/팀 생성 이라, 뭔가 버튼 이름이 저장이면 조금 혼란스러울 것 같아서요!요!

onMainButtonClick={teamInfo ? handleEditTeamButtonClick : handleAddTeamButtonClick}
>
<Flex direction="column" gap="10" w="100%">
<Flex direction="column">
Expand Down Expand Up @@ -112,6 +152,7 @@ const TeamModal = ({ isOpen, setIsOpen }: TeamModalProps) => {
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
setThumbnail(e.target.files[0]);
setThumbnailPath('');
Copy link
Member Author

Choose a reason for hiding this comment

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

여기서는 만약 사용자가 이미지를 업로드 했다면, 서버 이미지를 그냥 빈 문자열로 바꿔줬습니다. 165 line에 보이는 것처럼, 둘중 하나만 보이도록 하려구요! 근데 이렇게 되면 다시 이미지 되돌리기를 못한다는 단점이 있습니다..

사용자 입장에서는 이미지를 혹시라도 잘못 올리면(api는 안보냈지만, 프론트 상에서 이미지를 올렸을때), 원래 이미지로 못 돌아가고, 취소하기 위해서는 아예 모달창을 껐다 켜야 합니다..

이부분은 썸네일 올리는 부분에 [취소] 버튼을 넣는다거나 해서... 아예 이미지 업로드 공통 컴포넌트를 만드는게 좋아보입니다! (배포 이후...)

}
}}
/>
Expand All @@ -121,7 +162,11 @@ const TeamModal = ({ isOpen, setIsOpen }: TeamModalProps) => {
content={thumbnail ? thumbnail.name : '파일을 추가해주세요.'}
handleClick={() => inputFileRef.current?.click()}
/>
{thumbnail && <Image w="40" alt="thumbnail" src={URL.createObjectURL(thumbnail)} />}
{thumbnailPath ? (
<Image w="40" alt="thumbnail" src={thumbnailPath} />
) : (
thumbnail && <Image w="40" alt="thumbnail" src={URL.createObjectURL(thumbnail)} />
)}
Copy link
Member Author

Choose a reason for hiding this comment

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

thumbnailPath은 백엔드에서 가져온 url고, thumbnail은 사용자가 올린 FIle 객체입니다.
맨 처음 수정 모달을 켰을때, 현재 팀 이미지가 보이는게 맞는 것 같아, 있을 경우에 기본으로 뒀고,

  1. 서버 이미지 있다면 => 서버 이미지
  2. 사용자가 이미지 업로드 했다면 => 자기가 올린 이미지

둘 중 1개만 보이도록 했습니다!

</Flex>
</Flex>
</ActionModal>
Expand Down
5 changes: 4 additions & 1 deletion src/containers/team/TeamModal/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Team } from '@/types';

export interface TeamModalProps {
teamInfo?: Team;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
}
10 changes: 10 additions & 0 deletions src/mocks/teamInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { TeamDetail } from '@/types';

const teamInfoData: TeamDetail = {
id: 99,
name: '열사모',
description: '팀 설명이옵니다',
imageUrl: 'https://doo-re-dev-bucket.s3.ap-northeast-2.amazonaws.com/images/005bd2bf-b000-47e2-b092-f5210416ecbc.png',
Copy link
Member Author

Choose a reason for hiding this comment

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

이건 백엔드 이미지 아무거나 가져왔습니다!

attendanceRate: 50,
};
export default teamInfoData;
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ export interface EditStudyDto {
status: string;
}

export interface EditTeamDto {
export interface Team {
readonly id: number;
name: string;
description: string;
imageUrl: string;
}

export interface TeamDetail extends Team {
attendanceRate: number;
Copy link
Member Author

Choose a reason for hiding this comment

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

TeamDetail은 팀 조회에서만 쓰입니다.
백엔드 엔티티를 보니 attendanceRate는 들어가있지 않고, 이건 백엔드에서 따로 계산해서 주는 값이더라구요. 여쭤보니 팀 조회말고 다른데서 쓰이는 곳이 없을거라 하셔서, Team 타입에 옵셔널로 attendanceRate 을 넣는것보다, 따로 타입을 만들어주는 편이 깔끔하다 생각했습니다!

export interface Team {
  readonly id: number;
  name: string;
  description: string;
  imageUrl: string;
  attendanceRate? number;
} 이것보다요!

Copy link
Member Author

Choose a reason for hiding this comment

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

그리고 TeamDetail 타입명 짓기가 조금 어려웠는데요.. 제가 Team 타입의 데이터를 저장하는 변수를 만들때, team 보다는 teamInfo 로 지었단 말이죠(변수명이 team이면 조금 와닿지 않아서, info를 붙혀줬었습니다) 밑에 코드처럼요!

const teamInfo: Team = {어쩌구 저쩌구}

그래서 보통 teamInfo를 붙혀주니까, 팀 조회에서 쓰여야 할 "팀 정보"의 타입(지금 보이는 TeamDetail)을 어떻게 정해야할지 고민이 되더라구요.. teamWithAttendanceRate 는 너무 긴 것 같고,, 그래서 api 명이 "팀 상세 조회"니까, 상세를 강조하자..! 싶어서 detail를 붙혔습니다..!

더 좋은 방법/네이밍 있으시면 추천부탁드립니닷!!

}

export interface Curriculum {
Expand Down