Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/apis/setting/useDeleteMyProfileImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { CommonResponse } from '../../types/common';
import { axiosInstance } from '../axios';
import { useMutation } from '@tanstack/react-query';
import queryClient from '../../utils/queryClient';
import { queryKey } from '../../constants/queryKey';

// 설정 - 나의 프로필 이미지 삭제
const deleteMyProfileImage = async (): Promise<CommonResponse<{}>> => {
try {
const response = await axiosInstance.delete('/api/workspace/setting/my-profile/profileImage');
return response.data;
} catch (error) {
console.error('나의 프로필 이미지 삭제 실패', error);
throw error;
}
};

export const useDeleteMyProfileImage = () => {
return useMutation({
mutationFn: deleteMyProfileImage,
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [queryKey.MY_PROFILE],
});
},
});
};
22 changes: 22 additions & 0 deletions src/apis/setting/useGetMyProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { axiosInstance } from '../axios';
import type { MyProfile } from '../../types/setting';
import { useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey';

// 설정 - 나의 프로필 조회
const getMyProfile = async (): Promise<MyProfile> => {
try {
const response = await axiosInstance.get('/api/workspace/setting/my-profile');
return response.data.result;
} catch (error) {
console.error('나의 프로필 조회 실패', error);
throw error;
}
};

export const useGetMyProfile = () => {
return useQuery({
queryKey: [queryKey.MY_PROFILE],
queryFn: getMyProfile,
});
};
22 changes: 22 additions & 0 deletions src/apis/setting/useGetWorkspaceProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Workspace } from '../../types/setting';
import { axiosInstance } from '../axios';
import { useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey';

// 설정 - 워크스페이스 프로필 조회
const getWorkspaceProfile = async (): Promise<Workspace> => {
try {
const response = await axiosInstance.get('/api/workspace/setting/profile');
return response.data.result;
} catch (error) {
console.error('워크스페이스 프로필 조회 실패', error);
throw error;
}
};

export const useGetWorkspaceProfile = () => {
return useQuery({
queryKey: [queryKey.WORKSPACE_PROFILE],
queryFn: getWorkspaceProfile,
});
};
35 changes: 35 additions & 0 deletions src/apis/setting/usePatchMyProfileImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { axiosInstance } from '../axios';
import type { MyProfileImage } from '../../types/setting';
import { useMutation } from '@tanstack/react-query';
import queryClient from '../../utils/queryClient';
import { queryKey } from '../../constants/queryKey';

// 설정 - 나의 프로필 이미지 변경
const patchMyProfileImage = async (formData: FormData): Promise<MyProfileImage> => {
try {
const response = await axiosInstance.patch(
'/api/workspace/setting/my-profile/profileImage',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return response.data.result;
} catch (error) {
console.error('나의 프로필 이미지 변경 실패', error);
throw error;
}
};

export const usePatchMyProfileImage = () => {
return useMutation({
mutationFn: patchMyProfileImage,
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [queryKey.MY_PROFILE],
});
},
});
};
27 changes: 27 additions & 0 deletions src/apis/setting/usePatchWorkspaceTeams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { axiosInstance } from '../axios';
import { useMutation } from '@tanstack/react-query';
import type { CommonResponse } from '../../types/common';
import queryClient from '../../utils/queryClient';
import { queryKey } from '../../constants/queryKey';

// 사이드바, 설정 - 팀 순서 변경

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

p2 : 진주님 apis/setting 폴더 속 함수들 위에 이 부분처럼 주석 달아주셔야 합니다! 다른 파일들도 수정해서 푸쉬해주세요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

추가하여 푸쉬하였습니다. 알려주셔서 감사합니다!

const patchWorkspaceTeams = async (body: { teamIdList: number[] }): Promise<CommonResponse<{}>> => {
try {
const response = await axiosInstance.patch('/api/workspace/setting/teams', body);
return response.data;
} catch (error) {
console.error('워크스페이스 팀 순서 변경 실패', error);
throw error;
}
};

export const usePatchWorkspaceTeams = () => {
return useMutation({
mutationFn: patchWorkspaceTeams,
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [queryKey.WORKSPACE_TEAMS],
});
},
});
};
28 changes: 19 additions & 9 deletions src/components/Sidebar/FullSidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ import collapseIcon from '../../assets/icons/collapse.svg';
import { useNavigate } from 'react-router-dom';
import hamburgerIcon from '../../assets/icons/hamburger.svg';
import SortableDropdownList from './SortableDropdownList';
import vecocirclewhite from '../../assets/logos/veco-circle-logo-bg-white.svg';
// import { usePatchWorkspaceTeams } from '../../apis/setting/usePatchWorkspaceTeams';
import type { Team } from './types';

interface FullSidebarContentProps {
setExpanded: (value: boolean) => void;
teams: { id: number; name: string; icon: React.ReactNode }[];
teams: Team[];
}

const FullSidebarContent = ({ setExpanded, teams }: FullSidebarContentProps) => {
const navigate = useNavigate();
// todo: 팀 순서 변경 API 연동
// const { mutate: patchWorkspaceTeams } = usePatchWorkspaceTeams();

return (
<div className="w-full p-[3.2rem] pe-[2rem] min-h-screen">
<div className="flex flex-col items-start gap-[3.2rem] self-stretch">
Expand Down Expand Up @@ -137,14 +143,14 @@ const FullSidebarContent = ({ setExpanded, teams }: FullSidebarContentProps) =>
{/* 두 번째 드롭다운: 나의 팀 (내부에 드롭다운 또 포함) */}
<div className="flex flex-col items-start self-stretch">
<DropdownMenu headerTitle="나의 팀" initialOpen={true}>
{/* Team1 드롭다운 (내부 드롭다운) */}
{/* Team 드롭다운 (내부 드롭다운) */}
<SortableDropdownList
items={teams}
renderContent={(team, { listeners, attributes }, isOverlay) => (
<DropdownMenu
headerTitle={team.name}
initialOpen={!isOverlay}
headerTeamIcon={team.icon}
headerTeamIcon={<img src={team.profileUrl || vecocirclewhite} alt={team.name} />}
isNested={true}
dragHandle={
<button {...attributes} {...listeners} type="button" className="cursor-grab">
Expand All @@ -160,13 +166,13 @@ const FullSidebarContent = ({ setExpanded, teams }: FullSidebarContentProps) =>
label="목표"
onClick={() => {
navigate(
`/workspace/team/:teamId/goal`.replace(':teamId', String(team.id))
`/workspace/team/:teamId/goal`.replace(':teamId', String(team.teamId))
);
}}
onAddClick={() => {
navigate(
`/workspace/team/:teamId/goal/:goalId`
.replace(':teamId', String(team.id))
.replace(':teamId', String(team.teamId))
.replace(':goalId', String(123))
);
}}
Expand All @@ -177,13 +183,13 @@ const FullSidebarContent = ({ setExpanded, teams }: FullSidebarContentProps) =>
label="이슈"
onClick={() => {
navigate(
`/workspace/team/:teamId/issue`.replace(':teamId', String(team.id))
`/workspace/team/:teamId/issue`.replace(':teamId', String(team.teamId))
);
}}
onAddClick={() => {
navigate(
`/workspace/team/:teamId/issue/:issueId`
.replace(':teamId', String(team.id))
.replace(':teamId', String(team.teamId))
.replace(':issueId', String(123))
);
}}
Expand All @@ -194,15 +200,19 @@ const FullSidebarContent = ({ setExpanded, teams }: FullSidebarContentProps) =>
label="외부"
onClick={() => {
navigate(
`/workspace/team/:teamId/ext`.replace(':teamId', String(team.id))
`/workspace/team/:teamId/ext`.replace(':teamId', String(team.teamId))
);
}}
/>
</div>
)}
</DropdownMenu>
)}
onSorted={(newList: any) => console.log(newList)}
onSorted={(newList: Team[]) => {
const teamIdList = newList.map((item: Team) => item.teamId);
console.log(teamIdList);
// patchWorkspaceTeams(teamIdList);
}}
/>
</DropdownMenu>
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Sidebar/MiniSidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import externalHoverIcon from '../../assets/icons/external-hover.svg';
import DropdownMenu from './DropdownMenu';
import SidebarItem from './SidebarItem';
import SortableDropdownList from './SortableDropdownList';
import type { Team } from './types';

interface MiniSidebarContentProps {
setExpanded: (value: boolean) => void;
teams: { id: number; name: string; icon: React.ReactNode }[];
teams: Team[];
}

const MiniSidebarContent = ({ setExpanded, teams }: MiniSidebarContentProps) => {
Expand Down Expand Up @@ -115,7 +116,7 @@ const MiniSidebarContent = ({ setExpanded, teams }: MiniSidebarContentProps) =>
<DropdownMenu
headerTitle=""
initialOpen={!isOverlay}
headerTeamIcon={team.icon}
headerTeamIcon={team.profileUrl}
isNested={true}
>
{!isOverlay && (
Expand Down
19 changes: 17 additions & 2 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,28 @@ import FullSidebarContent from './FullSidebarContent';
import vecocirclewhite from '../../assets/logos/veco-circle-logo-bg-white.svg';
import clsx from 'clsx';
import { useSidebarStore } from '../../stores/useSidebarStore';
// import { useGetWorkspaceTeams } from '../../apis/setting/useGetWorkspaceTeams';

const Sidebar = () => {
const { isOpen, toggle } = useSidebarStore();
// todo: 팀 목록 조회 API 연동
// const { data: teams = [] } = useGetWorkspaceTeams();

const teams = [
{ id: 2, name: 'Team1', icon: <img src={vecocirclewhite} alt={'Team1'} /> },
{ id: 3, name: 'Team2', icon: <img src={vecocirclewhite} alt={'Team2'} /> },
{
teamId: 2,
name: 'Team1',
profileUrl: vecocirclewhite,
memberCount: 2,
createdAt: '2025-01-01',
},
{
teamId: 3,
name: 'Team2',
profileUrl: vecocirclewhite,
memberCount: 2,
createdAt: '2025-01-01',
},
];

return (
Expand Down
31 changes: 14 additions & 17 deletions src/components/Sidebar/SortableDropdownList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,17 @@ import {
import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
import React, { useState } from 'react';
import SortableDropdown from './SortableDropdown';

export interface SortableItem {
id: number;
name: string;
icon?: React.ReactNode;
}
import type { Team } from './types';

interface SortableDropdownListProps {
items: SortableItem[];
renderContent: (item: SortableItem, dragProps: any, isOverlay?: boolean) => React.ReactNode;
onSorted?: (sortedItems: SortableItem[]) => void;
items: Team[];
renderContent: (item: Team, dragProps: any, isOverlay?: boolean) => React.ReactNode;
onSorted?: (sortedItems: Team[]) => void;
}

const SortableDropdownList = ({ items, renderContent, onSorted }: SortableDropdownListProps) => {
const [list, setList] = useState(items);
const [activeItem, setActiveItem] = useState<SortableItem | null>(null);
const [activeItem, setActiveItem] = useState<Team | null>(null);
const [overlaySize, setOverlaySize] = useState<{ width: number; height: number }>({
width: 0,
height: 0,
Expand All @@ -35,13 +30,13 @@ const SortableDropdownList = ({ items, renderContent, onSorted }: SortableDropdo
const sensors = useSensors(useSensor(PointerSensor));

const handleDragStart = (event: DragStartEvent) => {
const dragged = list.find((item) => item.id === event.active.id);
const dragged = list.find((item) => item.teamId === event.active.id);
if (!dragged) return;

setActiveItem(dragged);

// 드래그 시작 시 DOM 요소 크기 측정
const node = document.querySelector(`[data-id="${dragged.id}"]`) as HTMLElement;
const node = document.querySelector(`[data-id="${dragged.teamId}"]`) as HTMLElement;
if (node) {
const { width, height } = node.getBoundingClientRect();
setOverlaySize({ width, height });
Expand All @@ -55,8 +50,8 @@ const SortableDropdownList = ({ items, renderContent, onSorted }: SortableDropdo
return;
}

const oldIndex = list.findIndex((item) => item.id === active.id);
const newIndex = list.findIndex((item) => item.id === over.id);
const oldIndex = list.findIndex((item) => item.teamId === active.id);
const newIndex = list.findIndex((item) => item.teamId === over.id);
const newList = arrayMove(list, oldIndex, newIndex);
setList(newList);
onSorted?.(newList);
Expand All @@ -70,10 +65,12 @@ const SortableDropdownList = ({ items, renderContent, onSorted }: SortableDropdo
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext items={list.map((i) => i.id)} strategy={verticalListSortingStrategy}>
<SortableContext items={list.map((i) => i.teamId)} strategy={verticalListSortingStrategy}>
{list.map((item) => (
<SortableDropdown key={item.id} id={item.id}>
{(dragProps) => <div data-id={item.id}>{renderContent(item, dragProps, false)}</div>}
<SortableDropdown key={item.teamId} id={item.teamId}>
{(dragProps) => (
<div data-id={item.teamId}>{renderContent(item, dragProps, false)}</div>
)}
</SortableDropdown>
))}
</SortableContext>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Sidebar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { TeamListResponse } from '../../types/setting';

export type Team = Pick<TeamListResponse, 'teamId' | 'name' | 'profileUrl'>;
2 changes: 2 additions & 0 deletions src/constants/queryKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const queryKey = {
WORKSPACE_PROFILE: 'workspace_profile',
WORKSPACE_TEAMS: 'workspace_teams',
WORKSPACE_MEMBERS: 'workspace_members',
MY_PROFILE: 'my_profile',
GITHUB_LINK: 'github_link',
};
Loading