From ade83e966fc9ddaac4a3350c9071090655dd0e34 Mon Sep 17 00:00:00 2001 From: jhynsoo Date: Fri, 27 Sep 2024 15:02:30 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 35 ++++ frontend/package.json | 1 + frontend/src/App.tsx | 2 + frontend/src/components/ImageCanvas/index.tsx | 9 +- .../ProjectCreateModal/ProjectCreateForm.tsx | 69 ++++++- .../components/ProjectCreateModal/index.tsx | 1 + .../components/WorkSpaceCreateModal/index.tsx | 11 +- .../WorkspaceLabelBar/LabelButton.tsx | 49 +++-- .../WorkspaceSidebar/ProjectStructure.tsx | 21 +- frontend/src/components/ui/toast.tsx | 105 ++++++++++ frontend/src/components/ui/toaster.tsx | 26 +++ frontend/src/hooks/use-toast.ts | 191 ++++++++++++++++++ frontend/src/router/index.tsx | 2 +- frontend/src/stores/useAuthStore.ts | 2 +- frontend/src/stores/useProjectStore.ts | 6 +- frontend/src/types/index.ts | 3 +- 16 files changed, 489 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/ui/toast.tsx create mode 100644 frontend/src/components/ui/toaster.tsx create mode 100644 frontend/src/hooks/use-toast.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 646ccad..b372119 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.52.1", "axios": "^1.7.5", @@ -4581,6 +4582,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz", + "integrity": "sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index e7183d9..28936da 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.52.1", "axios": "^1.7.5", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a99e427..bfd6abc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { RouterProvider } from 'react-router-dom'; import router from './router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Toaster } from './components/ui/toaster'; const queryClient = new QueryClient(); @@ -8,6 +9,7 @@ function App() { return ( + ); } diff --git a/frontend/src/components/ImageCanvas/index.tsx b/frontend/src/components/ImageCanvas/index.tsx index d25fe5e..4e39270 100644 --- a/frontend/src/components/ImageCanvas/index.tsx +++ b/frontend/src/components/ImageCanvas/index.tsx @@ -13,6 +13,7 @@ import useProjectStore from '@/stores/useProjectStore'; import { LABEL_CATEGORY } from '@/constants'; import { useQueryClient } from '@tanstack/react-query'; import useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery'; +import { useToast } from '@/hooks/use-toast'; export default function ImageCanvas() { const { project, folderId } = useProjectStore(); @@ -31,6 +32,7 @@ export default function ImageCanvas() { const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]); const saveImageLabelsMutation = useSaveImageLabelsQuery(); const queryClient = useQueryClient(); + const { toast } = useToast(); useEffect(() => { setLabels( @@ -73,9 +75,14 @@ export default function ImageCanvas() { { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['folder', project!.id.toString(), folderId] }); + toast({ + title: '저장 성공', + }); }, onError: () => { - alert('레이블 데이터 저장 실패'); + toast({ + title: '저장 실패', + }); }, } ); diff --git a/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx b/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx index 458b051..81c0efa 100644 --- a/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx +++ b/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx @@ -5,12 +5,17 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from ' import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; +import { useRef, useState } from 'react'; +import { X } from 'lucide-react'; const formSchema = z.object({ projectName: z.string().max(50).min(1, { message: '이름을 입력해주세요.', }), labelType: z.enum(['Classification', 'Detection', 'Segmentation']), + categories: z.array(z.string()).min(1, { + message: '카테고리를 하나 이상 입력해주세요.', + }), }); export type ProjectCreateFormValues = z.infer; @@ -21,16 +26,32 @@ const defaultValues: Partial = { }; export default function ProjectCreateForm({ onSubmit }: { onSubmit: (data: ProjectCreateFormValues) => void }) { + const [categories, setCategories] = useState([]); const form = useForm({ resolver: zodResolver(formSchema), - defaultValues, + defaultValues: { ...defaultValues, categories }, }); + const categoryRef = useRef(null); + const handleAddCategory = (event: React.MouseEvent, onChange: (value: string[]) => void) => { + event.preventDefault(); + + const category = categoryRef.current?.value; + if (!category) return; + + const newCategories = [...categories, category]; + + if (!categories.includes(category)) { + onChange(newCategories); + setCategories(newCategories); + } + categoryRef.current!.value = ''; + }; return (
)} /> + ( + <> +
카테고리
+
+ + + + +
+ {categories.length > 0 && ( +
    + {categories.map((category: string, index: number) => ( +
    + {category} + { + const newCategories = categories.filter((_, i) => i !== index); + field.onChange(newCategories); + setCategories(newCategories); + }} + /> +
    + ))} +
+ )} + + + )} + /> diff --git a/frontend/src/components/WorkspaceLabelBar/LabelButton.tsx b/frontend/src/components/WorkspaceLabelBar/LabelButton.tsx index c863d34..753274a 100644 --- a/frontend/src/components/WorkspaceLabelBar/LabelButton.tsx +++ b/frontend/src/components/WorkspaceLabelBar/LabelButton.tsx @@ -1,5 +1,5 @@ -import { LABEL_CATEGORY } from '@/constants'; import useCanvasStore from '@/stores/useCanvasStore'; +import useProjectStore from '@/stores/useProjectStore'; import { Label } from '@/types'; import { Trash2 } from 'lucide-react'; import { MouseEventHandler } from 'react'; @@ -12,13 +12,12 @@ export default function LabelButton({ setSelectedLabelId, }: Label & { selected: boolean; setSelectedLabelId: (id: number) => void }) { const { labels, setLabels } = useCanvasStore(); + const { categories } = useProjectStore(); const handleClick: MouseEventHandler = () => { - console.log(`LabelButton ${id} clicked`); setSelectedLabelId(id); }; const handleDelete: MouseEventHandler = (event) => { event.stopPropagation(); - console.log(`Delete LabelButton ${id}`); setLabels(labels.filter((label) => label.id !== id)); }; @@ -27,32 +26,32 @@ export default function LabelButton({ className={`flex items-center rounded-lg transition-colors ${selected ? 'bg-gray-200' : 'bg-gray-50 hover:bg-gray-100'}`} >
- +
+ +
}> ), diff --git a/frontend/src/stores/useAuthStore.ts b/frontend/src/stores/useAuthStore.ts index fa48c8a..f4b82e4 100644 --- a/frontend/src/stores/useAuthStore.ts +++ b/frontend/src/stores/useAuthStore.ts @@ -11,7 +11,7 @@ interface AuthState { } const useAuthStore = create()( - persist( + persist( (set) => ({ accessToken: '', profile: null, diff --git a/frontend/src/stores/useProjectStore.ts b/frontend/src/stores/useProjectStore.ts index 55798a9..6937735 100644 --- a/frontend/src/stores/useProjectStore.ts +++ b/frontend/src/stores/useProjectStore.ts @@ -1,11 +1,13 @@ import { create } from 'zustand'; -import { Project } from '@/types'; +import { LabelCategoryResponse, Project } from '@/types'; interface ProjectState { project: Project | null; setProject: (project: Project | null) => void; folderId: number; setFolderId: (folderId: number) => void; + categories: LabelCategoryResponse[]; + setCategories: (categories: LabelCategoryResponse[]) => void; } const useProjectStore = create((set) => ({ @@ -13,6 +15,8 @@ const useProjectStore = create((set) => ({ setProject: (project) => set({ project }), folderId: 0, setFolderId: (folderId) => set({ folderId }), + categories: [], + setCategories: (categories) => set({ categories }), })); export default useProjectStore; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 6514f0c..9b7728f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -122,6 +122,7 @@ export interface WorkspaceListResponse { export interface ProjectRequest { title: string; projectType: 'classification' | 'detection' | 'segmentation'; + categories: string[]; } export type ProjectResponse = { @@ -281,7 +282,7 @@ export interface ImageFolderRequest { } export interface LabelCategoryResponse { id: number; - name: string; + labelName: string; } // 카테고리 요청 DTO export interface LabelCategoryRequest {