Feat: 프로젝트 캔버스 사이드바 상태 연결 - S11P21S002-193
This commit is contained in:
parent
9e3f7764c7
commit
584788c260
@ -1,12 +1,8 @@
|
|||||||
import api from '@/api/axiosConfig';
|
import api from '@/api/axiosConfig';
|
||||||
import { FolderRequest, FolderResponse } from '@/types';
|
import { FolderRequest, FolderResponse } from '@/types';
|
||||||
|
|
||||||
export async function getFolder(projectId: number, folderId: number, memberId: number) {
|
export async function getFolder(projectId: string, folderId: number) {
|
||||||
return api
|
return api.get<FolderResponse>(`/projects/${projectId}/folders/${folderId}`).then(({ data }) => data);
|
||||||
.get<FolderResponse>(`/projects/${projectId}/folders/${folderId}`, {
|
|
||||||
params: { memberId },
|
|
||||||
})
|
|
||||||
.then(({ data }) => data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFolder(projectId: number, folderId: number, memberId: number, folderData: FolderRequest) {
|
export async function updateFolder(projectId: number, folderId: number, memberId: number, folderData: FolderRequest) {
|
||||||
|
@ -17,9 +17,9 @@ export default function ImageCanvas() {
|
|||||||
const stageRef = useRef<Konva.Stage>(null);
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
const dragLayerRef = useRef<Konva.Layer>(null);
|
const dragLayerRef = useRef<Konva.Layer>(null);
|
||||||
const scale = useRef<number>(0);
|
const scale = useRef<number>(0);
|
||||||
const imageUrl = '/sample.jpg';
|
const imageUrl = useCanvasStore((state) => state.image);
|
||||||
const labels = useCanvasStore((state) => state.labels) ?? [];
|
const labels = useCanvasStore((state) => state.labels) ?? [];
|
||||||
const [image, imageStatus] = useImage(imageUrl);
|
const [image] = useImage(imageUrl);
|
||||||
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
|
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
|
||||||
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
|
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
|
||||||
const drawState = useCanvasStore((state) => state.drawState);
|
const drawState = useCanvasStore((state) => state.drawState);
|
||||||
@ -183,6 +183,17 @@ export default function ImageCanvas() {
|
|||||||
return { x: scale.current, y: scale.current };
|
return { x: scale.current, y: scale.current };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!image) {
|
||||||
|
scale.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const widthRatio = stageWidth / image!.width;
|
||||||
|
const heightRatio = stageHeight / image!.height;
|
||||||
|
|
||||||
|
scale.current = Math.min(widthRatio, heightRatio);
|
||||||
|
}, [image, stageHeight, stageWidth]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stageRef.current) return;
|
if (!stageRef.current) return;
|
||||||
stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair';
|
stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair';
|
||||||
@ -190,9 +201,9 @@ export default function ImageCanvas() {
|
|||||||
if (drawState !== 'pointer') {
|
if (drawState !== 'pointer') {
|
||||||
setSelectedLabelId(null);
|
setSelectedLabelId(null);
|
||||||
}
|
}
|
||||||
}, [drawState]);
|
}, [drawState, setSelectedLabelId]);
|
||||||
|
|
||||||
return imageStatus === 'loaded' ? (
|
return image ? (
|
||||||
<div>
|
<div>
|
||||||
<Stage
|
<Stage
|
||||||
ref={stageRef}
|
ref={stageRef}
|
||||||
|
@ -2,7 +2,6 @@ import * as React from 'react';
|
|||||||
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
|
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { ProjectRequest } from '@/types';
|
import { ProjectRequest } from '@/types';
|
||||||
|
|
||||||
interface ProjectCreateModalProps {
|
interface ProjectCreateModalProps {
|
||||||
@ -16,19 +15,6 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje
|
|||||||
const handleOpen = () => setIsOpen(true);
|
const handleOpen = () => setIsOpen(true);
|
||||||
const handleClose = () => setIsOpen(false);
|
const handleClose = () => setIsOpen(false);
|
||||||
|
|
||||||
const formatLabelType = (
|
|
||||||
labelType: 'Classification' | 'Detection' | 'Segmentation'
|
|
||||||
): ProjectRequest['projectType'] => {
|
|
||||||
switch (labelType) {
|
|
||||||
case 'Classification':
|
|
||||||
return 'classification';
|
|
||||||
case 'Detection':
|
|
||||||
return 'detection';
|
|
||||||
case 'Segmentation':
|
|
||||||
return 'segmentation';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@ -38,10 +24,10 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje
|
|||||||
<Button
|
<Button
|
||||||
variant="outlinePrimary"
|
variant="outlinePrimary"
|
||||||
className={`${buttonClass}`}
|
className={`${buttonClass}`}
|
||||||
|
size={'xs'}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<span>새 프로젝트</span>
|
||||||
<span>프로젝트 추가</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -50,7 +36,8 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje
|
|||||||
onSubmit={(data: ProjectCreateFormValues) => {
|
onSubmit={(data: ProjectCreateFormValues) => {
|
||||||
const formattedData: ProjectRequest = {
|
const formattedData: ProjectRequest = {
|
||||||
title: data.projectName,
|
title: data.projectName,
|
||||||
projectType: formatLabelType(data.labelType),
|
projectType: (data.labelType.charAt(0).toUpperCase() +
|
||||||
|
data.labelType.slice(1)) as ProjectRequest['projectType'],
|
||||||
};
|
};
|
||||||
onSubmit(formattedData);
|
onSubmit(formattedData);
|
||||||
handleClose();
|
handleClose();
|
||||||
|
@ -1,58 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, Outlet } from 'react-router-dom';
|
import { useParams, Outlet } from 'react-router-dom';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
import { Label, Project } from '@/types';
|
import { Project } from '@/types';
|
||||||
import { ResizablePanelGroup } from '../ui/resizable';
|
import { ResizablePanelGroup } from '../ui/resizable';
|
||||||
// import { ResizablePanel } from '../ui/resizable';
|
|
||||||
import WorkspaceSidebar from '../WorkspaceSidebar';
|
import WorkspaceSidebar from '../WorkspaceSidebar';
|
||||||
import useAuthStore from '@/stores/useAuthStore';
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
|
||||||
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
|
||||||
import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery';
|
import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery';
|
||||||
import useProjectListQuery from '@/queries/projects/useProjectListQuery';
|
import useProjectListQuery from '@/queries/projects/useProjectListQuery';
|
||||||
|
|
||||||
const mockLabels: Label[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Label 1',
|
|
||||||
color: '#FFaa33',
|
|
||||||
coordinates: [
|
|
||||||
[700, 100],
|
|
||||||
[1200, 800],
|
|
||||||
],
|
|
||||||
type: 'rect',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Label 2',
|
|
||||||
color: '#aaFF55',
|
|
||||||
coordinates: [
|
|
||||||
[200, 200],
|
|
||||||
[400, 200],
|
|
||||||
[500, 500],
|
|
||||||
[400, 800],
|
|
||||||
[200, 800],
|
|
||||||
[100, 500],
|
|
||||||
],
|
|
||||||
type: 'polygon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Label 3',
|
|
||||||
color: '#77aaFF',
|
|
||||||
coordinates: [
|
|
||||||
[1000, 1000],
|
|
||||||
[1800, 1800],
|
|
||||||
],
|
|
||||||
type: 'rect',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function WorkspaceLayout() {
|
export default function WorkspaceLayout() {
|
||||||
const setLabels = useCanvasStore((state) => state.setLabels);
|
|
||||||
const params = useParams<{ workspaceId: string; projectId: string }>();
|
const params = useParams<{ workspaceId: string; projectId: string }>();
|
||||||
const workspaceId = Number(params.workspaceId);
|
const workspaceId = Number(params.workspaceId);
|
||||||
const projectId = Number(params.projectId);
|
|
||||||
const [workspace, setWorkspace] = useState<{ name: string; projects: Project[] }>({
|
const [workspace, setWorkspace] = useState<{ name: string; projects: Project[] }>({
|
||||||
name: '',
|
name: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
@ -61,7 +19,6 @@ export default function WorkspaceLayout() {
|
|||||||
const memberId = profile?.id || 0;
|
const memberId = profile?.id || 0;
|
||||||
const { data: workspaceData } = useWorkspaceQuery(workspaceId, memberId);
|
const { data: workspaceData } = useWorkspaceQuery(workspaceId, memberId);
|
||||||
const { data: projectListData } = useProjectListQuery(workspaceId, memberId);
|
const { data: projectListData } = useProjectListQuery(workspaceId, memberId);
|
||||||
const { data: folderData } = useFolderQuery(projectId, 0, memberId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceData) return;
|
if (!workspaceData) return;
|
||||||
@ -73,7 +30,6 @@ export default function WorkspaceLayout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectListData) return;
|
if (!projectListData) return;
|
||||||
console.log(folderData);
|
|
||||||
const projects = projectListData.workspaceResponses.map(
|
const projects = projectListData.workspaceResponses.map(
|
||||||
(project): Project => ({
|
(project): Project => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
@ -89,11 +45,7 @@ export default function WorkspaceLayout() {
|
|||||||
...prev,
|
...prev,
|
||||||
projects,
|
projects,
|
||||||
}));
|
}));
|
||||||
}, [folderData, projectListData]);
|
}, [projectListData]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLabels(mockLabels);
|
|
||||||
}, [setLabels]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { DirectoryItem } from '@/types';
|
import { ChildFolder } from '@/types';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ProjectFileItem from './ProjectFileItem';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function ProjectDirectoryItem({
|
export default function ProjectDirectoryItem({
|
||||||
className = '',
|
className = '',
|
||||||
item,
|
item,
|
||||||
depth = 1,
|
depth = 0,
|
||||||
|
initialExpanded = false,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
item: DirectoryItem;
|
item: ChildFolder;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
initialExpanded?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(initialExpanded);
|
||||||
const paddingLeft = depth * 12;
|
const paddingLeft = depth * 12;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,28 +32,9 @@ export default function ProjectDirectoryItem({
|
|||||||
className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{item.name}</span>
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{item.title}</span>
|
||||||
</div>
|
</div>
|
||||||
{item.children?.map((child) => {
|
{/* TODO: nested 폴더 구조 적용 */}
|
||||||
const childProps = {
|
|
||||||
className: isExpanded ? '' : 'hidden',
|
|
||||||
depth: depth + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return child.type === 'directory' ? (
|
|
||||||
<ProjectDirectoryItem
|
|
||||||
key={`${item.name}-${child.name}`}
|
|
||||||
item={child}
|
|
||||||
{...childProps}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ProjectFileItem
|
|
||||||
key={`${item.name}-${child.name}`}
|
|
||||||
item={child}
|
|
||||||
{...childProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { FileItem } from '@/types';
|
import { ImageResponse } from '@/types';
|
||||||
import { Check, Image, Minus } from 'lucide-react';
|
import { Check, Image, Minus } from 'lucide-react';
|
||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
|
||||||
@ -7,29 +7,38 @@ export default function ProjectFileItem({
|
|||||||
className = '',
|
className = '',
|
||||||
item,
|
item,
|
||||||
depth = 1,
|
depth = 1,
|
||||||
|
selected,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
item: FileItem;
|
item: ImageResponse;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
|
selected: boolean;
|
||||||
}) {
|
}) {
|
||||||
const paddingLeft = depth * 12;
|
const paddingLeft = depth * 12;
|
||||||
const changeImage = useCanvasStore((state) => state.changeImage);
|
const changeImage = useCanvasStore((state) => state.changeImage);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
changeImage(item.url, [
|
// TODO: fetch image
|
||||||
|
changeImage(item.imageUrl, [
|
||||||
{
|
{
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.imageTitle,
|
||||||
type: 'rect',
|
type: 'rect',
|
||||||
color: '#FF0000',
|
color: '#FF0000',
|
||||||
coordinates: [],
|
coordinates: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 100],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn('flex w-full gap-2 rounded-md py-0.5 pr-1 hover:bg-gray-200', className)}
|
className={cn(
|
||||||
|
`flex w-full gap-2 rounded-md py-0.5 pr-1 ${selected ? 'bg-gray-200' : 'hover:bg-gray-100'}`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
paddingLeft,
|
paddingLeft,
|
||||||
}}
|
}}
|
||||||
@ -41,17 +50,17 @@ export default function ProjectFileItem({
|
|||||||
className="stroke-gray-500"
|
className="stroke-gray-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.name}</span>
|
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.imageTitle}</span>
|
||||||
{item.status === 'idle' ? (
|
{item.status === 'COMPLETED' ? (
|
||||||
<Minus
|
|
||||||
size={16}
|
|
||||||
className="shrink-0 stroke-gray-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Check
|
<Check
|
||||||
size={16}
|
size={16}
|
||||||
className="shrink-0 stroke-green-500"
|
className="shrink-0 stroke-green-500"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Minus
|
||||||
|
size={16}
|
||||||
|
className="shrink-0 stroke-gray-400"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1,29 +1,19 @@
|
|||||||
import { Project } from '@/types';
|
import { Project } from '@/types';
|
||||||
import { ChevronRight, SquarePenIcon, Upload } from 'lucide-react';
|
import { SquarePenIcon, Upload } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
|
||||||
import ProjectFileItem from './ProjectFileItem';
|
import ProjectFileItem from './ProjectFileItem';
|
||||||
import ProjectDirectoryItem from './ProjectDirectoryItem';
|
import ProjectDirectoryItem from './ProjectDirectoryItem';
|
||||||
|
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
||||||
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
|
||||||
export default function ProjectStructure({ project }: { project: Project }) {
|
export default function ProjectStructure({ project }: { project: Project }) {
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const image = useCanvasStore((state) => state.image);
|
||||||
|
const { data: folderData } = useFolderQuery(project.id.toString(), 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex select-none flex-col px-1 pb-2">
|
<div className="flex h-full flex-col overflow-y-auto px-1 pb-2">
|
||||||
<header className="flex w-full items-center gap-2 rounded px-1 hover:bg-gray-200">
|
<header className="flex w-full items-center gap-2 rounded p-1">
|
||||||
<div
|
<div className="flex w-full items-center gap-1 overflow-hidden pr-1">
|
||||||
className="flex w-full cursor-pointer items-center gap-1 overflow-hidden pr-1"
|
<h3 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h3>
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
<ChevronRight
|
|
||||||
size={16}
|
|
||||||
className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="flex flex-col overflow-hidden">
|
|
||||||
<h2 className="body-small-strong overflow-hidden text-ellipsis whitespace-nowrap">{project.name}</h2>
|
|
||||||
<h3 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="flex gap-1"
|
className="flex gap-1"
|
||||||
@ -38,23 +28,28 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
<Upload size={16} />
|
<Upload size={16} />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className={`caption flex flex-col ${isExpanded ? '' : 'hidden'}`}>
|
{folderData.children.length === 0 && folderData.images.length === 0 ? (
|
||||||
{project.children.map((item) =>
|
<div className="body-small flex h-full select-none items-center justify-center text-gray-400">
|
||||||
item.type === 'directory' ? (
|
빈 프로젝트입니다.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="caption flex flex-col">
|
||||||
|
{folderData.children.map((item) => (
|
||||||
<ProjectDirectoryItem
|
<ProjectDirectoryItem
|
||||||
key={`${project.id}-${item.name}`}
|
key={`${project.id}-${item.title}`}
|
||||||
item={item}
|
item={item}
|
||||||
className={isExpanded ? '' : 'hidden'}
|
initialExpanded={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
))}
|
||||||
|
{folderData.images.map((item) => (
|
||||||
<ProjectFileItem
|
<ProjectFileItem
|
||||||
key={`${project.id}-${item.name}`}
|
key={`${project.id}-${item.imageTitle}`}
|
||||||
item={item}
|
item={item}
|
||||||
className={isExpanded ? '' : 'hidden'}
|
selected={image === item.imageUrl}
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { SquarePen } from 'lucide-react';
|
import { SquarePen } from 'lucide-react';
|
||||||
import { ResizableHandle, ResizablePanel } from '../ui/resizable';
|
import { ResizableHandle, ResizablePanel } from '../ui/resizable';
|
||||||
@ -7,15 +6,18 @@ import { Project } from '@/types';
|
|||||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../ui/select';
|
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../ui/select';
|
||||||
import ProjectCreateModal from '../ProjectCreateModal';
|
import ProjectCreateModal from '../ProjectCreateModal';
|
||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import { webPath } from '@/router';
|
||||||
|
|
||||||
export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) {
|
export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) {
|
||||||
|
const { projectId: selectedProjectId } = useParams<{ projectId: string }>();
|
||||||
|
const selectedProject = projects.find((project) => project.id.toString() === selectedProjectId);
|
||||||
const setSidebarSize = useCanvasStore((state) => state.setSidebarSize);
|
const setSidebarSize = useCanvasStore((state) => state.setSidebarSize);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | undefined>();
|
// const [selectedProjectId, setSelectedProjectId] = useState<string | undefined>();
|
||||||
const handleSelectProject = (projectId: string) => {
|
const handleSelectProject = (projectId: string) => {
|
||||||
setSelectedProjectId(projectId);
|
// setSelectedProjectId(projectId);
|
||||||
navigate(`/workspace/${workspaceId}/project/${projectId}`);
|
navigate(`${webPath.workspace()}/${workspaceId}/project/${projectId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,7 +26,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac
|
|||||||
minSize={10}
|
minSize={10}
|
||||||
maxSize={35}
|
maxSize={35}
|
||||||
defaultSize={20}
|
defaultSize={20}
|
||||||
className="flex h-full flex-col bg-gray-100"
|
className="flex h-full flex-col bg-gray-50"
|
||||||
onResize={(size) => setSidebarSize(size)}
|
onResize={(size) => setSidebarSize(size)}
|
||||||
>
|
>
|
||||||
<header className="body flex w-full items-center gap-2 p-2">
|
<header className="body flex w-full items-center gap-2 p-2">
|
||||||
@ -34,11 +36,14 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac
|
|||||||
</button>
|
</button>
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
onSubmit={(data) => console.log('프로젝트 생성:', data)}
|
onSubmit={(data) => console.log('프로젝트 생성:', data)}
|
||||||
buttonClass="caption border-gray-800 bg-gray-100 flex items-center gap-2"
|
buttonClass="caption border-primary bg-gray-50"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Select onValueChange={handleSelectProject}>
|
<Select
|
||||||
|
onValueChange={handleSelectProject}
|
||||||
|
defaultValue={selectedProjectId}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="프로젝트를 선택해주세요" />
|
<SelectValue placeholder="프로젝트를 선택해주세요" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -54,16 +59,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{selectedProject && <ProjectStructure project={selectedProject} />}
|
||||||
{projects
|
|
||||||
.filter((project) => project.id.toString() === selectedProjectId)
|
|
||||||
.map((project) => (
|
|
||||||
<ProjectStructure
|
|
||||||
key={project.id}
|
|
||||||
project={project}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle className="bg-gray-300" />
|
<ResizableHandle className="bg-gray-300" />
|
||||||
</>
|
</>
|
||||||
|
@ -1,30 +1,3 @@
|
|||||||
// import React from 'react';
|
|
||||||
// import ReactDOM from 'react-dom/client';
|
|
||||||
// import App from './App.tsx';
|
|
||||||
// import './index.css';
|
|
||||||
|
|
||||||
// async function enableMocking() {
|
|
||||||
// if (!import.meta.env.DEV) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const { worker } = await import('./mocks/browser.ts');
|
|
||||||
// await worker.start();
|
|
||||||
// console.log('[MSW] Mocking enabled. Service Worker is running.');
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('[MSW] Failed to start the Service Worker:', error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// enableMocking().then(() => {
|
|
||||||
// ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
// <React.StrictMode>
|
|
||||||
// <App />
|
|
||||||
// </React.StrictMode>
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import ImageCanvas from '@/components/ImageCanvas';
|
import ImageCanvas from '@/components/ImageCanvas';
|
||||||
import { ResizablePanel } from '@/components/ui/resizable';
|
import { ResizablePanel } from '@/components/ui/resizable';
|
||||||
import WorkspaceLabelBar from '@/components/WorkspaceLabelBar';
|
import WorkspaceLabelBar from '@/components/WorkspaceLabelBar';
|
||||||
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
export default function LabelCanvas() {
|
export default function LabelCanvas() {
|
||||||
|
const imageUrl = useCanvasStore((state) => state.image);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePanel className="flex w-full items-center">
|
<ResizablePanel className="flex w-full items-center">
|
||||||
<main className="h-full grow">
|
<Suspense fallback={<div></div>}>
|
||||||
<ImageCanvas />
|
<main className="h-full grow">
|
||||||
</main>
|
{imageUrl ? (
|
||||||
<WorkspaceLabelBar />
|
<ImageCanvas />
|
||||||
|
) : (
|
||||||
|
<div className="body flex h-full w-full select-none items-center justify-center bg-gray-200 text-gray-400">
|
||||||
|
이미지를 선택하세요.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
<WorkspaceLabelBar />
|
||||||
|
</Suspense>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ function HeaderSection({
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
buttonClass="mt-4 flex items-center gap-2"
|
buttonClass="mt-4 flex items-center gap-2 body-small-strong"
|
||||||
onSubmit={onCreateProject}
|
onSubmit={onCreateProject}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { getFolder } from '@/api/folderApi';
|
import { getFolder } from '@/api/folderApi';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function useFolderQuery(projectId: number, folderId: number, memberId: number) {
|
export default function useFolderQuery(projectId: string, folderId: number) {
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: ['folder', projectId, folderId, memberId],
|
queryKey: ['folder', projectId, folderId],
|
||||||
queryFn: () => getFolder(projectId, folderId, memberId),
|
queryFn: () => getFolder(projectId, folderId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user