Merge branch 'fe/refactor/improve-design' into 'fe/develop'

Feat: 워크스페이스 이름 변경 기능 추가, 워크스페이스 페이지 디자인 개선

See merge request s11-s-project/S11P21S002!294
This commit is contained in:
정현조 2024-10-05 10:40:34 +09:00
commit 6c0983ffdf
6 changed files with 291 additions and 70 deletions

View File

@ -31,7 +31,7 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader title="새 프로젝트" />
<DialogHeader title="새 프로젝트 추가" />
<ProjectCreateForm
onSubmit={(data: ProjectCreateFormValues) => {
const formattedData: ProjectRequest = {

View File

@ -34,7 +34,7 @@ export default function WorkSpaceCreateModal({ onSubmit }: WorkSpaceCreateModalP
</button>
</DialogTrigger>
<DialogContent>
<DialogHeader title="새 워크스페이스" />
<DialogHeader title="새 워크스페이스 추가" />
<WorkSpaceCreateForm onSubmit={handleFormSubmit} />
</DialogContent>
</Dialog>

View File

@ -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<boolean>(false);
const [isOpenWorkspaceUpdate, setIsOpenWorkspaceUpdate] = useState<boolean>(false);
const [isOpenProjectCreate, setIsOpenProjectCreate] = useState<boolean>(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() {
<h1 className="subheading mr-2.5 w-full overflow-hidden text-ellipsis whitespace-nowrap p-2">
</h1>
<WorkSpaceCreateModal onSubmit={handleCreateWorkspace} />
<DropdownMenu>
<DropdownMenuTrigger>
<div className="rounded-full p-2 duration-200 hover:bg-gray-200">
<Ellipsis size={16} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setIsOpenWorkspaceCreate(true)}>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex flex-col">
{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}
<div className="flex items-center justify-center">
<p className="w-full overflow-hidden text-ellipsis whitespace-nowrap p-3">{workspace.title}</p>
{workspace.id === activeWorkspaceId && (
<DropdownMenu>
<DropdownMenuTrigger>
<div className="mr-1 rounded-full p-2 duration-200 hover:bg-gray-300">
<Ellipsis size={16} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setIsOpenWorkspaceUpdate(true)}>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenProjectCreate(true)}>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</NavLink>
))
) : (
@ -74,6 +160,42 @@ export default function WorkspaceBrowseLayout() {
</div>
</div>
</div>
<Dialog
open={isOpenWorkspaceCreate}
onOpenChange={setIsOpenWorkspaceCreate}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent>
<DialogHeader title="새 워크스페이스 추가" />
<WorkSpaceCreateForm onSubmit={handleCreateWorkspace} />
</DialogContent>
</Dialog>
<Dialog
open={isOpenWorkspaceUpdate}
onOpenChange={setIsOpenWorkspaceUpdate}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent>
<DialogHeader title="워크스페이스 이름 변경" />
<WorkspaceUpdateForm
workspace={activeWorkspace}
onSubmit={handleUpdateWorkspace}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenProjectCreate}
onOpenChange={setIsOpenProjectCreate}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent>
<DialogHeader title="새 프로젝트 추가" />
<ProjectCreateForm onSubmit={handleCreateProject} />
</DialogContent>
</Dialog>
</>
);
}

View File

@ -31,52 +31,11 @@ export default function WorkspaceDropdownMenu({
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
const handleCloseUploadFile = () => {
setIsOpenUploadFile(false);
};
const handleFileCount = (fileCount: number) => {
setFileCount(fileCount);
};
const handleOpenUploadPresigned = () => setIsOpenUploadPresigned(true);
const handleCloseUploadPresigned = () => {
setIsOpenUploadPresigned(false);
};
const handlePresignedCount = (fileCount: number) => {
setPresignedCount(fileCount);
};
const handleOpenUploadFolderFile = () => setIsOpenUploadFolderFile(true);
const handleCloseUploadFolderFile = () => {
setIsOpenUploadFolderFile(false);
};
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
const handleCloseUploadFolder = () => {
setIsOpenUploadFolder(false);
};
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
const handleCloseUploadZip = () => {
setIsOpenUploadZip(false);
};
return (
<>
<DropdownMenu>
<DropdownMenuTrigger>
<Menu
size={16}
className="stroke-gray-900"
/>
<Menu size={16} />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
@ -87,11 +46,17 @@ export default function WorkspaceDropdownMenu({
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleOpenUploadFile}> </DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadPresigned}> (PresignedUrl )</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadFolderFile}> ( API )</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadFolder}> ( )</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadZip}> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadFile(true)}> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadPresigned(true)}>
(PresignedUrl )
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadFolderFile(true)}>
( API )
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadFolder(true)}>
( )
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadZip(true)}> </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -103,9 +68,9 @@ export default function WorkspaceDropdownMenu({
<DialogContent className="max-w-2xl">
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
<ImageUploadFileForm
onClose={handleCloseUploadFile}
onClose={() => setIsOpenUploadFile(false)}
onRefetch={onRefetch}
onFileCount={handleFileCount}
onFileCount={(fileCount: number) => setFileCount(fileCount)}
projectId={projectId}
folderId={folderId}
/>
@ -122,9 +87,9 @@ export default function WorkspaceDropdownMenu({
title={presignedCount > 0 ? `파일 업로드 PreSigned (${presignedCount})` : '파일 업로드 PreSigned'}
/>
<ImageUploadPresignedForm
onClose={handleCloseUploadPresigned}
onClose={() => setIsOpenUploadPresigned(false)}
onRefetch={onRefetch}
onFileCount={handlePresignedCount}
onFileCount={(fileCount: number) => setPresignedCount(fileCount)}
projectId={projectId}
folderId={folderId}
/>
@ -139,7 +104,7 @@ export default function WorkspaceDropdownMenu({
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
<ImageUploadFolderFileForm
onClose={handleCloseUploadFolderFile}
onClose={() => setIsOpenUploadFolderFile(false)}
onRefetch={onRefetch}
projectId={projectId}
folderId={folderId}
@ -155,7 +120,7 @@ export default function WorkspaceDropdownMenu({
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
<ImageUploadFolderForm
onClose={handleCloseUploadFolder}
onClose={() => setIsOpenUploadFolder(false)}
onRefetch={onRefetch}
projectId={projectId}
folderId={folderId}
@ -171,7 +136,7 @@ export default function WorkspaceDropdownMenu({
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 압축파일 업로드" />
<ImageUploadZipForm
onClose={handleCloseUploadZip}
onClose={() => setIsOpenUploadZip(false)}
onRefetch={onRefetch}
projectId={projectId}
folderId={folderId}

View File

@ -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<typeof formSchema>;
interface WorkspaceUpdateFormProps {
workspace: WorkspaceResponse;
onSubmit: (data: WorkspaceUpdateFormValues) => void;
}
export default function WorkspaceUpdateForm({ workspace, onSubmit }: WorkspaceUpdateFormProps) {
const defaultValues: Partial<WorkspaceUpdateFormValues> = {
workspaceName: workspace.title,
workspaceDescription: workspace.content,
};
const form = useForm<WorkspaceUpdateFormValues>({
resolver: zodResolver(formSchema),
defaultValues,
});
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5"
>
<FormField
name="workspaceName"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel className="body-strong"> </FormLabel>
<FormControl>
<Input
placeholder="이름을 입력해주세요."
maxLength={50}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="workspaceDescription"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel className="body-strong"> </FormLabel>
<FormControl>
<Input
placeholder="워크스페이스 설명을 입력해주세요."
maxLength={200}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="blue"
disabled={!form.formState.isValid}
>
</Button>
</form>
</Form>
);
}

View File

@ -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 (
<Dialog
open={isOpen}
onOpenChange={setIsOpen}
>
<DialogTrigger asChild>
<button
className="flex items-center justify-center p-2"
onClick={() => setIsOpen(true)}
>
<Plus size={20} />
</button>
</DialogTrigger>
<DialogContent>
<DialogHeader title="워크스페이스 이름 변경" />
<WorkspaceUpdateForm
workspace={workspace}
onSubmit={handleFormSubmit}
/>
</DialogContent>
</Dialog>
);
}