Refactor: 워크스페이스 페이지 리팩토링 - S11P21S002-139
This commit is contained in:
parent
04ba6cf258
commit
6b53eccd5c
@ -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 || '알 수 없는 오류');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user