From 038aca1a4ed16ccad0668907602332a731b1615b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:55:27 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat(src):=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=B8=EC=A7=91=20api=20=EC=88=98=EC=A0=95(ordin?= =?UTF-8?q?al=20=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/categories.ts | 10 ++---- frontend/src/api/index.ts | 2 +- frontend/src/hooks/category/useCategory.ts | 4 +-- frontend/src/mocks/fixtures/categoryList.json | 12 ++++--- frontend/src/mocks/handlers/category.ts | 33 ++----------------- .../CategoryFilterMenu/CategoryFilterMenu.tsx | 4 ++- frontend/src/queries/categories/index.ts | 1 - .../categories/useCategoryDeleteMutation.ts | 32 ------------------ frontend/src/types/api.ts | 13 ++------ frontend/src/types/index.ts | 2 -- frontend/src/types/template.ts | 1 + 11 files changed, 24 insertions(+), 90 deletions(-) delete mode 100644 frontend/src/queries/categories/useCategoryDeleteMutation.ts diff --git a/frontend/src/api/categories.ts b/frontend/src/api/categories.ts index 501c617cd..f1c75bf28 100644 --- a/frontend/src/api/categories.ts +++ b/frontend/src/api/categories.ts @@ -1,6 +1,6 @@ import { apiClient } from '@/api/config'; import { END_POINTS } from '@/routes'; -import type { CategoryUploadRequest, CategoryEditRequest, CategoryDeleteRequest } from '@/types'; +import type { CategoryEditRequest, Category } from '@/types'; export const getCategoryList = async (memberId: number) => { const queryParams = new URLSearchParams({ @@ -11,14 +11,10 @@ export const getCategoryList = async (memberId: number) => { return await response.json(); }; -export const postCategory = async (newCategory: CategoryUploadRequest) => { +export const postCategory = async (newCategory: Omit) => { const response = await apiClient.post(`${END_POINTS.CATEGORIES}`, newCategory); return await response.json(); }; -export const editCategory = async ({ id, name }: CategoryEditRequest) => - await apiClient.put(`${END_POINTS.CATEGORIES}/${id}`, { name }); - -export const deleteCategory = async ({ id }: CategoryDeleteRequest) => - await apiClient.delete(`${END_POINTS.CATEGORIES}/${id}`); +export const editCategory = async (body: CategoryEditRequest) => await apiClient.put(`${END_POINTS.CATEGORIES}`, body); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 44bc59bf3..ce81c97fb 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -9,7 +9,7 @@ export { deleteTemplate, } from './templates'; export { postSignup, postLogin, postLogout, getLoginState, checkName } from './authentication'; -export { getCategoryList, postCategory, editCategory, deleteCategory } from './categories'; +export { getCategoryList, postCategory, editCategory } from './categories'; export { getTagList } from './tags'; export { postLike, deleteLike } from './like'; export { getMemberName } from './members'; diff --git a/frontend/src/hooks/category/useCategory.ts b/frontend/src/hooks/category/useCategory.ts index 7be45b209..fb44c7851 100644 --- a/frontend/src/hooks/category/useCategory.ts +++ b/frontend/src/hooks/category/useCategory.ts @@ -12,7 +12,7 @@ export const useCategory = ({ memberId, initCategory }: Props) => { const options = data?.categories || []; if (!initCategory) { - initCategory = { id: options[0]?.id, name: '카테고리 없음' }; + initCategory = { id: options[0]?.id, name: '카테고리 없음', ordinal: 0 }; } const { isOpen, toggleDropdown, currentValue, handleCurrentValue, dropdownRef } = useDropdown(initCategory); @@ -31,7 +31,7 @@ export const useCategory = ({ memberId, initCategory }: Props) => { return; } - const newCategory = { name: categoryName }; + const newCategory = { name: categoryName, ordinal: options.length }; await postCategory(newCategory); }; diff --git a/frontend/src/mocks/fixtures/categoryList.json b/frontend/src/mocks/fixtures/categoryList.json index ed83c49aa..ab45e18d6 100644 --- a/frontend/src/mocks/fixtures/categoryList.json +++ b/frontend/src/mocks/fixtures/categoryList.json @@ -2,19 +2,23 @@ "categories": [ { "id": 1, - "name": "카테고리 없음" + "name": "카테고리 없음", + "ordinal": 0 }, { "id": 2, - "name": "Category1" + "name": "Category1", + "ordinal": 1 }, { "id": 3, - "name": "Category2" + "name": "Category2", + "ordinal": 2 }, { "id": 4, - "name": "Category3" + "name": "Category3", + "ordinal": 3 } ] } diff --git a/frontend/src/mocks/handlers/category.ts b/frontend/src/mocks/handlers/category.ts index c5d95c7db..dc27ac920 100644 --- a/frontend/src/mocks/handlers/category.ts +++ b/frontend/src/mocks/handlers/category.ts @@ -1,7 +1,7 @@ import { http } from 'msw'; import { API_URL } from '@/api'; -import categories from '@/mocks/fixtures/categoryList.json'; +import categories from '@/mocks/fixtures/categoryList.json'; import { END_POINTS } from '@/routes'; import { Category } from '@/types'; import { mockResponse } from '@/utils/mockResponse'; @@ -33,19 +33,12 @@ export const categoryHandlers = [ }); }), - http.put(`${API_URL}${END_POINTS.CATEGORIES}/:id`, async (req) => { - const { id } = req.params; + http.put(`${API_URL}${END_POINTS.CATEGORIES}`, async (req) => { const updatedCategory = await req.request.json(); - const categoryIndex = mockCategoryList.findIndex((cat) => cat.id.toString() === id); - - if (categoryIndex !== -1 && typeof updatedCategory === 'object' && updatedCategory !== null) { - mockCategoryList[categoryIndex] = { id: parseInt(id as string), ...updatedCategory } as Category; + if (typeof updatedCategory === 'object' && updatedCategory !== null) { return mockResponse({ status: 200, - body: { - category: mockCategoryList[categoryIndex], - }, }); } @@ -56,24 +49,4 @@ export const categoryHandlers = [ }, }); }), - - http.delete(`${API_URL}${END_POINTS.CATEGORIES}/:id`, (req) => { - const { id } = req.params; - const categoryIndex = mockCategoryList.findIndex((cat) => cat.id.toString() === id); - - if (categoryIndex !== -1) { - mockCategoryList.splice(categoryIndex, 1); - - return mockResponse({ - status: 204, - }); - } - - return mockResponse({ - status: 404, - body: { - message: 'Category not found', - }, - }); - }), ]; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx index 55200604f..4e78afd27 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx @@ -42,7 +42,9 @@ const CategoryFilterMenu = ({ memberId, categoryList, onSelectCategory }: Catego } }; - const [defaultCategory, ...userCategories] = categoryList.length ? categoryList : [{ id: 0, name: '' }]; + const [defaultCategory, ...userCategories] = categoryList.length + ? categoryList + : [{ id: 0, name: '', ordinal: categoryList.length + 1 }]; const indexById: Record = useMemo(() => { const map: Record = { 0: 0, [defaultCategory.id]: categoryList.length }; diff --git a/frontend/src/queries/categories/index.ts b/frontend/src/queries/categories/index.ts index e3ff6d547..e8cde1b47 100644 --- a/frontend/src/queries/categories/index.ts +++ b/frontend/src/queries/categories/index.ts @@ -1,4 +1,3 @@ export { useCategoryListQuery } from './useCategoryListQuery'; export { useCategoryUploadMutation } from './useCategoryUploadMutation'; export { useCategoryEditMutation } from './useCategoryEditMutation'; -export { useCategoryDeleteMutation } from './useCategoryDeleteMutation'; diff --git a/frontend/src/queries/categories/useCategoryDeleteMutation.ts b/frontend/src/queries/categories/useCategoryDeleteMutation.ts deleted file mode 100644 index e84691c15..000000000 --- a/frontend/src/queries/categories/useCategoryDeleteMutation.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { QUERY_KEY, deleteCategory } from '@/api'; -import { ApiError } from '@/api/Error'; -import { ToastContext } from '@/contexts'; -import { useCustomContext } from '@/hooks'; -import { Category } from '@/types'; - -export const useCategoryDeleteMutation = (categories: Category[]) => { - const queryClient = useQueryClient(); - const { failAlert } = useCustomContext(ToastContext); - - return useMutation({ - mutationFn: deleteCategory, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [QUERY_KEY.CATEGORY_LIST] }); - }, - onError: (error, targetCategory) => { - if (error instanceof ApiError) { - const categoryId = targetCategory.id; - const categoryName = - categories.find((category) => category.id === categoryId)?.name || '카테고리를 찾을 수 없음'; - - if (error.statusCode === 400) { - failAlert(`템플릿이 존재하는 카테고리(${categoryName})는 삭제할 수 없습니다.`); - } else { - failAlert(`카테고리 삭제 중 오류가 발생했습니다: ${categoryName}`); - } - } - }, - }); -}; diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 9ca57781d..ca6e2a2e0 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -40,17 +40,10 @@ export interface CategoryListResponse { categories: Category[]; } -export interface CategoryUploadRequest { - name: string; -} - export interface CategoryEditRequest { - id: number; - name: string; -} - -export interface CategoryDeleteRequest { - id: number; + createCategories: Omit[]; + updateCategories: Category[]; + deleteCategoryIds: number[]; } export interface TagListResponse { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 667958552..991ea9678 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -9,9 +9,7 @@ export type { TemplateListRequest, LikePostRequest, LikeDeleteRequest, - CategoryUploadRequest, CategoryEditRequest, - CategoryDeleteRequest, CategoryListResponse, TagListResponse, GetMemberNameResponse, diff --git a/frontend/src/types/template.ts b/frontend/src/types/template.ts index 8fa14bdb6..f4cadbc31 100644 --- a/frontend/src/types/template.ts +++ b/frontend/src/types/template.ts @@ -20,6 +20,7 @@ export interface Tag { export interface Category { id: number; name: string; + ordinal: number; } export interface Template { From 2cb12f2dac5b070a30e8ed50a0969a10b0aec411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:07:45 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat(src):=20drag=EB=A1=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/images/drag.svg | 3 + frontend/src/assets/images/index.ts | 1 + .../category/useCategoryNameValidation.ts | 12 +- .../CategoryEditModal.style.ts | 2 + .../CategoryEditModal/CategoryEditModal.tsx | 421 ++++++++++++------ 5 files changed, 304 insertions(+), 135 deletions(-) create mode 100644 frontend/src/assets/images/drag.svg diff --git a/frontend/src/assets/images/drag.svg b/frontend/src/assets/images/drag.svg new file mode 100644 index 000000000..866bad48d --- /dev/null +++ b/frontend/src/assets/images/drag.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/index.ts b/frontend/src/assets/images/index.ts index 6dae1521d..b5b7ba918 100644 --- a/frontend/src/assets/images/index.ts +++ b/frontend/src/assets/images/index.ts @@ -20,6 +20,7 @@ export { default as LikeIcon } from './like'; export { default as PrivateIcon } from './private.svg'; export { default as PublicIcon } from './public.svg'; export { default as ShareIcon } from './share.svg'; +export { default as DragIcon } from './drag.svg'; // Logo export { default as CodeZapLogo } from './codezapLogo.svg'; diff --git a/frontend/src/hooks/category/useCategoryNameValidation.ts b/frontend/src/hooks/category/useCategoryNameValidation.ts index 908b5e846..b36fd5f76 100644 --- a/frontend/src/hooks/category/useCategoryNameValidation.ts +++ b/frontend/src/hooks/category/useCategoryNameValidation.ts @@ -6,8 +6,8 @@ const INVALID_NAMES = ['전체보기', '카테고리 없음', '']; export const useCategoryNameValidation = ( categories: Category[], - newCategories: { id: number; name: string }[], - editedCategories: Record, + newCategories: Category[], + editedCategories: Category[], ) => { const [invalidIds, setInvalidIds] = useState([]); @@ -39,13 +39,13 @@ export const useCategoryNameValidation = ( } }); - Object.entries(editedCategories).forEach(([id, name]) => { - const originalName = categories.find((category) => category.id === Number(id))?.name; + editedCategories.forEach(({ id, name }) => { + const originalName = categories.find((category) => category.id === id)?.name; if (INVALID_NAMES.includes(name)) { - invalidNames.add(Number(id)); + invalidNames.add(id); } else if (name !== originalName) { - addNameToMap(Number(id), name); + addNameToMap(id, name); } }); diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.style.ts b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.style.ts index 3d8a864fb..9fbdd64fc 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.style.ts +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.style.ts @@ -11,6 +11,8 @@ export const EditCategoryItemList = styled.div` `; export const EditCategoryItem = styled.div<{ hasError?: boolean; isButton?: boolean; disabled?: boolean }>` + cursor: move; + display: flex; gap: 1rem; align-items: center; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx index 6faeaa2b5..f095d9463 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/react'; -import { useState } from 'react'; +import { PropsWithChildren, useEffect, useRef, useState } from 'react'; -import { PencilIcon, SpinArrowIcon, TrashcanIcon } from '@/assets/images'; +import { PencilIcon, SpinArrowIcon, TrashcanIcon, DragIcon } from '@/assets/images'; import { Text, Modal, Input, Flex, Button } from '@/components'; import { useCategoryNameValidation } from '@/hooks/category'; -import { useCategoryDeleteMutation, useCategoryEditMutation, useCategoryUploadMutation } from '@/queries/categories'; +import { useCategoryEditMutation } from '@/queries/categories'; import { validateCategoryName } from '@/service/validates'; import { ICON_SIZE } from '@/style/styleConstants'; import { theme } from '@/style/theme'; -import type { Category, ErrorBody } from '@/types'; +import type { Category } from '@/types'; import * as S from './CategoryEditModal.style'; @@ -27,25 +27,23 @@ const CategoryEditModal = ({ handleCancelEdit, onDeleteCategory, }: CategoryEditModalProps) => { - const [editedCategories, setEditedCategories] = useState>({}); - const [categoriesToDelete, setCategoriesToDelete] = useState([]); - const [newCategories, setNewCategories] = useState<{ id: number; name: string }[]>([]); + const [editedCategories, setEditedCategories] = useState([]); + const [newCategories, setNewCategories] = useState([]); + const [deleteCategoryIds, setDeleteCategoryIds] = useState([]); const [editingCategoryId, setEditingCategoryId] = useState(null); const { mutateAsync: editCategory } = useCategoryEditMutation(); - const { mutateAsync: deleteCategory } = useCategoryDeleteMutation(categories); - const { mutateAsync: postCategory } = useCategoryUploadMutation(); const { invalidIds, isValid } = useCategoryNameValidation(categories, newCategories, editedCategories); const resetState = () => { - setEditedCategories({}); - setCategoriesToDelete([]); + setEditedCategories([]); + setDeleteCategoryIds([]); setNewCategories([]); setEditingCategoryId(null); }; - const isCategoryNew = (id: number) => newCategories.some((category) => category.id === id); + const isNewCategory = (id: number) => newCategories.some((category) => category.id === id); const handleNameInputChange = (id: number, name: string) => { const errorMessage = validateCategoryName(name); @@ -54,23 +52,44 @@ const CategoryEditModal = ({ return; } - if (isCategoryNew(id)) { + if (isNewCategory(id)) { setNewCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); } else { - setEditedCategories((prev) => ({ ...prev, [id]: name })); + const ordinal = editedCategories.find((category) => category.id === id)?.ordinal as number; + + setEditedCategories((prev) => [...prev, { id, name, ordinal }]); } }; + const handleDrag = (categories: Category[]) => { + const updatedCategories: Category[] = []; + const updatedNewCategories: Category[] = []; + + categories.forEach((category) => { + if (isNewCategory(category.id)) { + updatedNewCategories.push(category); + } else { + updatedCategories.push({ + ...category, + name: editedCategories.find((editedCategory) => editedCategory.id === category.id)?.name ?? category.name, + }); + } + }); + + setNewCategories(updatedNewCategories); + setEditedCategories(updatedCategories); + }; + const handleDeleteClick = (id: number) => { - if (isCategoryNew(id)) { + if (isNewCategory(id)) { setNewCategories((prev) => prev.filter((category) => category.id !== id)); } else { - setCategoriesToDelete((prev) => [...prev, id]); + setDeleteCategoryIds((prev) => [...prev, id]); } }; const handleRestoreClick = (id: number) => { - setCategoriesToDelete((prev) => prev.filter((categoryId) => categoryId !== id)); + setDeleteCategoryIds((prev) => prev.filter((categoryId) => categoryId !== id)); }; const handleEditClick = (id: number) => { @@ -78,9 +97,9 @@ const CategoryEditModal = ({ }; const handleNameInputBlur = (id: number) => { - const trimmedName = isCategoryNew(id) + const trimmedName = isNewCategory(id) ? newCategories.find((category) => category.id === id)?.name.trim() - : editedCategories[id]?.trim(); + : editedCategories.find((category) => category.id === id)?.name.trim(); if (trimmedName !== undefined) { handleNameInputChange(id, trimmedName); @@ -90,13 +109,15 @@ const CategoryEditModal = ({ }; const handleAddCategory = () => { - const newCategoryId = + const id = categories.length > 0 ? categories[categories.length - 1].id + newCategories.length + 1 : newCategories.length + 1; - setNewCategories((prev) => [...prev, { id: newCategoryId, name: '' }]); - setEditingCategoryId(newCategoryId); + const ordinal = categories.length + newCategories.length; + + setNewCategories((prev) => [...prev, { id, name: '', ordinal }]); + setEditingCategoryId(id); }; const handleSaveChanges = async () => { @@ -104,29 +125,20 @@ const CategoryEditModal = ({ return; } - try { - if (categoriesToDelete.length > 0) { - await Promise.all(categoriesToDelete.map((id) => deleteCategory({ id }))); - onDeleteCategory(categoriesToDelete); - } - - await Promise.all( - Object.entries(editedCategories).map(async ([id, name]) => { - const originalCategory = categories.find((category) => category.id === Number(id)); + const body = { + createCategories: newCategories.map(({ name, ordinal }) => ({ name, ordinal })), + updateCategories: editedCategories, + deleteCategoryIds, + }; - if (originalCategory && originalCategory.name !== name) { - await editCategory({ id: Number(id), name }); - } - }), - ); + await editCategory(body); - await Promise.all(newCategories.map((category) => postCategory({ name: category.name }))); - - resetState(); - toggleModal(); - } catch (error) { - console.error((error as ErrorBody).detail); + if (deleteCategoryIds.length > 0) { + onDeleteCategory(deleteCategoryIds); } + + resetState(); + toggleModal(); }; const handleCancelEditWithReset = () => { @@ -142,10 +154,12 @@ const CategoryEditModal = ({ ; + newCategories: Category[]; + editedCategories: Category[]; categoriesToDelete: number[]; editingCategoryId: number | null; invalidIds: number[]; + isNewCategory: (id: number) => boolean; + handleDrag: (categories: Category[]) => void; onEditClick: (id: number) => void; onDeleteClick: (id: number) => void; onRestoreClick: (id: number) => void; @@ -201,105 +217,252 @@ const CategoryItems = ({ categoriesToDelete, editingCategoryId, invalidIds, + isNewCategory, + handleDrag, onEditClick, onDeleteClick, onRestoreClick, onNameInputChange, onNameInputBlur, -}: CategoryItemsProps) => ( +}: CategoryItemsProps) => { + const categoriesMap = new Map(); + + [...categories, ...editedCategories, ...newCategories].forEach((category) => { + categoriesMap.set(category.id, category); + }); + + const initOrderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); + + const [orderedCategories, setOrderedCategories] = useState(initOrderedCategoriesArray); + + useEffect(() => { + const categoriesMap = new Map(); + + [...categories, ...editedCategories, ...newCategories].forEach((category) => { + categoriesMap.set(category.id, category); + }); + + const orderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); + + setOrderedCategories(orderedCategoriesArray); + }, [newCategories, editedCategories, categories]); + + const dragItem = useRef(null); + const dragOverItem = useRef(null); + + const handleDragStart = (e: React.DragEvent, position: number) => { + dragItem.current = position; + e.currentTarget.style.opacity = '0.5'; + }; + + const handleDragEnter = (e: React.DragEvent, position: number) => { + dragOverItem.current = position; + e.currentTarget.style.backgroundColor = '#f5f5f5'; + }; + + const handleDragEnd = (e: React.DragEvent) => { + if (dragItem.current === null || dragOverItem.current === null) { + return; + } + + e.currentTarget.style.opacity = '1'; + e.currentTarget.style.backgroundColor = ''; + + const copyListItems = [...orderedCategories]; + const dragItemContent = copyListItems[dragItem.current]; + + copyListItems.splice(dragItem.current, 1); + copyListItems.splice(dragOverItem.current, 0, dragItemContent); + + const updatedItems = copyListItems.map((item, index) => ({ + ...item, + ordinal: index + 1, + })); + + dragItem.current = null; + dragOverItem.current = null; + handleDrag(updatedItems); + setOrderedCategories(updatedItems); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.currentTarget.style.backgroundColor = ''; + }; + + return ( + <> + {orderedCategories.map(({ id, name }, index) => ( + handleDragStart(e, index)} + onDragEnter={(e) => handleDragEnter(e, index)} + onDragEnd={handleDragEnd} + onDragLeave={handleDragLeave} + onDragOver={(e) => e.preventDefault()} + > + {isNewCategory(id) ? ( + onNameInputChange(id, e.target.value)} + onBlur={() => onNameInputBlur(id)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNameInputBlur(id); + } + }} + onEditClick={() => onEditClick(id)} + onDeleteClick={() => onDeleteClick(id)} + /> + ) : ( + category.id === id)?.name ?? name} + isEditing={editingCategoryId === id} + isDeleted={categoriesToDelete.includes(id)} + onChange={(e) => onNameInputChange(id, e.target.value)} + onBlur={() => onNameInputBlur(id)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNameInputBlur(id); + } + }} + onEditClick={() => onEditClick(id)} + onDeleteClick={() => onDeleteClick(id)} + onRestoreClick={() => onRestoreClick(id)} + /> + )} + + ))} + + ); +}; + +interface ExistingCategoryItemProps { + id: number; + name: string; + isEditing: boolean; + isDeleted: boolean; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onEditClick: (id: number) => void; + onDeleteClick: (id: number) => void; + onRestoreClick: (id: number) => void; +} + +const ExistingCategoryItem = ({ + id, + name, + isEditing, + isDeleted, + onChange, + onBlur, + onKeyDown, + onEditClick, + onDeleteClick, + onRestoreClick, +}: ExistingCategoryItemProps) => ( <> - {categories.map(({ id, name }) => ( - - {categoriesToDelete.includes(id) ? ( - // 기존 : 삭제 상태 - <> - - - {name} - - - onRestoreClick(id)} /> - - ) : ( - <> - - {editingCategoryId === id ? ( - // 기존 : 수정 상태 - - onNameInputChange(id, e.target.value)} - onBlur={() => onNameInputBlur(id)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNameInputBlur(id); - } - }} - autoFocus - css={css` - font-weight: bold; - &::placeholder { - font-weight: normal; - color: ${theme.color.light.secondary_400}; - } - `} - /> - - ) : ( - // 기존 : 기본 상태 - - {editedCategories[id] !== undefined ? editedCategories[id] : name} - - )} - - onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> - - )} - - ))} - - {newCategories.map(({ id, name }) => ( - - - {editingCategoryId === id ? ( - // 생성 : 수정 상태 - - onNameInputChange(id, e.target.value)} - onBlur={() => onNameInputBlur(id)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNameInputBlur(id); - } - }} - autoFocus - css={css` - font-weight: bold; - &::placeholder { - font-weight: normal; - color: ${theme.color.light.secondary_400}; - } - `} - /> - + {isDeleted ? ( + <> + + + {name} + + + onRestoreClick(id)} /> + + ) : ( + <> + + {isEditing ? ( + ) : ( - // 생성 : 기본 상태 {name} )} - + onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> - - ))} + + )} ); +interface NewCategoryItemProps { + id: number; + name: string; + isEditing: boolean; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onEditClick: (id: number) => void; + onDeleteClick: (id: number) => void; +} + +const NewCategoryItem = ({ + id, + name, + isEditing, + onChange, + onBlur, + onKeyDown, + onEditClick, + onDeleteClick, +}: NewCategoryItemProps) => ( + <> + + {isEditing ? ( + + ) : ( + + {name} + + )} + + onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> + +); + +const CategoryName = ({ children }: PropsWithChildren) => ( + + + {children} + +); + +interface CategoryNameInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; +} + +const CategoryNameInput = ({ value, onChange, onBlur, onKeyDown }: CategoryNameInputProps) => ( + + + +); + interface IconButtonsProps { onRestoreClick?: () => void; onEditClick?: () => void; From f77574c706b46a763d267c018d1d13740f747046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:08:46 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor(LandingPage):=20EXPLAIN=20?= =?UTF-8?q?=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/LandingPage/LandingPage.tsx | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/LandingPage/LandingPage.tsx b/frontend/src/pages/LandingPage/LandingPage.tsx index 7b86d378e..f5402b901 100644 --- a/frontend/src/pages/LandingPage/LandingPage.tsx +++ b/frontend/src/pages/LandingPage/LandingPage.tsx @@ -21,6 +21,12 @@ const LandingPage = () => { const { isLogin } = useAuth(); + const EXPLAIN = [ + { title: 'ZAP하게 저장', description: '자주 쓰는 나의 코드를 간편하게 저장하세요' }, + { title: 'ZAP하게 관리', description: '직관적인 분류 시스템으로 체계적으로 관리하세요' }, + { title: 'ZAP하게 검색', description: '필요한 나의 코드를 빠르게 찾아 사용하세요' }, + ]; + return ( @@ -40,29 +46,15 @@ const LandingPage = () => { - - - - ZAP하게 저장 - - 자주 쓰는 나의 코드를 간편하게 저장하세요 - - - - - ZAP하게 관리 - - - 직관적인 분류 시스템으로 체계적으로 관리하세요 - - - - - - ZAP하게 검색 - - 필요한 나의 코드를 빠르게 찾아 사용하세요 - + {EXPLAIN.map((el, idx) => ( + + + + {el.title} + + {el.description} + + ))} From 2f47f9726315b034b06f1727a8a0b13ca4ea7116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Sun, 12 Jan 2025 08:14:21 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor(CategoryListSection):=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20ordinal=20=EC=98=A4?= =?UTF-8?q?=EB=A6=84=EC=B0=A8=EC=88=9C=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=ED=95=98=EC=97=AC=20props=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CategoryListSection/CategoryListSection.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryListSection/CategoryListSection.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryListSection/CategoryListSection.tsx index c5995080b..c2e93cda0 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryListSection/CategoryListSection.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryListSection/CategoryListSection.tsx @@ -14,7 +14,11 @@ const CategoryListSection = ({ onSelectCategory, memberId }: Props) => { return ( - + a.ordinal - b.ordinal)} + onSelectCategory={onSelectCategory} + /> ); }; From e6b238822b203ee9ef5b8fcc1d7b8de5b479d614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Sun, 12 Jan 2025 09:24:46 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor(CategoryEditModal):=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategoryEditModal/CategoryEditModal.tsx | 330 ++---------------- .../CategoryEditModal/CategoryItems.tsx | 156 +++++++++ .../CategoryEditModal/CategoryName.tsx | 14 + .../CategoryEditModal/CategoryNameInput.tsx | 34 ++ .../ExistingCategoryItem.tsx | 60 ++++ .../CategoryEditModal/IconButtons.tsx | 35 ++ .../CategoryEditModal/NewCategoryItem.tsx | 43 +++ 7 files changed, 362 insertions(+), 310 deletions(-) create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryItems.tsx create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryName.tsx create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryNameInput.tsx create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/ExistingCategoryItem.tsx create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/IconButtons.tsx create mode 100644 frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/NewCategoryItem.tsx diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx index f095d9463..9abb7edbd 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx @@ -1,15 +1,12 @@ -import { css } from '@emotion/react'; -import { PropsWithChildren, useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; -import { PencilIcon, SpinArrowIcon, TrashcanIcon, DragIcon } from '@/assets/images'; -import { Text, Modal, Input, Flex, Button } from '@/components'; +import { Text, Modal, Flex, Button } from '@/components'; import { useCategoryNameValidation } from '@/hooks/category'; import { useCategoryEditMutation } from '@/queries/categories'; import { validateCategoryName } from '@/service/validates'; -import { ICON_SIZE } from '@/style/styleConstants'; -import { theme } from '@/style/theme'; import type { Category } from '@/types'; +import CategoryItems from './CategoryItems'; import * as S from './CategoryEditModal.style'; interface CategoryEditModalProps { @@ -54,14 +51,25 @@ const CategoryEditModal = ({ if (isNewCategory(id)) { setNewCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); - } else { - const ordinal = editedCategories.find((category) => category.id === id)?.ordinal as number; - setEditedCategories((prev) => [...prev, { id, name, ordinal }]); + return; + } + + //TODO: targetCategory 네이밍 바꾸기 + const targetCategory = editedCategories.find((category) => category.id === id); + + if (targetCategory) { + setEditedCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); + + return; } + + const ordinal = categories.find((category) => category.id === id)?.ordinal as number; + + setEditedCategories((prev) => [...prev, { id, name, ordinal }]); }; - const handleDrag = (categories: Category[]) => { + const handleOrdinalChange = (categories: Category[]) => { const updatedCategories: Category[] = []; const updatedNewCategories: Category[] = []; @@ -114,7 +122,7 @@ const CategoryEditModal = ({ ? categories[categories.length - 1].id + newCategories.length + 1 : newCategories.length + 1; - const ordinal = categories.length + newCategories.length; + const ordinal = categories.length + 1 + newCategories.length; setNewCategories((prev) => [...prev, { id, name: '', ordinal }]); setEditingCategoryId(id); @@ -154,7 +162,7 @@ const CategoryEditModal = ({ boolean; - handleDrag: (categories: Category[]) => void; - onEditClick: (id: number) => void; - onDeleteClick: (id: number) => void; - onRestoreClick: (id: number) => void; - onNameInputChange: (id: number, name: string) => void; - onNameInputBlur: (id: number) => void; -} - -const CategoryItems = ({ - categories, - newCategories, - editedCategories, - categoriesToDelete, - editingCategoryId, - invalidIds, - isNewCategory, - handleDrag, - onEditClick, - onDeleteClick, - onRestoreClick, - onNameInputChange, - onNameInputBlur, -}: CategoryItemsProps) => { - const categoriesMap = new Map(); - - [...categories, ...editedCategories, ...newCategories].forEach((category) => { - categoriesMap.set(category.id, category); - }); - - const initOrderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); - - const [orderedCategories, setOrderedCategories] = useState(initOrderedCategoriesArray); - - useEffect(() => { - const categoriesMap = new Map(); - - [...categories, ...editedCategories, ...newCategories].forEach((category) => { - categoriesMap.set(category.id, category); - }); - - const orderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); - - setOrderedCategories(orderedCategoriesArray); - }, [newCategories, editedCategories, categories]); - - const dragItem = useRef(null); - const dragOverItem = useRef(null); - - const handleDragStart = (e: React.DragEvent, position: number) => { - dragItem.current = position; - e.currentTarget.style.opacity = '0.5'; - }; - - const handleDragEnter = (e: React.DragEvent, position: number) => { - dragOverItem.current = position; - e.currentTarget.style.backgroundColor = '#f5f5f5'; - }; - - const handleDragEnd = (e: React.DragEvent) => { - if (dragItem.current === null || dragOverItem.current === null) { - return; - } - - e.currentTarget.style.opacity = '1'; - e.currentTarget.style.backgroundColor = ''; - - const copyListItems = [...orderedCategories]; - const dragItemContent = copyListItems[dragItem.current]; - - copyListItems.splice(dragItem.current, 1); - copyListItems.splice(dragOverItem.current, 0, dragItemContent); - - const updatedItems = copyListItems.map((item, index) => ({ - ...item, - ordinal: index + 1, - })); - - dragItem.current = null; - dragOverItem.current = null; - handleDrag(updatedItems); - setOrderedCategories(updatedItems); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.currentTarget.style.backgroundColor = ''; - }; - - return ( - <> - {orderedCategories.map(({ id, name }, index) => ( - handleDragStart(e, index)} - onDragEnter={(e) => handleDragEnter(e, index)} - onDragEnd={handleDragEnd} - onDragLeave={handleDragLeave} - onDragOver={(e) => e.preventDefault()} - > - {isNewCategory(id) ? ( - onNameInputChange(id, e.target.value)} - onBlur={() => onNameInputBlur(id)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNameInputBlur(id); - } - }} - onEditClick={() => onEditClick(id)} - onDeleteClick={() => onDeleteClick(id)} - /> - ) : ( - category.id === id)?.name ?? name} - isEditing={editingCategoryId === id} - isDeleted={categoriesToDelete.includes(id)} - onChange={(e) => onNameInputChange(id, e.target.value)} - onBlur={() => onNameInputBlur(id)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNameInputBlur(id); - } - }} - onEditClick={() => onEditClick(id)} - onDeleteClick={() => onDeleteClick(id)} - onRestoreClick={() => onRestoreClick(id)} - /> - )} - - ))} - - ); -}; - -interface ExistingCategoryItemProps { - id: number; - name: string; - isEditing: boolean; - isDeleted: boolean; - onChange: (e: React.ChangeEvent) => void; - onBlur: () => void; - onKeyDown: (e: React.KeyboardEvent) => void; - onEditClick: (id: number) => void; - onDeleteClick: (id: number) => void; - onRestoreClick: (id: number) => void; -} - -const ExistingCategoryItem = ({ - id, - name, - isEditing, - isDeleted, - onChange, - onBlur, - onKeyDown, - onEditClick, - onDeleteClick, - onRestoreClick, -}: ExistingCategoryItemProps) => ( - <> - {isDeleted ? ( - <> - - - {name} - - - onRestoreClick(id)} /> - - ) : ( - <> - - {isEditing ? ( - - ) : ( - - {name} - - )} - - onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> - - )} - -); - -interface NewCategoryItemProps { - id: number; - name: string; - isEditing: boolean; - onChange: (e: React.ChangeEvent) => void; - onBlur: () => void; - onKeyDown: (e: React.KeyboardEvent) => void; - onEditClick: (id: number) => void; - onDeleteClick: (id: number) => void; -} - -const NewCategoryItem = ({ - id, - name, - isEditing, - onChange, - onBlur, - onKeyDown, - onEditClick, - onDeleteClick, -}: NewCategoryItemProps) => ( - <> - - {isEditing ? ( - - ) : ( - - {name} - - )} - - onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> - -); - -const CategoryName = ({ children }: PropsWithChildren) => ( - - - {children} - -); - -interface CategoryNameInputProps { - value: string; - onChange: (e: React.ChangeEvent) => void; - onBlur: () => void; - onKeyDown: (e: React.KeyboardEvent) => void; -} - -const CategoryNameInput = ({ value, onChange, onBlur, onKeyDown }: CategoryNameInputProps) => ( - - - -); - -interface IconButtonsProps { - onRestoreClick?: () => void; - onEditClick?: () => void; - onDeleteClick?: () => void; - restore?: boolean; - edit?: boolean; - delete?: boolean; -} - -const IconButtons = ({ onRestoreClick, onEditClick, onDeleteClick, restore, edit, delete: del }: IconButtonsProps) => ( - - {restore && ( - - - - )} - {edit && ( - - - - )} - {del && ( - - - - )} - -); - export default CategoryEditModal; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryItems.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryItems.tsx new file mode 100644 index 000000000..dfcbb89f9 --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryItems.tsx @@ -0,0 +1,156 @@ +import { useEffect, useRef, useState } from 'react'; + +import { Category } from '@/types'; + +import ExistingCategoryItem from './ExistingCategoryItem'; +import NewCategoryItem from './NewCategoryItem'; +import * as S from './CategoryEditModal.style'; + +interface CategoryItemsProps { + categories: Category[]; + newCategories: Category[]; + editedCategories: Category[]; + categoriesToDelete: number[]; + editingCategoryId: number | null; + invalidIds: number[]; + isNewCategory: (id: number) => boolean; + handleDrag: (categories: Category[]) => void; + onEditClick: (id: number) => void; + onDeleteClick: (id: number) => void; + onRestoreClick: (id: number) => void; + onNameInputChange: (id: number, name: string) => void; + onNameInputBlur: (id: number) => void; +} + +const CategoryItems = ({ + categories, + newCategories, + editedCategories, + categoriesToDelete, + editingCategoryId, + invalidIds, + isNewCategory, + handleDrag, + onEditClick, + onDeleteClick, + onRestoreClick, + onNameInputChange, + onNameInputBlur, +}: CategoryItemsProps) => { + const categoriesMap = new Map(); + + [...categories, ...editedCategories, ...newCategories].forEach((category) => { + categoriesMap.set(category.id, category); + }); + + const initOrderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); + + const [orderedCategories, setOrderedCategories] = useState(initOrderedCategoriesArray); + + useEffect(() => { + const categoriesMap = new Map(); + + [...categories, ...editedCategories, ...newCategories].forEach((category) => { + categoriesMap.set(category.id, category); + }); + + const orderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); + + setOrderedCategories(orderedCategoriesArray); + }, [newCategories, editedCategories, categories]); + + const dragItem = useRef(null); + const dragOverItem = useRef(null); + + const handleDragStart = (e: React.DragEvent, position: number) => { + dragItem.current = position; + e.currentTarget.style.opacity = '0.5'; + }; + + const handleDragEnter = (e: React.DragEvent, position: number) => { + dragOverItem.current = position; + e.currentTarget.style.backgroundColor = '#f5f5f5'; + }; + + const handleDragEnd = (e: React.DragEvent) => { + if (dragItem.current === null || dragOverItem.current === null) { + return; + } + + e.currentTarget.style.opacity = '1'; + e.currentTarget.style.backgroundColor = ''; + + const copyListItems = [...orderedCategories]; + const dragItemContent = copyListItems[dragItem.current]; + + copyListItems.splice(dragItem.current, 1); + copyListItems.splice(dragOverItem.current, 0, dragItemContent); + + const updatedItems = copyListItems.map((item, index) => ({ + ...item, + ordinal: index + 1, + })); + + dragItem.current = null; + dragOverItem.current = null; + handleDrag(updatedItems); + setOrderedCategories(updatedItems); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.currentTarget.style.backgroundColor = ''; + }; + + return ( + <> + {orderedCategories.map(({ id, name }, index) => ( + handleDragStart(e, index)} + onDragEnter={(e) => handleDragEnter(e, index)} + onDragEnd={handleDragEnd} + onDragLeave={handleDragLeave} + onDragOver={(e) => e.preventDefault()} + > + {isNewCategory(id) ? ( + onNameInputChange(id, e.target.value)} + onBlur={() => onNameInputBlur(id)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNameInputBlur(id); + } + }} + onEditClick={() => onEditClick(id)} + onDeleteClick={() => onDeleteClick(id)} + /> + ) : ( + category.id === id)?.name ?? name} + isEditing={editingCategoryId === id} + isDeleted={categoriesToDelete.includes(id)} + onChange={(e) => onNameInputChange(id, e.target.value)} + onBlur={() => onNameInputBlur(id)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNameInputBlur(id); + } + }} + onEditClick={() => onEditClick(id)} + onDeleteClick={() => onDeleteClick(id)} + onRestoreClick={() => onRestoreClick(id)} + /> + )} + + ))} + + ); +}; + +export default CategoryItems; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryName.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryName.tsx new file mode 100644 index 000000000..383d2eee1 --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryName.tsx @@ -0,0 +1,14 @@ +import { PropsWithChildren } from 'react'; + +import { DragIcon } from '@/assets/images'; +import { Flex } from '@/components'; +import { theme } from '@/style/theme'; + +const CategoryName = ({ children }: PropsWithChildren) => ( + + + {children} + +); + +export default CategoryName; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryNameInput.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryNameInput.tsx new file mode 100644 index 000000000..26ad8122c --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryNameInput.tsx @@ -0,0 +1,34 @@ +import { css } from '@emotion/react'; + +import { Input } from '@/components'; +import { theme } from '@/style/theme'; + +interface CategoryNameInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; +} + +const CategoryNameInput = ({ value, onChange, onBlur, onKeyDown }: CategoryNameInputProps) => ( + + + +); + +export default CategoryNameInput; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/ExistingCategoryItem.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/ExistingCategoryItem.tsx new file mode 100644 index 000000000..befafda1c --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/ExistingCategoryItem.tsx @@ -0,0 +1,60 @@ +import { Text } from '@/components'; +import { theme } from '@/style/theme'; + +import CategoryName from './CategoryName'; +import CategoryNameInput from './CategoryNameInput'; +import IconButtons from './IconButtons'; + +interface ExistingCategoryItemProps { + id: number; + name: string; + isEditing: boolean; + isDeleted: boolean; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onEditClick: (id: number) => void; + onDeleteClick: (id: number) => void; + onRestoreClick: (id: number) => void; +} + +const ExistingCategoryItem = ({ + id, + name, + isEditing, + isDeleted, + onChange, + onBlur, + onKeyDown, + onEditClick, + onDeleteClick, + onRestoreClick, +}: ExistingCategoryItemProps) => ( + <> + {isDeleted ? ( + <> + + + {name} + + + onRestoreClick(id)} /> + + ) : ( + <> + + {isEditing ? ( + + ) : ( + + {name} + + )} + + onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> + + )} + +); + +export default ExistingCategoryItem; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/IconButtons.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/IconButtons.tsx new file mode 100644 index 000000000..d6e4f1de9 --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/IconButtons.tsx @@ -0,0 +1,35 @@ +import { PencilIcon, SpinArrowIcon, TrashcanIcon } from '@/assets/images'; +import { ICON_SIZE } from '@/style/styleConstants'; + +import * as S from './CategoryEditModal.style'; + +interface IconButtonsProps { + onRestoreClick?: () => void; + onEditClick?: () => void; + onDeleteClick?: () => void; + restore?: boolean; + edit?: boolean; + delete?: boolean; +} + +const IconButtons = ({ onRestoreClick, onEditClick, onDeleteClick, restore, edit, delete: del }: IconButtonsProps) => ( + + {restore && ( + + + + )} + {edit && ( + + + + )} + {del && ( + + + + )} + +); + +export default IconButtons; diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/NewCategoryItem.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/NewCategoryItem.tsx new file mode 100644 index 000000000..cafbd4437 --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/NewCategoryItem.tsx @@ -0,0 +1,43 @@ +import { Text } from '@/components'; +import { theme } from '@/style/theme'; + +import CategoryName from './CategoryName'; +import CategoryNameInput from './CategoryNameInput'; +import IconButtons from './IconButtons'; + +interface NewCategoryItemProps { + id: number; + name: string; + isEditing: boolean; + onChange: (e: React.ChangeEvent) => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onEditClick: (id: number) => void; + onDeleteClick: (id: number) => void; +} + +const NewCategoryItem = ({ + id, + name, + isEditing, + onChange, + onBlur, + onKeyDown, + onEditClick, + onDeleteClick, +}: NewCategoryItemProps) => ( + <> + + {isEditing ? ( + + ) : ( + + {name} + + )} + + onEditClick(id)} onDeleteClick={() => onDeleteClick(id)} /> + +); + +export default NewCategoryItem; From 9d37b9351d1afbeab6d10af2f58d0260b29b21e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Sun, 12 Jan 2025 09:52:53 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor(CategoryEditModal):=20editedCate?= =?UTF-8?q?gories=20=EC=B4=88=EA=B8=B0=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CategoryEditModal/CategoryEditModal.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx index 9abb7edbd..8f5d4d981 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx @@ -24,7 +24,7 @@ const CategoryEditModal = ({ handleCancelEdit, onDeleteCategory, }: CategoryEditModalProps) => { - const [editedCategories, setEditedCategories] = useState([]); + const [editedCategories, setEditedCategories] = useState([...categories]); const [newCategories, setNewCategories] = useState([]); const [deleteCategoryIds, setDeleteCategoryIds] = useState([]); const [editingCategoryId, setEditingCategoryId] = useState(null); @@ -55,7 +55,6 @@ const CategoryEditModal = ({ return; } - //TODO: targetCategory 네이밍 바꾸기 const targetCategory = editedCategories.find((category) => category.id === id); if (targetCategory) { @@ -63,10 +62,6 @@ const CategoryEditModal = ({ return; } - - const ordinal = categories.find((category) => category.id === id)?.ordinal as number; - - setEditedCategories((prev) => [...prev, { id, name, ordinal }]); }; const handleOrdinalChange = (categories: Category[]) => { From 353a5d3f20881a59210c9daaf64b5d77cae99e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 22 Jan 2025 06:19:35 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat(src):=20editedCategoryList=EB=A1=9C?= =?UTF-8?q?=20=EB=AA=A8=EB=93=A0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20useCategoryEditMod?= =?UTF-8?q?al=20=ED=9B=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../category/useCategoryNameValidation.ts | 22 +-- .../CategoryEditModal/CategoryEditModal.tsx | 166 +++-------------- .../CategoryEditModal/CategoryItems.tsx | 72 +++----- .../CategoryFilterMenu/CategoryFilterMenu.tsx | 11 +- .../pages/MemberTemplatePage/hooks/index.ts | 1 + .../hooks/useCategoryEditModal.ts | 173 ++++++++++++++++++ 6 files changed, 232 insertions(+), 213 deletions(-) create mode 100644 frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts diff --git a/frontend/src/hooks/category/useCategoryNameValidation.ts b/frontend/src/hooks/category/useCategoryNameValidation.ts index b36fd5f76..f8e7757e5 100644 --- a/frontend/src/hooks/category/useCategoryNameValidation.ts +++ b/frontend/src/hooks/category/useCategoryNameValidation.ts @@ -4,11 +4,7 @@ import type { Category } from '@/types'; const INVALID_NAMES = ['전체보기', '카테고리 없음', '']; -export const useCategoryNameValidation = ( - categories: Category[], - newCategories: Category[], - editedCategories: Category[], -) => { +export const useCategoryNameValidation = (categoryList: Category[], editedCategoryList: Category[]) => { const [invalidIds, setInvalidIds] = useState([]); useEffect(() => { @@ -23,7 +19,7 @@ export const useCategoryNameValidation = ( allNames.get(name)!.push(id); }; - categories.forEach(({ id, name }) => { + categoryList.forEach(({ id, name }) => { if (INVALID_NAMES.includes(name)) { invalidNames.add(id); } else { @@ -31,16 +27,8 @@ export const useCategoryNameValidation = ( } }); - newCategories.forEach(({ id, name }) => { - if (INVALID_NAMES.includes(name)) { - invalidNames.add(id); - } else { - addNameToMap(id, name); - } - }); - - editedCategories.forEach(({ id, name }) => { - const originalName = categories.find((category) => category.id === id)?.name; + editedCategoryList.forEach(({ id, name }) => { + const originalName = categoryList.find((category) => category.id === id)?.name; if (INVALID_NAMES.includes(name)) { invalidNames.add(id); @@ -56,7 +44,7 @@ export const useCategoryNameValidation = ( }); setInvalidIds(Array.from(invalidNames)); - }, [categories, newCategories, editedCategories]); + }, [categoryList, editedCategoryList]); return { invalidIds, diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx index 8f5d4d981..0eaf89151 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx @@ -1,153 +1,35 @@ -import { useState } from 'react'; - import { Text, Modal, Flex, Button } from '@/components'; -import { useCategoryNameValidation } from '@/hooks/category'; -import { useCategoryEditMutation } from '@/queries/categories'; -import { validateCategoryName } from '@/service/validates'; import type { Category } from '@/types'; import CategoryItems from './CategoryItems'; +import { useCategoryEditModal } from '../../hooks'; import * as S from './CategoryEditModal.style'; interface CategoryEditModalProps { isOpen: boolean; toggleModal: () => void; - categories: Category[]; - handleCancelEdit: () => void; + categoryList: Category[]; onDeleteCategory: (deletedIds: number[]) => void; } -const CategoryEditModal = ({ - isOpen, - toggleModal, - categories, - handleCancelEdit, - onDeleteCategory, -}: CategoryEditModalProps) => { - const [editedCategories, setEditedCategories] = useState([...categories]); - const [newCategories, setNewCategories] = useState([]); - const [deleteCategoryIds, setDeleteCategoryIds] = useState([]); - const [editingCategoryId, setEditingCategoryId] = useState(null); - - const { mutateAsync: editCategory } = useCategoryEditMutation(); - - const { invalidIds, isValid } = useCategoryNameValidation(categories, newCategories, editedCategories); - - const resetState = () => { - setEditedCategories([]); - setDeleteCategoryIds([]); - setNewCategories([]); - setEditingCategoryId(null); - }; - - const isNewCategory = (id: number) => newCategories.some((category) => category.id === id); - - const handleNameInputChange = (id: number, name: string) => { - const errorMessage = validateCategoryName(name); - - if (errorMessage && name.length > 0) { - return; - } - - if (isNewCategory(id)) { - setNewCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); - - return; - } - - const targetCategory = editedCategories.find((category) => category.id === id); - - if (targetCategory) { - setEditedCategories((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); - - return; - } - }; - - const handleOrdinalChange = (categories: Category[]) => { - const updatedCategories: Category[] = []; - const updatedNewCategories: Category[] = []; - - categories.forEach((category) => { - if (isNewCategory(category.id)) { - updatedNewCategories.push(category); - } else { - updatedCategories.push({ - ...category, - name: editedCategories.find((editedCategory) => editedCategory.id === category.id)?.name ?? category.name, - }); - } - }); - - setNewCategories(updatedNewCategories); - setEditedCategories(updatedCategories); - }; - - const handleDeleteClick = (id: number) => { - if (isNewCategory(id)) { - setNewCategories((prev) => prev.filter((category) => category.id !== id)); - } else { - setDeleteCategoryIds((prev) => [...prev, id]); - } - }; - - const handleRestoreClick = (id: number) => { - setDeleteCategoryIds((prev) => prev.filter((categoryId) => categoryId !== id)); - }; - - const handleEditClick = (id: number) => { - setEditingCategoryId(id); - }; - - const handleNameInputBlur = (id: number) => { - const trimmedName = isNewCategory(id) - ? newCategories.find((category) => category.id === id)?.name.trim() - : editedCategories.find((category) => category.id === id)?.name.trim(); - - if (trimmedName !== undefined) { - handleNameInputChange(id, trimmedName); - } - - setEditingCategoryId(null); - }; - - const handleAddCategory = () => { - const id = - categories.length > 0 - ? categories[categories.length - 1].id + newCategories.length + 1 - : newCategories.length + 1; - - const ordinal = categories.length + 1 + newCategories.length; - - setNewCategories((prev) => [...prev, { id, name: '', ordinal }]); - setEditingCategoryId(id); - }; - - const handleSaveChanges = async () => { - if (!isValid) { - return; - } - - const body = { - createCategories: newCategories.map(({ name, ordinal }) => ({ name, ordinal })), - updateCategories: editedCategories, - deleteCategoryIds, - }; - - await editCategory(body); - - if (deleteCategoryIds.length > 0) { - onDeleteCategory(deleteCategoryIds); - } - - resetState(); - toggleModal(); - }; - - const handleCancelEditWithReset = () => { - resetState(); - handleCancelEdit(); - }; +const CategoryEditModal = ({ isOpen, toggleModal, categoryList, onDeleteCategory }: CategoryEditModalProps) => { + const { + editedCategoryList, + deleteCategoryIds, + editingCategoryId, + invalidIds, + isValid, + isNewCategory, + handleNameInputChange, + handleOrdinalChange, + handleDeleteClick, + handleRestoreClick, + handleEditClick, + handleNameInputBlur, + handleAddCategory, + handleSaveChanges, + handleCancelEditWithReset, + } = useCategoryEditModal({ categoryList, toggleModal, onDeleteCategory }); return ( @@ -155,13 +37,11 @@ const CategoryEditModal = ({ boolean; - handleDrag: (categories: Category[]) => void; + handleOrdinalChange: (categoryList: Category[]) => void; onEditClick: (id: number) => void; onDeleteClick: (id: number) => void; onRestoreClick: (id: number) => void; @@ -23,41 +22,19 @@ interface CategoryItemsProps { } const CategoryItems = ({ - categories, - newCategories, - editedCategories, - categoriesToDelete, + editedCategoryList, + deleteCategoryIds, editingCategoryId, invalidIds, isNewCategory, - handleDrag, + handleOrdinalChange, onEditClick, onDeleteClick, onRestoreClick, onNameInputChange, onNameInputBlur, }: CategoryItemsProps) => { - const categoriesMap = new Map(); - - [...categories, ...editedCategories, ...newCategories].forEach((category) => { - categoriesMap.set(category.id, category); - }); - - const initOrderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); - - const [orderedCategories, setOrderedCategories] = useState(initOrderedCategoriesArray); - - useEffect(() => { - const categoriesMap = new Map(); - - [...categories, ...editedCategories, ...newCategories].forEach((category) => { - categoriesMap.set(category.id, category); - }); - - const orderedCategoriesArray = Array.from(categoriesMap.values()).sort((a, b) => a.ordinal - b.ordinal); - - setOrderedCategories(orderedCategoriesArray); - }, [newCategories, editedCategories, categories]); + const orderedCategoryList = [...editedCategoryList].sort((a, b) => a.ordinal - b.ordinal); const dragItem = useRef(null); const dragOverItem = useRef(null); @@ -69,7 +46,7 @@ const CategoryItems = ({ const handleDragEnter = (e: React.DragEvent, position: number) => { dragOverItem.current = position; - e.currentTarget.style.backgroundColor = '#f5f5f5'; + e.currentTarget.style.backgroundColor = theme.color.dark.white; }; const handleDragEnd = (e: React.DragEvent) => { @@ -80,21 +57,22 @@ const CategoryItems = ({ e.currentTarget.style.opacity = '1'; e.currentTarget.style.backgroundColor = ''; - const copyListItems = [...orderedCategories]; - const dragItemContent = copyListItems[dragItem.current]; - - copyListItems.splice(dragItem.current, 1); - copyListItems.splice(dragOverItem.current, 0, dragItemContent); + const reorderedCategoryList = getReorderedCategoryList(orderedCategoryList, dragItem.current, dragOverItem.current); - const updatedItems = copyListItems.map((item, index) => ({ - ...item, - ordinal: index + 1, - })); + handleOrdinalChange(reorderedCategoryList); dragItem.current = null; dragOverItem.current = null; - handleDrag(updatedItems); - setOrderedCategories(updatedItems); + }; + + const getReorderedCategoryList = (categoryList: Category[], startIndex: number, endIndex: number) => { + const copyListItems = [...categoryList]; + const dragItem = copyListItems[startIndex]; + + copyListItems.splice(startIndex, 1); + copyListItems.splice(endIndex, 0, dragItem); + + return copyListItems; }; const handleDragLeave = (e: React.DragEvent) => { @@ -103,7 +81,7 @@ const CategoryItems = ({ return ( <> - {orderedCategories.map(({ id, name }, index) => ( + {orderedCategoryList.map(({ id, name }, index) => ( category.id === id)?.name ?? name} + name={editedCategoryList.find((category) => category.id === id)?.name ?? name} isEditing={editingCategoryId === id} - isDeleted={categoriesToDelete.includes(id)} + isDeleted={deleteCategoryIds.includes(id)} onChange={(e) => onNameInputChange(id, e.target.value)} onBlur={() => onNameInputBlur(id)} onKeyDown={(e) => { diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx index 4e78afd27..6f0357118 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryFilterMenu/CategoryFilterMenu.tsx @@ -42,19 +42,19 @@ const CategoryFilterMenu = ({ memberId, categoryList, onSelectCategory }: Catego } }; - const [defaultCategory, ...userCategories] = categoryList.length + const [defaultCategory, ...userCategoryList] = categoryList.length ? categoryList : [{ id: 0, name: '', ordinal: categoryList.length + 1 }]; const indexById: Record = useMemo(() => { const map: Record = { 0: 0, [defaultCategory.id]: categoryList.length }; - userCategories.forEach(({ id }, index) => { + userCategoryList.forEach(({ id }, index) => { map[id] = index + 1; }); return map; - }, [categoryList.length, defaultCategory.id, userCategories]); + }, [categoryList.length, defaultCategory.id, userCategoryList]); return ( <> @@ -76,7 +76,7 @@ const CategoryFilterMenu = ({ memberId, categoryList, onSelectCategory }: Catego handleCategorySelect(0)} /> - {userCategories.map(({ id, name }) => ( + {userCategoryList.map(({ id, name }) => ( handleCategorySelect(id)} /> @@ -101,8 +101,7 @@ const CategoryFilterMenu = ({ memberId, categoryList, onSelectCategory }: Catego diff --git a/frontend/src/pages/MemberTemplatePage/hooks/index.ts b/frontend/src/pages/MemberTemplatePage/hooks/index.ts index 4825988ac..458fd8cd9 100644 --- a/frontend/src/pages/MemberTemplatePage/hooks/index.ts +++ b/frontend/src/pages/MemberTemplatePage/hooks/index.ts @@ -1,2 +1,3 @@ export { useFilteredTemplateList } from './useFilteredTemplateList'; export { useSelectAndDeleteTemplateList } from './useSelectAndDeleteTemplateList'; +export { useCategoryEditModal } from './useCategoryEditModal'; diff --git a/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts b/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts new file mode 100644 index 000000000..294e98b24 --- /dev/null +++ b/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts @@ -0,0 +1,173 @@ +import { useEffect, useState } from 'react'; + +import { useCategoryNameValidation } from '@/hooks/category'; +import { useCategoryEditMutation } from '@/queries/categories'; +import { validateCategoryName } from '@/service/validates'; +import { Category } from '@/types'; + +interface Props { + categoryList: Category[]; + toggleModal: () => void; + onDeleteCategory: (deletedIds: number[]) => void; +} + +export const useCategoryEditModal = ({ categoryList, toggleModal, onDeleteCategory }: Props) => { + const [editedCategoryList, setEditedCategoryList] = useState([...categoryList]); + const [deleteCategoryIds, setDeleteCategoryIds] = useState([]); + const [editingCategoryId, setEditingCategoryId] = useState(null); + + const { mutateAsync: editCategory } = useCategoryEditMutation(); + + const { invalidIds, isValid } = useCategoryNameValidation(categoryList, editedCategoryList); + + useEffect(() => { + if (!isEqualCategoryList(categoryList, editedCategoryList)) { + setEditedCategoryList([...categoryList]); + } + }, [categoryList]); + + const isEqualCategoryList = (arr1: Category[], arr2: Category[]): boolean => { + if (arr1.length !== arr2.length) { + return false; + } + + return arr1.every((category, index) => { + const category2 = arr2[index]; + + return category.id === category2.id && category.name === category2.name && category.ordinal === category2.ordinal; + }); + }; + + const isNewCategory = (id: number) => categoryList.every((category) => category.id !== id); + + const resetState = () => { + setEditedCategoryList([...categoryList]); + setDeleteCategoryIds([]); + setEditingCategoryId(null); + }; + + const handleNameInputChange = (id: number, name: string) => { + const errorMessage = validateCategoryName(name); + + if (errorMessage && name.length > 0) { + return; + } + + setEditedCategoryList((prev) => prev.map((category) => (category.id === id ? { ...category, name } : category))); + }; + + const handleOrdinalChange = (categoryList: Category[]) => { + const updatedCategoryList = categoryList.map((category, index) => ({ + ...category, + ordinal: index + 1, + })); + + setEditedCategoryList(updatedCategoryList); + }; + + const handleDeleteClick = (id: number) => { + if (isNewCategory(id)) { + setEditedCategoryList((prev) => prev.filter((category) => category.id !== id)); + + const updatedCategoryList = [...editedCategoryList.filter((category) => category.id !== id)].sort( + (a, b) => a.ordinal - b.ordinal, + ); + + handleOrdinalChange(updatedCategoryList); + + return; + } + + setDeleteCategoryIds((prev) => [...prev, id]); + + const updatedCategoryList = [...editedCategoryList].sort((a, b) => a.ordinal - b.ordinal); + + handleOrdinalChange(updatedCategoryList); + }; + + const handleRestoreClick = (id: number) => { + setDeleteCategoryIds((prev) => prev.filter((categoryId) => categoryId !== id)); + }; + + const handleEditClick = (id: number) => { + setEditingCategoryId(id); + }; + + const handleNameInputBlur = (id: number) => { + const trimmedName = editedCategoryList.find((category) => category.id === id)?.name.trim(); + + if (trimmedName !== undefined) { + handleNameInputChange(id, trimmedName); + } + + setEditingCategoryId(null); + }; + + const handleAddCategory = () => { + const id = + categoryList.length > 0 + ? categoryList[categoryList.length - 1].id + editedCategoryList.length + 1 + : editedCategoryList.length + 1; + + const ordinal = editedCategoryList.length + 1; + + setEditedCategoryList((prev) => [...prev, { id, name: '', ordinal }]); + setEditingCategoryId(id); + }; + + const handleSaveChanges = async () => { + if (!isValid) { + return; + } + + const body = getCategoryEditRequestBody(); + + await editCategory(body); + + if (deleteCategoryIds.length > 0) { + onDeleteCategory(deleteCategoryIds); + } + + resetState(); + toggleModal(); + }; + + const getCategoryEditRequestBody = () => { + const filteredCategoryList = editedCategoryList + .filter(({ id }) => !deleteCategoryIds.includes(id)) + .map((category, idx) => ({ ...category, ordinal: idx + 1 })); + + const body = { + createCategories: filteredCategoryList + .filter(({ id }) => isNewCategory(id)) + .map(({ name, ordinal }) => ({ name, ordinal })), + updateCategories: filteredCategoryList.filter(({ id }) => !isNewCategory(id)), + deleteCategoryIds, + }; + + return body; + }; + + const handleCancelEditWithReset = () => { + resetState(); + toggleModal(); + }; + + return { + editedCategoryList, + deleteCategoryIds, + editingCategoryId, + invalidIds, + isValid, + isNewCategory, + handleNameInputChange, + handleOrdinalChange, + handleDeleteClick, + handleRestoreClick, + handleEditClick, + handleNameInputBlur, + handleAddCategory, + handleSaveChanges, + handleCancelEditWithReset, + }; +}; From aa4dd66dd9ddf37841a735632c61e857baecd94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 22 Jan 2025 07:32:00 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor(useCategoryEditModal):=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20id=20=EC=83=9D=EC=84=B1=EC=8B=9C=20Date.now()=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/MemberTemplatePage/hooks/useCategoryEditModal.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts b/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts index 294e98b24..28e077d0e 100644 --- a/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts +++ b/frontend/src/pages/MemberTemplatePage/hooks/useCategoryEditModal.ts @@ -104,10 +104,7 @@ export const useCategoryEditModal = ({ categoryList, toggleModal, onDeleteCatego }; const handleAddCategory = () => { - const id = - categoryList.length > 0 - ? categoryList[categoryList.length - 1].id + editedCategoryList.length + 1 - : editedCategoryList.length + 1; + const id = Date.now(); const ordinal = editedCategoryList.length + 1; From 7adc561958356a455717a22f4bd86216266dd7d7 Mon Sep 17 00:00:00 2001 From: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:41:03 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor(CategoryEditModel):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategoryEditModal/CategoryEditModal.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx index 44b2d08dc..0eaf89151 100644 --- a/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx +++ b/frontend/src/pages/MemberTemplatePage/components/CategoryEditModal/CategoryEditModal.tsx @@ -1,14 +1,5 @@ -import { css } from '@emotion/react'; -import { useState } from 'react'; - -import { PencilIcon, SpinArrowIcon, TrashcanIcon } from '@/assets/images'; -import { Text, Modal, Input, Flex, Button } from '@/components'; -import { useCategoryNameValidation } from '@/hooks/category'; -import { useCategoryDeleteMutation, useCategoryEditMutation, useCategoryUploadMutation } from '@/queries/categories'; -import { validateCategoryName } from '@/service/validates'; -import { ICON_SIZE } from '@/style/styleConstants'; -import { theme } from '@design/style/theme'; -import type { Category, ErrorBody } from '@/types'; +import { Text, Modal, Flex, Button } from '@/components'; +import type { Category } from '@/types'; import CategoryItems from './CategoryItems'; import { useCategoryEditModal } from '../../hooks'; From d921a3c1f0a167cd1acd49f48c77cf934d696baa Mon Sep 17 00:00:00 2001 From: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:44:08 +0900 Subject: [PATCH 10/11] =?UTF-8?q?chore:=20frontend=20CD=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20pnpm=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/frontend_cd.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/frontend_cd.yml b/.github/workflows/frontend_cd.yml index 6a6dffe28..72df7983d 100644 --- a/.github/workflows/frontend_cd.yml +++ b/.github/workflows/frontend_cd.yml @@ -22,6 +22,11 @@ jobs: with: node-version: "20" + - name: pnpm 설치 + uses: pnpm/action-setup@v2 + with: + version: 8 + - name: 환경 파일 생성 run: | if [ "${{ github.ref_name }}" == "main" ]; then @@ -38,12 +43,11 @@ jobs: - name: 환경 파일 권한 설정 run: chmod 644 ${{ env.frontend-directory }}/.env.* - - name: npm install - run: npm install - working-directory: ${{ env.frontend-directory }} + - name: 의존성 설치 + run: pnpm install - - name: npm run build - run: npm run build + - name: 빌드 실행 + run: pnpm run build working-directory: ${{ env.frontend-directory }} - name: AWS credentials 설정 From cc89a1a0eb33fb876c9547f735668f28a9617bc6 Mon Sep 17 00:00:00 2001 From: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:44:34 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore:=20frontend=20CI=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20path=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/frontend_ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index d15ac86e8..98195c7da 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -27,7 +27,6 @@ jobs: - name: 의존성 설치 run: pnpm install - working-directory: ${{ env.frontend-directory }} - name: 타입 체크 실행 run: pnpm run tsc