diff --git a/frontend/src/components/ProjectCreateModal/index.tsx b/frontend/src/components/ProjectCreateModal/index.tsx index 8408f1c..0694ad9 100644 --- a/frontend/src/components/ProjectCreateModal/index.tsx +++ b/frontend/src/components/ProjectCreateModal/index.tsx @@ -31,7 +31,7 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje - + { const formattedData: ProjectRequest = { diff --git a/frontend/src/components/WorkSpaceCreateModal/index.tsx b/frontend/src/components/WorkSpaceCreateModal/index.tsx index 1e8bf36..f87fa68 100644 --- a/frontend/src/components/WorkSpaceCreateModal/index.tsx +++ b/frontend/src/components/WorkSpaceCreateModal/index.tsx @@ -34,7 +34,7 @@ export default function WorkSpaceCreateModal({ onSubmit }: WorkSpaceCreateModalP - + diff --git a/frontend/src/components/WorkspaceBrowseLayout/index.tsx b/frontend/src/components/WorkspaceBrowseLayout/index.tsx index f0a97b4..fb800fa 100644 --- a/frontend/src/components/WorkspaceBrowseLayout/index.tsx +++ b/frontend/src/components/WorkspaceBrowseLayout/index.tsx @@ -1,17 +1,29 @@ -import { useEffect, Suspense } from 'react'; -import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { useEffect, Suspense, useState } from 'react'; +import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'; import Header from '../Header'; import useAuthStore from '@/stores/useAuthStore'; -import WorkSpaceCreateModal from '../WorkSpaceCreateModal'; -import { WorkspaceRequest, WorkspaceResponse } from '@/types'; +import { ProjectRequest, WorkspaceRequest, WorkspaceResponse } from '@/types'; import useWorkspaceListQuery from '@/queries/workspaces/useWorkspaceListQuery'; import useCreateWorkspaceQuery from '@/queries/workspaces/useCreateWorkspaceQuery'; import { cn } from '@/lib/utils'; +import { Ellipsis } from 'lucide-react'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu'; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; +import WorkSpaceCreateForm, { WorkSpaceCreateFormValues } from '../WorkSpaceCreateModal/WorkSpaceCreateForm'; +import ProjectCreateForm, { ProjectCreateFormValues } from '../ProjectCreateModal/ProjectCreateForm'; +import useCreateProjectQuery from '@/queries/projects/useCreateProjectQuery'; +import WorkspaceUpdateForm, { WorkspaceUpdateFormValues } from '../WorkspaceUpdateModal/WorkspaceUpdateForm'; +import useUpdateWorkspaceQuery from '@/queries/workspaces/useUpdateWorkspaceQuery'; export default function WorkspaceBrowseLayout() { + const location = useLocation(); + const navigate = useNavigate(); const { profile } = useAuthStore(); const memberId = profile?.id ?? 0; - const navigate = useNavigate(); + + const [isOpenWorkspaceCreate, setIsOpenWorkspaceCreate] = useState(false); + const [isOpenWorkspaceUpdate, setIsOpenWorkspaceUpdate] = useState(false); + const [isOpenProjectCreate, setIsOpenProjectCreate] = useState(false); useEffect(() => { if (memberId == 0) { @@ -19,17 +31,64 @@ export default function WorkspaceBrowseLayout() { } }, [memberId, navigate]); - const { data: workspacesResponse } = useWorkspaceListQuery(memberId ?? 0); const createWorkspace = useCreateWorkspaceQuery(); + const updateWorkspace = useUpdateWorkspaceQuery(); + const createProject = useCreateProjectQuery(); + + const { data: workspacesResponse, refetch } = useWorkspaceListQuery(memberId ?? 0); + const workspaces = workspacesResponse?.workspaceResponses ?? []; + const activeWorkspaceId: number = Number(location.pathname.split('/')[2] || '-1'); + const activeWorkspace = workspaces.filter((workspace) => workspace.id === activeWorkspaceId)[0] ?? null; + + const handleCreateWorkspace = (values: WorkSpaceCreateFormValues) => { + const data: WorkspaceRequest = { + title: values.workspaceName, + content: values.workspaceDescription || '', + }; - const handleCreateWorkspace = (data: WorkspaceRequest) => { createWorkspace.mutate({ memberId, data, }); + + setIsOpenWorkspaceCreate(false); }; - const workspaces = workspacesResponse?.workspaceResponses ?? []; + const handleUpdateWorkspace = (values: WorkspaceUpdateFormValues) => { + const data: WorkspaceRequest = { + title: values.workspaceName, + content: values.workspaceDescription || '', + }; + + updateWorkspace.mutate({ + workspaceId: activeWorkspaceId, + memberId, + data, + }); + + setIsOpenWorkspaceUpdate(false); + refetch(); + }; + + const handleCreateProject = (values: ProjectCreateFormValues) => { + const data: ProjectRequest = { + title: values.projectName, + projectType: values.labelType.toLowerCase() as ProjectRequest['projectType'], + categories: values.categories, + }; + + createProject.mutate({ + workspaceId: activeWorkspaceId, + memberId, + data, + }); + + setIsOpenProjectCreate(false); + }; + + useEffect(() => { + refetch(); + }, [isOpenWorkspaceUpdate, refetch]); return ( <> @@ -42,7 +101,18 @@ export default function WorkspaceBrowseLayout() {

내 워크스페이스

- + + +
+ +
+
+ + setIsOpenWorkspaceCreate(true)}> + 새 워크스페이스 추가 + + +
{workspaces.length > 0 ? ( @@ -51,13 +121,29 @@ export default function WorkspaceBrowseLayout() { key={workspace.id} to={`/browse/${workspace.id}`} className={({ isActive }) => - cn( - 'cursor-pointer rounded-lg p-3 hover:bg-gray-200', - isActive ? 'body-strong bg-gray-300' : 'body' - ) + cn('cursor-pointer rounded-lg hover:bg-gray-200', isActive ? 'body-strong bg-gray-200' : 'body') } > - {workspace.title} +
+

{workspace.title}

+ {workspace.id === activeWorkspaceId && ( + + +
+ +
+
+ + setIsOpenWorkspaceUpdate(true)}> + 워크스페이스 이름 변경 + + setIsOpenProjectCreate(true)}> + 워크스페이스에 새 프로젝트 추가 + + +
+ )} +
)) ) : ( @@ -74,6 +160,42 @@ export default function WorkspaceBrowseLayout() {
+ + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/components/WorkspaceDropdownMenu/index.tsx b/frontend/src/components/WorkspaceDropdownMenu/index.tsx index b518f58..742e4f0 100644 --- a/frontend/src/components/WorkspaceDropdownMenu/index.tsx +++ b/frontend/src/components/WorkspaceDropdownMenu/index.tsx @@ -60,19 +60,22 @@ export default function WorkspaceDropdownMenu({ <> - + console.log('프로젝트 이름 수정')}>프로젝트 이름 수정 - 파일 업로드 - 파일 업로드 (PresignedUrl 이용) - 폴더 업로드 (파일 업로드 API 이용) - 폴더 업로드 (백엔드 구현 필요) - 폴더 압축파일 업로드 + setIsOpenUploadFile(true)}>파일 업로드 + setIsOpenUploadPresigned(true)}> + 파일 업로드 (PresignedUrl 이용) + + setIsOpenUploadFolderFile(true)}> + 폴더 업로드 (파일 업로드 API 이용) + + setIsOpenUploadFolder(true)}> + 폴더 업로드 (백엔드 구현 필요) + + setIsOpenUploadZip(true)}>폴더 압축파일 업로드 @@ -86,7 +89,7 @@ export default function WorkspaceDropdownMenu({ setFileCount(fileCount)} projectId={projectId} folderId={folderId} uploadImageZipMutation={uploadImageZipMutation} @@ -107,9 +110,9 @@ export default function WorkspaceDropdownMenu({ title={presignedCount > 0 ? `파일 업로드 PreSigned (${presignedCount})` : '파일 업로드 PreSigned'} /> setIsOpenUploadPresigned(false)} onRefetch={onRefetch} - onFileCount={handlePresignedCount} + onFileCount={(fileCount: number) => setPresignedCount(fileCount)} projectId={projectId} folderId={folderId} /> diff --git a/frontend/src/components/WorkspaceUpdateModal/WorkspaceUpdateForm.tsx b/frontend/src/components/WorkspaceUpdateModal/WorkspaceUpdateForm.tsx new file mode 100644 index 0000000..4a3c59c --- /dev/null +++ b/frontend/src/components/WorkspaceUpdateModal/WorkspaceUpdateForm.tsx @@ -0,0 +1,88 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { WorkspaceResponse } from '@/types'; + +const formSchema = z.object({ + workspaceName: z + .string() + .min(1, { message: '이름을 입력해주세요.' }) + .max(50, { message: '이름은 50자 이내여야 합니다.' }), + workspaceDescription: z + .string() + .min(1, { message: '설명을 입력해주세요.' }) + .max(200, { message: '설명은 200자 이내여야 합니다.' }), +}); + +export type WorkspaceUpdateFormValues = z.infer; + +interface WorkspaceUpdateFormProps { + workspace: WorkspaceResponse; + onSubmit: (data: WorkspaceUpdateFormValues) => void; +} + +export default function WorkspaceUpdateForm({ workspace, onSubmit }: WorkspaceUpdateFormProps) { + const defaultValues: Partial = { + workspaceName: workspace.title, + workspaceDescription: workspace.content, + }; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues, + }); + + return ( +
+ + ( + + 워크스페이스 이름 + + + + + + )} + /> + ( + + 워크스페이스 설명 + + + + + + )} + /> + + + + ); +} diff --git a/frontend/src/components/WorkspaceUpdateModal/index.tsx b/frontend/src/components/WorkspaceUpdateModal/index.tsx new file mode 100644 index 0000000..17a7e07 --- /dev/null +++ b/frontend/src/components/WorkspaceUpdateModal/index.tsx @@ -0,0 +1,46 @@ +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; +import { Plus } from 'lucide-react'; +import { WorkspaceRequest, WorkspaceResponse } from '@/types'; +import { useState } from 'react'; +import WorkspaceUpdateForm, { WorkspaceUpdateFormValues } from './WorkspaceUpdateForm'; + +interface WorkspaceUpdateModalProps { + workspace: WorkspaceResponse; + onSubmit: (data: WorkspaceRequest) => void; +} + +export default function WorkspaceUpdateModal({ workspace, onSubmit }: WorkspaceUpdateModalProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleFormSubmit = (data: WorkspaceUpdateFormValues) => { + const formattedData: WorkspaceRequest = { + title: data.workspaceName, + content: data.workspaceDescription || '', + }; + onSubmit(formattedData); + setIsOpen(false); + }; + + return ( + + + + + + + + + + ); +}