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 { Button } from '@/components/ui/button';
import { fetchProfileApi, reissueTokenApi } from '@/api/authApi';
import { SuccessResponse, MemberResponseDTO, CustomError } from '@/types';
import { AxiosError } from 'axios';
const DOMAIN = 'https://j11s002.p.ssafy.io';
@ -12,24 +14,20 @@ export default function Home() {
const { isLoggedIn, setLoggedIn, profile, setProfile } = useAuthStore();
const hasFetchedProfile = useRef(false);
if (!isLoggedIn && !profile.id && !hasFetchedProfile.current) {
const accessToken = localStorage.getItem('accessToken');
if (!isLoggedIn && !profile && !hasFetchedProfile.current) {
const accessToken = sessionStorage.getItem('accessToken');
if (accessToken) {
setLoggedIn(true, accessToken);
fetchProfileApi()
.then((data) => {
.then((data: SuccessResponse<MemberResponseDTO>) => {
if (data?.isSuccess && data.data) {
setProfile({
id: data.data.id,
nickname: data.data.nickname,
profileImage: data.data.profileImage,
});
setProfile(data.data);
hasFetchedProfile.current = true;
}
})
.catch((error) => {
.catch((error: AxiosError<CustomError>) => {
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 { Plus } from 'lucide-react';
export default function ProjectCreateModal({
onSubmit,
}: {
onSubmit: (data: { title: string; labelType: 'Classification' | 'Detection' | 'Segmentation' }) => void;
}) {
interface ProjectCreateModalProps {
onSubmit: (data: { title: string; labelType: 'classification' | 'detection' | 'segmentation' }) => void;
}
export default function ProjectCreateModal({ onSubmit }: ProjectCreateModalProps) {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true);
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 (
<Dialog
open={isOpen}
@ -35,7 +48,7 @@ export default function ProjectCreateModal({
onSubmit={(data: ProjectCreateFormValues) => {
const formattedData = {
title: data.projectName,
labelType: data.labelType,
labelType: formatLabelType(data.labelType),
};
onSubmit(formattedData);
handleClose();

View File

@ -19,7 +19,11 @@ const defaultValues: Partial<WorkSpaceCreateFormValues> = {
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>({
resolver: zodResolver(formSchema),
defaultValues,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +1,25 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form';
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider
const Form = FormProvider;
type FormFieldContextValue<
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>(
{} as FormFieldContextValue
)
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@ -36,21 +27,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState)
const fieldState = getFieldState(fieldContext.name, formState);
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 {
id,
@ -59,112 +50,107 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
return (
<FormItemContext.Provider value={{ id }}>
<div
ref={ref}
className={cn('space-y-2', className)}
{...props}
/>
</FormItemContext.Provider>
);
}
);
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
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}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
);
});
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${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
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
);
FormControl.displayName = 'FormControl';
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"
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;
}
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 {
// eslint-disable-next-line react-refresh/only-export-components
useFormField,
Form,
FormItem,
@ -173,4 +159,4 @@ export {
FormDescription,
FormMessage,
FormField,
}
};