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
30 changes: 30 additions & 0 deletions src/apis/setting/usePatchWorkspaceTeams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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';

// 사이드바, 설정 - 팀 순서 변경
const patchWorkspaceTeams = async (body: { teamIdList: number[] }): Promise<CommonResponse<{}>> => {
try {
const response = await axiosInstance.patch<CommonResponse<{}>>(
'/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'>;