Refactor: 워크스페이스 페이지 리팩토링 - S11P21S002-139

This commit is contained in:
정현조 2024-09-12 11:01:06 +09:00
parent 04ba6cf258
commit 6b53eccd5c
8 changed files with 182 additions and 160 deletions

View File

@ -4,6 +4,8 @@ import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { fetchProfileApi, reissueTokenApi } from '@/api/authApi'; import { fetchProfileApi, reissueTokenApi } from '@/api/authApi';
import { SuccessResponse, MemberResponseDTO, CustomError } from '@/types';
import { AxiosError } from 'axios';
const DOMAIN = 'https://j11s002.p.ssafy.io'; const DOMAIN = 'https://j11s002.p.ssafy.io';
@ -12,24 +14,20 @@ export default function Home() {
const { isLoggedIn, setLoggedIn, profile, setProfile } = useAuthStore(); const { isLoggedIn, setLoggedIn, profile, setProfile } = useAuthStore();
const hasFetchedProfile = useRef(false); const hasFetchedProfile = useRef(false);
if (!isLoggedIn && !profile.id && !hasFetchedProfile.current) { if (!isLoggedIn && !profile && !hasFetchedProfile.current) {
const accessToken = localStorage.getItem('accessToken'); const accessToken = sessionStorage.getItem('accessToken');
if (accessToken) { if (accessToken) {
setLoggedIn(true, accessToken); setLoggedIn(true, accessToken);
fetchProfileApi() fetchProfileApi()
.then((data) => { .then((data: SuccessResponse<MemberResponseDTO>) => {
if (data?.isSuccess && data.data) { if (data?.isSuccess && data.data) {
setProfile({ setProfile(data.data);
id: data.data.id,
nickname: data.data.nickname,
profileImage: data.data.profileImage,
});
hasFetchedProfile.current = true; hasFetchedProfile.current = true;
} }
}) })
.catch((error) => { .catch((error: AxiosError<CustomError>) => {
alert('프로필을 가져오는 중 오류가 발생했습니다. 다시 시도해주세요.'); alert('프로필을 가져오는 중 오류가 발생했습니다. 다시 시도해주세요.');
console.error('프로필 가져오기 실패:', error); console.error('프로필 가져오기 실패:', error?.response?.data?.message || '알 수 없는 오류');
}); });
} }
} }

View File

@ -4,16 +4,29 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
export default function ProjectCreateModal({ interface ProjectCreateModalProps {
onSubmit, onSubmit: (data: { title: string; labelType: 'classification' | 'detection' | 'segmentation' }) => void;
}: { }
onSubmit: (data: { title: string; labelType: 'Classification' | 'Detection' | 'Segmentation' }) => void;
}) { export default function ProjectCreateModal({ onSubmit }: ProjectCreateModalProps) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false); const handleClose = () => setIsOpen(false);
const formatLabelType = (
labelType: 'Classification' | 'Detection' | 'Segmentation'
): 'classification' | 'detection' | 'segmentation' => {
switch (labelType) {
case 'Classification':
return 'classification';
case 'Detection':
return 'detection';
case 'Segmentation':
return 'segmentation';
}
};
return ( return (
<Dialog <Dialog
open={isOpen} open={isOpen}
@ -35,7 +48,7 @@ export default function ProjectCreateModal({
onSubmit={(data: ProjectCreateFormValues) => { onSubmit={(data: ProjectCreateFormValues) => {
const formattedData = { const formattedData = {
title: data.projectName, title: data.projectName,
labelType: data.labelType, labelType: formatLabelType(data.labelType),
}; };
onSubmit(formattedData); onSubmit(formattedData);
handleClose(); handleClose();

View File

@ -19,7 +19,11 @@ const defaultValues: Partial<WorkSpaceCreateFormValues> = {
workspaceDescription: '', workspaceDescription: '',
}; };
export default function WorkSpaceCreateForm({ onSubmit }: { onSubmit: (data: WorkSpaceCreateFormValues) => void }) { interface WorkSpaceCreateFormProps {
onSubmit: (data: WorkSpaceCreateFormValues) => void;
}
export default function WorkSpaceCreateForm({ onSubmit }: WorkSpaceCreateFormProps) {
const form = useForm<WorkSpaceCreateFormValues>({ const form = useForm<WorkSpaceCreateFormValues>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues, defaultValues,

View File

@ -2,17 +2,27 @@ import * as React from 'react';
import WorkSpaceCreateForm, { WorkSpaceCreateFormValues } from './WorkSpaceCreateForm'; import WorkSpaceCreateForm, { WorkSpaceCreateFormValues } from './WorkSpaceCreateForm';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { WorkspaceRequestDTO } from '@/types';
export default function WorkSpaceCreateModal({ interface WorkSpaceCreateModalProps {
onSubmit, onSubmit: (data: WorkspaceRequestDTO) => void;
}: { }
onSubmit: (data: { title: string; content: string }) => void;
}) { export default function WorkSpaceCreateModal({ onSubmit }: WorkSpaceCreateModalProps) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false); const handleClose = () => setIsOpen(false);
const handleFormSubmit = (data: WorkSpaceCreateFormValues) => {
const formattedData: WorkspaceRequestDTO = {
title: data.workspaceName,
content: data.workspaceDescription || '',
};
onSubmit(formattedData);
handleClose();
};
return ( return (
<Dialog <Dialog
open={isOpen} open={isOpen}
@ -28,16 +38,7 @@ export default function WorkSpaceCreateModal({
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader title="새 워크스페이스" /> <DialogHeader title="새 워크스페이스" />
<WorkSpaceCreateForm <WorkSpaceCreateForm onSubmit={handleFormSubmit} />
onSubmit={(data: WorkSpaceCreateFormValues) => {
const formattedData = {
title: data.workspaceName,
content: data.workspaceDescription || '',
};
onSubmit(formattedData);
handleClose();
}}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -4,23 +4,31 @@ import { Smile } from 'lucide-react';
import ProjectCreateModal from '../ProjectCreateModal'; import ProjectCreateModal from '../ProjectCreateModal';
import { useGetAllProjects, useCreateProject } from '@/hooks/useProjectHooks'; import { useGetAllProjects, useCreateProject } from '@/hooks/useProjectHooks';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { Key } from 'react'; import { ProjectResponseDTO } from '@/types';
export default function WorkspaceBrowseDetail() { export default function WorkspaceBrowseDetail() {
const { workspaceId } = useParams<{ workspaceId: string }>(); const { workspaceId } = useParams<{ workspaceId: string }>();
const numericWorkspaceId = Number(workspaceId); const numericWorkspaceId = Number(workspaceId);
const { profile } = useAuthStore(); const { profile } = useAuthStore();
const memberId = profile.id ?? 0; const memberId = profile?.id ?? 0;
const {
data: projectsResponse,
isLoading,
isError,
refetch,
} = useGetAllProjects(numericWorkspaceId, memberId, {
enabled: !isNaN(numericWorkspaceId),
});
const { data: projectsResponse, isLoading, isError, refetch } = useGetAllProjects(numericWorkspaceId || 0, memberId);
const createProject = useCreateProject(); const createProject = useCreateProject();
const handleCreateProject = (data: { title: string; labelType: 'Classification' | 'Detection' | 'Segmentation' }) => { const handleCreateProject = (data: { title: string; labelType: 'classification' | 'detection' | 'segmentation' }) => {
createProject.mutate( createProject.mutate(
{ {
workspaceId: numericWorkspaceId, workspaceId: numericWorkspaceId,
memberId, memberId,
data: { title: data.title, projectType: data.labelType.toLowerCase() }, data: { title: data.title, projectType: data.labelType },
}, },
{ {
onSuccess: () => { onSuccess: () => {
@ -29,8 +37,6 @@ export default function WorkspaceBrowseDetail() {
}, },
onError: (error) => { onError: (error) => {
console.error('프로젝트 생성 실패:', error); console.error('프로젝트 생성 실패:', error);
console.log('Error details:', JSON.stringify(error, null, 2));
const errorMessage = error?.response?.data?.message || error.message || '알 수 없는 오류'; const errorMessage = error?.response?.data?.message || error.message || '알 수 없는 오류';
console.error('프로젝트 생성 실패:', errorMessage); console.error('프로젝트 생성 실패:', errorMessage);
}, },
@ -38,9 +44,21 @@ export default function WorkspaceBrowseDetail() {
); );
}; };
const projects = Array.isArray(projectsResponse?.data?.workspaceResponses) const projects: ProjectResponseDTO[] = projectsResponse?.data?.workspaceResponses ?? [];
? projectsResponse.data.workspaceResponses
: []; if (isNaN(numericWorkspaceId)) {
return (
<div className="flex h-full w-full flex-col items-center justify-center">
<div className="flex flex-col items-center">
<Smile
size={48}
className="mb-2 text-gray-300"
/>
<div className="body text-gray-400"> </div>
</div>
</div>
);
}
if (isLoading) { if (isLoading) {
return <p>Loading projects...</p>; return <p>Loading projects...</p>;
@ -74,7 +92,7 @@ export default function WorkspaceBrowseDetail() {
</div> </div>
{projects.length > 0 ? ( {projects.length > 0 ? (
<div className="flex flex-wrap gap-6"> <div className="flex flex-wrap gap-6">
{projects.map((project: { id: Key | null | undefined; title: string; projectType: string }) => ( {projects.map((project: ProjectResponseDTO) => (
<ProjectCard <ProjectCard
key={project.id} key={project.id}
title={project.title} title={project.title}

View File

@ -6,10 +6,11 @@ import useAuthStore from '@/stores/useAuthStore';
import WorkSpaceCreateModal from '../WorkSpaceCreateModal'; import WorkSpaceCreateModal from '../WorkSpaceCreateModal';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { WorkspaceRequestDTO, WorkspaceResponseDTO, CustomError } from '@/types';
export default function WorkspaceBrowseLayout() { export default function WorkspaceBrowseLayout() {
const { profile, isLoggedIn } = useAuthStore(); const { profile, isLoggedIn } = useAuthStore();
const memberId = profile.id; const memberId = profile?.id ?? 0;
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -26,7 +27,7 @@ export default function WorkspaceBrowseLayout() {
const createWorkspace = useCreateWorkspace(); const createWorkspace = useCreateWorkspace();
const handleCreateWorkspace = (data: { title: string; content: string }) => { const handleCreateWorkspace = (data: WorkspaceRequestDTO) => {
if (!memberId) return; if (!memberId) return;
createWorkspace.mutate( createWorkspace.mutate(
{ memberId, data }, { memberId, data },
@ -35,7 +36,7 @@ export default function WorkspaceBrowseLayout() {
console.log('워크스페이스가 성공적으로 생성되었습니다.'); console.log('워크스페이스가 성공적으로 생성되었습니다.');
queryClient.invalidateQueries({ queryKey: ['workspaces'] }); queryClient.invalidateQueries({ queryKey: ['workspaces'] });
}, },
onError: (error: AxiosError) => { onError: (error: AxiosError<CustomError>) => {
console.error('워크스페이스 생성 실패:', error.message); console.error('워크스페이스 생성 실패:', error.message);
}, },
} }
@ -61,7 +62,7 @@ export default function WorkspaceBrowseLayout() {
<WorkSpaceCreateModal onSubmit={handleCreateWorkspace} /> <WorkSpaceCreateModal onSubmit={handleCreateWorkspace} />
</div> </div>
{workspaces.length > 0 ? ( {workspaces.length > 0 ? (
workspaces.map((workspace) => ( workspaces.map((workspace: WorkspaceResponseDTO) => (
<NavLink <NavLink
to={`/browse/${workspace.id}`} to={`/browse/${workspace.id}`}
key={workspace.id} key={workspace.id}

View File

@ -57,4 +57,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
); );
Button.displayName = 'Button'; Button.displayName = 'Button';
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }; export { Button, buttonVariants };

View File

@ -1,34 +1,25 @@
import * as React from "react" import * as React from 'react';
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot';
import { import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Label } from "@/components/ui/label" import { Label } from '@/components/ui/label';
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
@ -36,21 +27,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error('useFormField should be used within <FormField>');
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -59,112 +50,107 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
{} as FormItemContextValue
)
const FormItem = React.forwardRef< const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => {
React.HTMLAttributes<HTMLDivElement> const id = React.useId();
>(({ className, ...props }, ref) => {
const id = React.useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div
</FormItemContext.Provider> ref={ref}
) className={cn('space-y-2', className)}
}) {...props}
FormItem.displayName = "FormItem" />
</FormItemContext.Provider>
);
}
);
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
ref={ref} ref={ref}
className={cn(error && "text-red-500 dark:text-red-900", className)} className={cn(error && 'text-red-500 dark:text-red-900', className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef< const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
React.ElementRef<typeof Slot>, ({ ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof Slot> const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
!error aria-invalid={!!error}
? `${formDescriptionId}` {...props}
: `${formDescriptionId} ${formMessageId}` />
} );
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
} }
);
FormControl.displayName = 'FormControl';
return ( const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
<p ({ className, ...props }, ref) => {
ref={ref} const { formDescriptionId } = useFormField();
id={formMessageId}
className={cn("text-sm font-medium text-red-500 dark:text-red-900", className)} return (
{...props} <p
> ref={ref}
{body} id={formDescriptionId}
</p> className={cn('text-sm text-gray-500 dark:text-gray-400', className)}
) {...props}
}) />
FormMessage.displayName = "FormMessage" );
}
);
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-red-500 dark:text-red-900', className)}
{...props}
>
{body}
</p>
);
}
);
FormMessage.displayName = 'FormMessage';
export { export {
// eslint-disable-next-line react-refresh/only-export-components
useFormField, useFormField,
Form, Form,
FormItem, FormItem,
@ -173,4 +159,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };