Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/GU-99/grow-up-fe into fe…
Browse files Browse the repository at this point in the history
…ature/#279-fix-image-upload-error
  • Loading branch information
Yoonyesol committed Dec 7, 2024
2 parents 37e7ba3 + fc3be2e commit d4d043b
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 103 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: deploy-workflow

on:
push:
branches:
- main
workflow_dispatch:

jobs:
aws-deploy:
runs-on: ubuntu-latest
steps:
# Github workspace 로컬 다운로드
- name: Checkout branch
uses: actions/checkout@v4

# NodeJS 설치
- name: Setup Node.js environment v20
uses: actions/setup-node@v4
with:
node-version: 20

# Yarn 의존성 설치
- name: Install yarn dependencies
run: yarn install

# 빌드용 환경 변수 .env 파일 생성
- name: Create .env file for Vite
run: |
echo "VITE_BASE_URL=${{ secrets.VITE_BASE_URL }}" >> .env
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" >> .env
shell: bash

# 프로젝트 빌드
- name: Build the project
run: yarn build

# AWS IAM credentials 설정
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}

# S3에 빌드 파일 배포
- name: Deploy file to S3
run: |
aws s3 sync --delete --region ${{ secrets.AWS_REGION }} ./dist ${{ secrets.AWS_S3_BUCKET }}
# CloudFront 캐시 무효화
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
12 changes: 6 additions & 6 deletions src/components/modal/team/UpdateModalTeam.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function UpdateModalTeam({ teamId, onClose: handleClose }: Update
const [keyword, setKeyword] = useState('');
const { toastInfo } = useToast();

const { coworkers, isLoading: isTeamCoworkersLoading } = useReadTeamCoworkers(teamId);
const { teamCoworkers, isLoading: isTeamCoworkersLoading } = useReadTeamCoworkers(teamId);
const { teamList, isLoading: isTeamListLoading } = useReadTeams();
const { teamInfo } = useReadTeamInfo(Number(teamId));
const teamNameList = useMemo(() => getTeamNameList(teamList, teamInfo?.teamName), [teamList, teamInfo?.teamName]);
Expand All @@ -66,10 +66,10 @@ export default function UpdateModalTeam({ teamId, onClose: handleClose }: Update
} = methods;

useEffect(() => {
if (teamInfo?.teamName && teamInfo?.content && coworkers) {
reset({ teamName: teamInfo.teamName, content: teamInfo.content, coworkers });
if (teamInfo?.teamName && teamInfo?.content && teamCoworkers) {
reset({ teamName: teamInfo.teamName, content: teamInfo.content });
}
}, [teamInfo, coworkers, reset]);
}, [teamInfo, teamCoworkers, reset]);

const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value.trim());
Expand All @@ -81,7 +81,7 @@ export default function UpdateModalTeam({ teamId, onClose: handleClose }: Update
};

const handleCoworkersClick = (userId: User['userId'], roleName: TeamRoleName) => {
const isIncludedUser = coworkers.find((coworker) => coworker.userId === userId);
const isIncludedUser = teamCoworkers.find((coworker) => coworker.userId === userId);
if (isIncludedUser) return toastInfo('이미 포함된 팀원입니다');

addTeamCoworkerMutate({ userId, roleName });
Expand Down Expand Up @@ -142,7 +142,7 @@ export default function UpdateModalTeam({ teamId, onClose: handleClose }: Update
onUserClick={(user) => handleCoworkersClick(user.userId, TEAM_DEFAULT_ROLE)}
/>
<div className="flex flex-wrap">
{coworkers.map(({ userId, nickname, roleName }) => (
{teamCoworkers.map(({ userId, nickname, roleName }) => (
<UserRoleSelectBox
key={userId}
userId={userId}
Expand Down
8 changes: 8 additions & 0 deletions src/components/project/EmptyProjectItemList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function EmptyProjectItemList() {
return (
<div className="flex h-full items-center justify-center text-center">
진행중인 프로젝트가 없습니다! <br />
새로운 프로젝트를 생성해보세요 😄
</div>
);
}
89 changes: 89 additions & 0 deletions src/components/project/ProjectItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Link } from 'react-router-dom';
import { FaRegTrashAlt } from 'react-icons/fa';
import { IoIosSettings } from 'react-icons/io';
import useModal from '@hooks/useModal';
import useToast from '@hooks/useToast';
import { useDeleteProject, useReadProjectCoworkers } from '@hooks/query/useProjectQuery';
import useStore from '@stores/useStore';
import UpdateModalProject from '@components/modal/project/UpdateModalProject';

import type { Team } from '@/types/TeamType';
import type { Project } from '@/types/ProjectType';

type ProjectItemProps = {
teamId: Team['teamId'];
project: Project;
};

export default function ProjectItem({ teamId, project }: ProjectItemProps) {
const { toastWarn } = useToast();
const { showModal: showUpdateModal, openModal: openUpdateModal, closeModal: closeUpdateModal } = useModal();
const {
userInfo: { userId },
} = useStore();

const { projectCoworkers } = useReadProjectCoworkers(project.projectId);
const { mutate: deleteProjectMutate } = useDeleteProject(teamId);

const userProjectRole = projectCoworkers.find((coworker) => coworker.userId === userId)?.roleName;

const handleOpenUpdateModal = () => {
if (userProjectRole !== 'ADMIN') return toastWarn('프로젝트 수정 권한이 없습니다.');
openUpdateModal();
};

const handleDeleteClick = (projectId: Project['projectId']) => {
if (userProjectRole !== 'ADMIN') return toastWarn('프로젝트 삭제 권한이 없습니다.');
deleteProjectMutate(projectId);
};

return (
<>
<li key={project.projectId} className="min-w-300 space-y-2 text-sm">
<Link to={`/teams/${teamId}/projects/${project.projectId}`} className="flex h-50 items-center border p-8">
<div className="flex max-h-full grow">
<div className="max-h-full w-60 shrink-0">
<small className="flex flex-col text-xs font-bold text-category">project</small>
<p className="truncate">{project.projectName}</p>
</div>

<div className="flex max-h-full max-w-350 flex-col px-4">
<small className="text-xs font-bold text-category">desc</small>
<p className="truncate">{project.content}</p>
</div>
</div>

<div className="mr-6 flex shrink-0 space-x-10">
<button
type="button"
className="flex items-center text-main hover:brightness-50"
aria-label="Settings"
onClick={(e) => {
e.preventDefault();
handleOpenUpdateModal();
}}
>
<IoIosSettings size={20} className="mr-2" />
setting
</button>

<button
type="button"
className="hover:brightness-200"
aria-label="Delete"
onClick={(e) => {
e.preventDefault();
handleDeleteClick(project.projectId);
}}
>
<FaRegTrashAlt size={20} />
</button>
</div>
</Link>
</li>
{showUpdateModal && project.projectId && (
<UpdateModalProject projectId={project.projectId} onClose={closeUpdateModal} />
)}
</>
);
}
18 changes: 18 additions & 0 deletions src/components/project/ProjectItemList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ProjectItem from '@components/project/ProjectItem';
import type { Team } from '@/types/TeamType';
import type { Project } from '@/types/ProjectType';

type ProjectItemListProps = {
teamId: Team['teamId'];
projectList: Project[];
};

export default function ProjectItemList({ teamId, projectList }: ProjectItemListProps) {
return (
<ul>
{projectList.map((project) => (
<ProjectItem key={project.projectId} teamId={teamId} project={project} />
))}
</ul>
);
}
15 changes: 10 additions & 5 deletions src/components/user/auth-form/ProfileImageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { USER_SETTINGS } from '@constants/settings';
import { JPG, PNG, SVG, WEBP } from '@constants/mimeFileType';
import useAxios from '@hooks/useAxios';
import useToast from '@hooks/useToast';
import { useUploadProfileImage } from '@hooks/query/useUserQuery';
import { useDeleteProfileImage, useUploadProfileImage } from '@hooks/query/useUserQuery';
import useStore from '@stores/useStore';
import { getProfileImage } from '@services/userService';

Expand All @@ -17,8 +17,9 @@ type ProfileImageContainerProps = {

export default function ProfileImageContainer({ imageUrl, setImageUrl }: ProfileImageContainerProps) {
const { toastWarn } = useToast();
const { editUserInfo, userInfo } = useStore();
const { userInfo } = useStore();
const { mutate: uploadImageMutate } = useUploadProfileImage();
const { mutateAsync: deleteImageMutateAsync } = useDeleteProfileImage();
const { fetchData } = useAxios(getProfileImage);
const { toastError } = useToast();

Expand Down Expand Up @@ -63,9 +64,13 @@ export default function ProfileImageContainer({ imageUrl, setImageUrl }: Profile
uploadImageMutate({ file });
};

const handleRemoveImg = () => {
setImageUrl('');
editUserInfo({ fileName: null });
const handleRemoveImg = async () => {
try {
await deleteImageMutateAsync();
setImageUrl('');
} catch (error) {
console.error('이미지 삭제 중 에러 발생:', error);
}
};

return (
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/query/useTeamQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export function useUpdateTeamCoworkerRole(teamId: Team['teamId']) {
// 팀원 목록 조회
export function useReadTeamCoworkers(teamId: Team['teamId']) {
const {
data: coworkers = [] as TeamCoworker[],
data: teamCoworkers = [],
isLoading,
isError,
} = useQuery({
Expand All @@ -263,5 +263,5 @@ export function useReadTeamCoworkers(teamId: Team['teamId']) {
},
});

return { coworkers, isLoading, isError };
return { teamCoworkers, isLoading, isError };
}
24 changes: 23 additions & 1 deletion src/hooks/query/useUserQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import useToast from '@hooks/useToast';
import { updateLinks, updateUserInfo, uploadProfileImage } from '@services/userService';
import { deleteProfileImage, updateLinks, updateUserInfo, uploadProfileImage } from '@services/userService';
import useStore from '@stores/useStore';
import { generateLinksQueryKey, generateProfileFileQueryKey, generateUserInfoQueryKey } from '@utils/queryKeyGenerator';
import type { EditUserInfoRequest, EditUserLinksForm } from '@/types/UserType';
Expand Down Expand Up @@ -48,6 +48,28 @@ export function useUploadProfileImage() {
return mutation;
}

export function useDeleteProfileImage() {
const queryClient = useQueryClient();
const { toastSuccess, toastError } = useToast();
const { userInfo, editUserInfo } = useStore();
const userProfileImageQueryKey = generateProfileFileQueryKey(userInfo.userId);

const mutation = useMutation({
mutationFn: () => deleteProfileImage(),
onError: () => toastError('이미지 삭제에 실패했습니다. 다시 시도해 주세요.'),
onSuccess: () => {
toastSuccess('이미지가 삭제되었습니다.');
editUserInfo({ profileImageName: null });
queryClient.invalidateQueries({ queryKey: userProfileImageQueryKey });
},
});

return {
...mutation,
mutateAsync: mutation.mutateAsync,
};
}

export function useUpdateLinks() {
const { userInfo } = useStore();
const queryClient = useQueryClient();
Expand Down
18 changes: 10 additions & 8 deletions src/layouts/page/TeamLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@ import { useReadTeams } from '@hooks/query/useTeamQuery';
import Spinner from '@components/common/Spinner';

export default function TeamLayout() {
const { showModal: showTeamModal, openModal: openTeamModal, closeModal: closeTeamModal } = useModal();
const location = useLocation();
const { teamId } = useParams();
const { joinedTeamList: teamData, isLoading: isTeamLoading } = useReadTeams();
const selectedTeam = useMemo(() => teamData.find((team) => team.teamId.toString() === teamId), [teamId, teamData]);
const { showModal: showTeamModal, openModal: openTeamModal, closeModal: closeTeamModal } = useModal();
const { joinedTeamList, isLoading: isTeamLoading } = useReadTeams();

const selectedTeam = useMemo(
() => joinedTeamList.find((team) => team.teamId.toString() === teamId),
[teamId, joinedTeamList],
);
const hasProjectRoute = location.pathname.split('/').includes('projects');

if (isTeamLoading) {
return <Spinner />;
}
if (isTeamLoading) return <Spinner />;

if (!selectedTeam && teamId) return <Navigate to="/error" replace />;

Expand All @@ -27,10 +29,10 @@ export default function TeamLayout() {
<>
<section className="flex h-full gap-10 p-15">
<ListSidebar title="팀 목록" showButton text="팀 생성" onClick={openTeamModal}>
<ListTeam data={teamData} targetId={teamId} />
<ListTeam data={joinedTeamList} targetId={teamId} />
</ListSidebar>
<section className="flex grow flex-col border border-list bg-contents-box">
{teamData.length === 0 ? (
{joinedTeamList.length === 0 ? (
<div className="flex h-full items-center justify-center text-center">
소속된 팀이 없습니다! <br />
팀을 생성하여 다른 사람들과 함께 프로젝트를 관리해보세요 😄
Expand Down
29 changes: 29 additions & 0 deletions src/mocks/services/userServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,35 @@ const userServiceHandler = [
},
});
}),
// 유저 프로필 이미지 삭제 API
http.delete(`${API_URL}/user/profile/image`, async ({ request }) => {
const accessToken = request.headers.get('Authorization');
if (!accessToken) return new HttpResponse(null, { status: 401 });

const userId = convertTokenToUserId(accessToken);
if (!userId) {
return HttpResponse.json({ message: '토큰에 유저 정보가 존재하지 않습니다.' }, { status: 401 });
}

const userIndex = USER_DUMMY.findIndex((user) => user.userId === userId);
if (userIndex === -1) {
return HttpResponse.json(
{ message: '해당 사용자를 찾을 수 없습니다. 입력 정보를 확인해 주세요.' },
{ status: 401 },
);
}

USER_DUMMY[userIndex].profileImageName = null;

const fileIndex = PROFILE_IMAGE_DUMMY.findIndex((file) => file.userId === userId);
if (fileIndex === -1) {
return HttpResponse.json({ message: '삭제할 프로필 이미지가 없습니다.' }, { status: 404 });
}

PROFILE_IMAGE_DUMMY.splice(fileIndex, 1);

return new HttpResponse(null, { status: 204 });
}),
// 전체 팀 목록 조회 API (가입한 팀, 대기중인 팀)
http.get(`${API_URL}/user/team`, ({ request }) => {
const accessToken = request.headers.get('Authorization');
Expand Down
Loading

0 comments on commit d4d043b

Please sign in to comment.