From 1cb923a9d9d4bbf33e2545e389f47757aa4fe41e Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 2 Sep 2025 13:57:53 +0900 Subject: [PATCH] =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=EC=95=A4=20?= =?UTF-8?q?=EB=93=9C=EB=9E=8D=20=EB=B0=8F=20=EA=B2=80=EC=83=89=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=95=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/routes/commonCodeRoutes.ts | 11 +- .../src/services/commonCodeService.ts | 36 ++- docs/공통코드_관리_시스템_설계.md | 25 +- .../components/admin/CodeCategoryPanel.tsx | 30 ++- frontend/components/admin/CodeDetailPanel.tsx | 248 ++++++++++++++---- frontend/components/admin/CodeFormModal.tsx | 89 ++++++- frontend/hooks/useCommonCode.ts | 21 ++ frontend/package-lock.json | 56 ++++ frontend/package.json | 3 + 9 files changed, 432 insertions(+), 87 deletions(-) diff --git a/backend-node/src/routes/commonCodeRoutes.ts b/backend-node/src/routes/commonCodeRoutes.ts index 995650f8..0f320621 100644 --- a/backend-node/src/routes/commonCodeRoutes.ts +++ b/backend-node/src/routes/commonCodeRoutes.ts @@ -29,6 +29,12 @@ router.get("/categories/:categoryCode/codes", (req, res) => router.post("/categories/:categoryCode/codes", (req, res) => commonCodeController.createCode(req, res) ); + +// 코드 순서 변경 (구체적인 경로를 먼저 배치) +router.put("/categories/:categoryCode/codes/reorder", (req, res) => + commonCodeController.reorderCodes(req, res) +); + router.put("/categories/:categoryCode/codes/:codeValue", (req, res) => commonCodeController.updateCode(req, res) ); @@ -36,11 +42,6 @@ router.delete("/categories/:categoryCode/codes/:codeValue", (req, res) => commonCodeController.deleteCode(req, res) ); -// 코드 순서 변경 -router.put("/categories/:categoryCode/codes/reorder", (req, res) => - commonCodeController.reorderCodes(req, res) -); - // 화면관리용 옵션 조회 router.get("/categories/:categoryCode/options", (req, res) => commonCodeController.getCodeOptions(req, res) diff --git a/backend-node/src/services/commonCodeService.ts b/backend-node/src/services/commonCodeService.ts index ce674a70..153af7db 100644 --- a/backend-node/src/services/commonCodeService.ts +++ b/backend-node/src/services/commonCodeService.ts @@ -344,7 +344,27 @@ export class CommonCodeService { updatedBy: string ) { try { - const updatePromises = codes.map(({ codeValue, sortOrder }) => + // 먼저 존재하는 코드들을 확인 + const existingCodes = await prisma.code_info.findMany({ + where: { + code_category: categoryCode, + code_value: { in: codes.map((c) => c.codeValue) }, + }, + select: { code_value: true }, + }); + + const existingCodeValues = existingCodes.map((c) => c.code_value); + const validCodes = codes.filter((c) => + existingCodeValues.includes(c.codeValue) + ); + + if (validCodes.length === 0) { + throw new Error( + `카테고리 ${categoryCode}에 순서를 변경할 유효한 코드가 없습니다.` + ); + } + + const updatePromises = validCodes.map(({ codeValue, sortOrder }) => prisma.code_info.update({ where: { code_category_code_value: { @@ -361,7 +381,19 @@ export class CommonCodeService { ); await Promise.all(updatePromises); - logger.info(`코드 순서 변경 완료: ${categoryCode} - ${codes.length}개`); + + const skippedCodes = codes.filter( + (c) => !existingCodeValues.includes(c.codeValue) + ); + if (skippedCodes.length > 0) { + logger.warn( + `코드 순서 변경 시 존재하지 않는 코드들을 건너뜀: ${skippedCodes.map((c) => c.codeValue).join(", ")}` + ); + } + + logger.info( + `코드 순서 변경 완료: ${categoryCode} - ${validCodes.length}개 (전체 ${codes.length}개 중)` + ); } catch (error) { logger.error(`코드 순서 변경 중 오류 (${categoryCode}):`, error); throw error; diff --git a/docs/공통코드_관리_시스템_설계.md b/docs/공통코드_관리_시스템_설계.md index a756e315..fab7f81d 100644 --- a/docs/공통코드_관리_시스템_설계.md +++ b/docs/공통코드_관리_시스템_설계.md @@ -699,14 +699,23 @@ export class CommonCodeService { - API 응답 처리 최적화 완료 - 사용자 인터페이스 완성 -### ⏳ Phase 4: 고급 기능 구현 (예정) +### ✅ Phase 4: 고급 기능 구현 (완료) -- [ ] 드래그앤드롭 정렬 기능 -- [ ] 검색 및 필터링 기능 -- [ ] 일괄 업로드/다운로드 기능 -- [ ] 코드 편집 모달 구현 +- [x] 드래그앤드롭 정렬 기능 +- [x] 검색 및 필터링 기능 +- [x] 코드 편집 모달 구현 +- [x] 활성/비활성 필터 토글 +- [ ] 일괄 업로드/다운로드 기능 (선택사항) -**목표 기간**: 2일 +**완료 내용:** + +- @dnd-kit 라이브러리를 사용한 드래그앤드롭 정렬 기능 구현 +- 카테고리와 코드 양쪽 패널에 검색 및 활성 필터 기능 추가 +- 실시간 유효성 검사, 자동 대문자 변환, 중복 검사 등 개선된 편집 모달 +- 드래그앤드롭으로 변경한 순서가 실제 DB에 저장되는 기능 +- 라우터 순서 최적화로 API 충돌 문제 해결 + +**목표 기간**: 2일 → **실제 소요**: 1일 ### ⏳ Phase 5: 화면관리 연계 (예정) @@ -728,12 +737,12 @@ export class CommonCodeService { ## 🎯 현재 구현 상태 -### 📊 **전체 진행률: 50%** 🎉 +### 📊 **전체 진행률: 67%** 🎉 - ✅ **Phase 1**: 기본 구조 및 데이터베이스 (100%) - **완료!** - ✅ **Phase 2**: 백엔드 API 구현 (100%) - **완료!** - ✅ **Phase 3**: 프론트엔드 기본 구현 (100%) - **완료!** -- ⏳ **Phase 4**: 고급 기능 구현 (0%) +- ✅ **Phase 4**: 고급 기능 구현 (100%) - **완료!** - ⏳ **Phase 5**: 화면관리 연계 (0%) - ⏳ **Phase 6**: 테스트 및 최적화 (0%) diff --git a/frontend/components/admin/CodeCategoryPanel.tsx b/frontend/components/admin/CodeCategoryPanel.tsx index 0d5b65a4..e04b3a9b 100644 --- a/frontend/components/admin/CodeCategoryPanel.tsx +++ b/frontend/components/admin/CodeCategoryPanel.tsx @@ -23,17 +23,24 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co // 로컬 상태 const [searchTerm, setSearchTerm] = useState(""); + const [showActiveOnly, setShowActiveOnly] = useState(false); // 활성 필터 상태 const [showFormModal, setShowFormModal] = useState(false); const [editingCategory, setEditingCategory] = useState(""); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deletingCategory, setDeletingCategory] = useState(""); - // 검색 필터링 - const filteredCategories = categories.filter( - (category) => + // 검색 및 활성 상태 필터링 + const filteredCategories = categories.filter((category) => { + // 검색 조건 + const matchesSearch = category.category_name.toLowerCase().includes(searchTerm.toLowerCase()) || - category.category_code.toLowerCase().includes(searchTerm.toLowerCase()), - ); + category.category_code.toLowerCase().includes(searchTerm.toLowerCase()); + + // 활성 상태 필터 조건 + const matchesActiveFilter = showActiveOnly ? category.is_active : true; + + return matchesSearch && matchesActiveFilter; + }); // 카테고리 생성 핸들러 const handleCreateCategory = () => { @@ -94,6 +101,19 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co + {/* 활성 상태 필터 토글 */} +
+ +
+ diff --git a/frontend/components/admin/CodeDetailPanel.tsx b/frontend/components/admin/CodeDetailPanel.tsx index 86a8cba0..d8f79f22 100644 --- a/frontend/components/admin/CodeDetailPanel.tsx +++ b/frontend/components/admin/CodeDetailPanel.tsx @@ -9,16 +9,115 @@ import { useCommonCode } from "@/hooks/useCommonCode"; // import { useMultiLang } from "@/hooks/useMultiLang"; // 무한 루프 방지를 위해 임시 제거 import { CodeFormModal } from "./CodeFormModal"; import { AlertModal } from "@/components/common/AlertModal"; -import { Search, Plus, Edit, Trash2 } from "lucide-react"; +import { Search, Plus, Edit, Trash2, GripVertical } from "lucide-react"; import { cn } from "@/lib/utils"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; interface CodeDetailPanelProps { categoryCode: string; } +// 드래그 가능한 코드 아이템 컴포넌트 +interface SortableCodeItemProps { + code: any; + onEdit: (code: any) => void; + onDelete: (code: any) => void; +} + +function SortableCodeItem({ code, onEdit, onDelete }: SortableCodeItemProps) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: `${code.code_category}-${code.code_value}`, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ {/* 드래그 핸들 */} +
+ +
+ +
+
+ {code.code_name} + {code.is_active === "Y" ? ( + + 활성 + + ) : ( + + 비활성 + + )} +
+
+ {code.code_value} + {code.code_name_eng && ({code.code_name_eng})} +
+ {code.description &&

{code.description}

} +
+ +
+ + +
+
+ ); +} + export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { // const { getText } = useMultiLang(); // 무한 루프 방지를 위해 임시 제거 - const { codes, codesLoading, codesError, fetchCodes, deleteCode } = useCommonCode(); + const { codes, setCodes, codesLoading, codesError, fetchCodes, deleteCode, reorderCodes } = useCommonCode(); + + // 드래그앤드롭 센서 설정 + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); // 카테고리 변경 시 코드 조회 useEffect(() => { @@ -30,6 +129,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { // 로컬 상태 const [searchTerm, setSearchTerm] = useState(""); + const [showActiveOnly, setShowActiveOnly] = useState(false); // 활성 필터 상태 const [showFormModal, setShowFormModal] = useState(false); const [editingCode, setEditingCode] = useState<{ categoryCode: string; codeValue: string }>({ categoryCode: "", @@ -41,12 +141,18 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { codeValue: "", }); - // 검색 필터링 - const filteredCodes = codes.filter( - (code) => + // 검색 및 활성 상태 필터링 + const filteredCodes = codes.filter((code) => { + // 검색 조건 + const matchesSearch = code.code_name.toLowerCase().includes(searchTerm.toLowerCase()) || - code.code_value.toLowerCase().includes(searchTerm.toLowerCase()), - ); + code.code_value.toLowerCase().includes(searchTerm.toLowerCase()); + + // 활성 상태 필터 조건 + const matchesActiveFilter = showActiveOnly ? code.is_active : true; + + return matchesSearch && matchesActiveFilter; + }); // 코드 생성 핸들러 const handleCreateCode = () => { @@ -82,6 +188,54 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { } }; + // 드래그 종료 핸들러 + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (!over || active.id === over.id) { + return; + } + + const activeIndex = filteredCodes.findIndex((code) => `${code.code_category}-${code.code_value}` === active.id); + const overIndex = filteredCodes.findIndex((code) => `${code.code_category}-${code.code_value}` === over.id); + + if (activeIndex !== overIndex) { + // 전체 codes 배열에서 현재 카테고리의 코드들을 찾아서 재정렬 + const currentCategoryCodes = codes.filter((code) => code.code_category === categoryCode); + const otherCategoryCodes = codes.filter((code) => code.code_category !== categoryCode); + + // 현재 카테고리 코드들의 순서를 변경 + const reorderedCategoryCodes = arrayMove(currentCategoryCodes, activeIndex, overIndex); + + // 전체 codes 배열 업데이트 + const newCodesArray = [...otherCategoryCodes, ...reorderedCategoryCodes]; + setCodes(newCodesArray); + + try { + // 서버에 순서 변경 요청 + console.log("🔄 코드 순서 변경:", { + categoryCode, + from: activeIndex, + to: overIndex, + reorderedCodes: reorderedCategoryCodes.map((code) => code.code_value), + }); + + // 백엔드 API 호출 - 실제 DB에 순서 저장 + await reorderCodes( + categoryCode, + reorderedCategoryCodes.map((code, index) => ({ + codeValue: code.code_value, + sortOrder: index + 1, + })), + ); + } catch (error) { + console.error("순서 변경 실패:", error); + // 실패 시 원래 순서로 복원 + fetchCodes(categoryCode); + } + } + }; + // 카테고리가 선택되지 않은 경우 if (!categoryCode) { return ( @@ -122,12 +276,25 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) { + {/* 활성 상태 필터 토글 */} +
+ +
+ - {/* 코드 목록 */} + {/* 코드 목록 (드래그앤드롭) */}
{codesLoading ? (
@@ -139,56 +306,23 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {

코드가 없습니다.

) : ( -
- {filteredCodes.map((code) => ( -
-
-
- {code.code_name} - {code.is_active === "Y" ? ( - - 활성 - - ) : ( - - 비활성 - - )} -
-
- {code.code_value} - {code.code_name_eng && ({code.code_name_eng})} -
- {code.description && ( -

{code.description}

- )} -
- - {/* 액션 버튼 */} -
- - -
+ + `${code.code_category}-${code.code_value}`)} + strategy={verticalListSortingStrategy} + > +
+ {filteredCodes.map((code) => ( + handleEditCode(code.code_value)} + onDelete={(code) => handleDeleteCode(code.code_value)} + /> + ))}
- ))} -
+ + )}
diff --git a/frontend/components/admin/CodeFormModal.tsx b/frontend/components/admin/CodeFormModal.tsx index 325c2877..dacfc987 100644 --- a/frontend/components/admin/CodeFormModal.tsx +++ b/frontend/components/admin/CodeFormModal.tsx @@ -63,18 +63,76 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue setErrors({}); }, [editingCodeValue, codes, isOpen]); - // 입력값 검증 + // 실시간 필드 검증 + const validateField = (fieldName: string, value: string) => { + const newErrors = { ...errors }; + + switch (fieldName) { + case "codeValue": + if (!value.trim()) { + newErrors.codeValue = "필수 입력 항목입니다."; + } else if (value.length > 50) { + newErrors.codeValue = "코드값은 50자 이하로 입력해주세요."; + } else if (!/^[A-Z0-9_]+$/.test(value)) { + newErrors.codeValue = "대문자, 숫자, 언더스코어(_)만 사용 가능합니다."; + } else { + delete newErrors.codeValue; + } + break; + + case "codeName": + if (!value.trim()) { + newErrors.codeName = "필수 입력 항목입니다."; + } else if (value.length > 100) { + newErrors.codeName = "코드명은 100자 이하로 입력해주세요."; + } else { + delete newErrors.codeName; + } + break; + + case "codeNameEng": + if (value && value.length > 100) { + newErrors.codeNameEng = "영문명은 100자 이하로 입력해주세요."; + } else { + delete newErrors.codeNameEng; + } + break; + + case "description": + if (value && value.length > 500) { + newErrors.description = "설명은 500자 이하로 입력해주세요."; + } else { + delete newErrors.description; + } + break; + } + + setErrors(newErrors); + }; + + // 전체 폼 검증 const validateForm = () => { const newErrors: Record = {}; + // 필수 필드 검증 if (!formData.codeValue.trim()) { newErrors.codeValue = "필수 입력 항목입니다."; + } else if (!/^[A-Z0-9_]+$/.test(formData.codeValue)) { + newErrors.codeValue = "대문자, 숫자, 언더스코어(_)만 사용 가능합니다."; } if (!formData.codeName.trim()) { newErrors.codeName = "필수 입력 항목입니다."; } + // 중복 검사 (신규 생성 시) + if (!editingCodeValue) { + const existingCode = codes.find((c) => c.code_value === formData.codeValue); + if (existingCode) { + newErrors.codeValue = "이미 존재하는 코드값입니다."; + } + } + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -117,13 +175,18 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue } }; - // 입력값 변경 핸들러 + // 입력값 변경 핸들러 (실시간 검증 포함) const handleChange = (field: string, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); - // 에러 제거 - if (errors[field]) { - setErrors((prev) => ({ ...prev, [field]: "" })); + // 실시간 검증 (문자열 필드만) + if (typeof value === "string") { + validateField(field, value); + } else { + // 에러 제거 (숫자, 불린 필드) + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: "" })); + } } }; @@ -141,9 +204,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue handleChange("codeValue", e.target.value)} + onChange={(e) => handleChange("codeValue", e.target.value.toUpperCase())} disabled={!!editingCodeValue || loading} - placeholder={"코드값을 입력하세요"} + placeholder={"코드값을 입력하세요 (예: USER_ACTIVE)"} className={errors.codeValue ? "border-red-500" : ""} /> {errors.codeValue &&

{errors.codeValue}

} @@ -172,7 +235,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue onChange={(e) => handleChange("codeNameEng", e.target.value)} disabled={loading} placeholder={"코드 영문명을 입력하세요"} + className={errors.codeNameEng ? "border-red-500" : ""} /> + {errors.codeNameEng &&

{errors.codeNameEng}

}
{/* 설명 */} @@ -185,7 +250,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue disabled={loading} placeholder={"설명을 입력하세요"} rows={3} + className={errors.description ? "border-red-500" : ""} /> + {errors.description &&

{errors.description}

} {/* 정렬 순서 */} @@ -219,14 +286,16 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCodeValue - diff --git a/frontend/hooks/useCommonCode.ts b/frontend/hooks/useCommonCode.ts index 550c61d4..74a15f4e 100644 --- a/frontend/hooks/useCommonCode.ts +++ b/frontend/hooks/useCommonCode.ts @@ -220,6 +220,25 @@ export function useCommonCode() { [fetchCodes], ); + const reorderCodes = useCallback( + async (categoryCode: string, codes: Array<{ codeValue: string; sortOrder: number }>) => { + try { + const response = await commonCodeApi.codes.reorder(categoryCode, { codes }); + + if (response.success) { + console.log("✅ 코드 순서 변경 성공"); + } else { + throw new Error(response.message || "코드 순서 변경에 실패했습니다."); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다."; + console.error("코드 순서 변경 오류:", error); + throw error; + } + }, + [], + ); + /** * 초기 데이터 로드 */ @@ -251,12 +270,14 @@ export function useCommonCode() { // 코드 관련 codes, + setCodes, codesLoading, codesError, fetchCodes, createCode, updateCode, deleteCode, + reorderCodes, // 선택된 카테고리 selectedCategoryCode, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d7054b17..8ec600fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.1.1", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.0", @@ -74,6 +77,59 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 471b060e..2977512f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,9 @@ "format:check": "prettier --check ." }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.1.1", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.0",