Merge pull request 'feat: 화면 관리 기능 개선 (복제/삭제/그룹 관리)' (#359) from feature/v2-renewal into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/359
This commit is contained in:
commit
9dc549be09
63
PLAN.MD
63
PLAN.MD
|
|
@ -1,4 +1,65 @@
|
|||
# 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
|
||||
# 프로젝트: 화면 관리 기능 개선 (복제/삭제/그룹 관리)
|
||||
|
||||
## 개요
|
||||
화면 관리 시스템의 복제 및 삭제 기능을 전면 개선하여, 단일 화면 복제, 그룹(폴더) 전체 복제, 정렬 순서 유지, 일괄 이름 변경 등 다양한 고급 기능을 지원합니다.
|
||||
|
||||
## 핵심 기능
|
||||
|
||||
### 1. 단일 화면 복제
|
||||
- [x] 우클릭 컨텍스트 메뉴에서 "복제" 선택
|
||||
- [x] 화면명, 화면 코드 자동 생성 (중복 시 `_COPY` 접미사 추가)
|
||||
- [x] 연결된 모달 화면 함께 복제
|
||||
- [x] 대상 그룹 선택 가능
|
||||
- [x] 복제 후 목록 자동 새로고침
|
||||
|
||||
### 2. 그룹(폴더) 전체 복제
|
||||
- [x] 대분류 폴더 복제 시 모든 하위 폴더 + 화면 재귀적 복제
|
||||
- [x] 정렬 순서(display_order) 유지
|
||||
- 그룹 생성 시 원본 display_order 전달
|
||||
- 화면 추가 시 원본 display_order 유지
|
||||
- 하위 그룹들 display_order 순으로 정렬 후 복제
|
||||
- [x] 대분류(최상위 그룹) 복제 시 경고 문구 표시
|
||||
- [x] 정렬 순서 입력 필드 추가 (사용자가 직접 수정 가능)
|
||||
- [x] 원본 그룹 정보 표시 개선
|
||||
- 직접 포함 화면 수
|
||||
- 하위 그룹 수
|
||||
- 복제될 총 화면 수 (하위 그룹 포함)
|
||||
|
||||
### 3. 고급 옵션: 이름 일괄 변경
|
||||
- [x] 삭제할 텍스트 지정 (모든 폴더/화면 이름에서 제거)
|
||||
- [x] 추가할 접미사 지정 (기본값: " (복제)")
|
||||
- [x] 미리보기 기능
|
||||
|
||||
### 4. 삭제 기능
|
||||
- [x] 단일 화면 삭제 (휴지통으로 이동)
|
||||
- [x] 그룹 삭제 시 옵션 선택
|
||||
- "화면도 함께 삭제" 체크박스
|
||||
- 체크 시: 그룹 + 포함된 화면 모두 삭제
|
||||
- 미체크 시: 화면은 "미분류"로 이동
|
||||
|
||||
### 5. 회사 코드 지원 (최고 관리자)
|
||||
- [x] 대상 회사 선택 가능
|
||||
- [x] 복제된 그룹/화면에 선택한 회사 코드 적용
|
||||
|
||||
## 관련 파일
|
||||
- `frontend/components/screen/CopyScreenModal.tsx` - 복제 모달 (화면/그룹 통합)
|
||||
- `frontend/components/screen/ScreenGroupTreeView.tsx` - 트리 뷰 + 컨텍스트 메뉴
|
||||
- `frontend/lib/api/screen.ts` - 화면 API (복제, 삭제)
|
||||
- `frontend/lib/api/screenGroup.ts` - 그룹 API
|
||||
|
||||
## 진행 상태
|
||||
- [완료] 단일 화면 복제 + 새로고침
|
||||
- [완료] 그룹 전체 복제 (재귀적)
|
||||
- [완료] 정렬 순서(display_order) 유지
|
||||
- [완료] 대분류 경고 문구
|
||||
- [완료] 정렬 순서 입력 필드
|
||||
- [완료] 고급 옵션: 이름 일괄 변경
|
||||
- [완료] 단일 화면 삭제
|
||||
- [완료] 그룹 삭제 (화면 함께 삭제 옵션)
|
||||
|
||||
---
|
||||
|
||||
# 이전 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
|
||||
|
||||
## 개요
|
||||
현재 GET 방식 위주로 구현된 외부 REST API 커넥션 관리 기능을 확장하여, POST, PUT, DELETE 등 다양한 HTTP 메서드와 JSON Request Body를 설정하고 테스트할 수 있도록 개선합니다. 이를 통해 토큰 발급 API나 데이터 전송 API 등 다양한 외부 시스템과의 연동을 지원합니다.
|
||||
|
|
|
|||
|
|
@ -2597,10 +2597,10 @@ export class ScreenManagementService {
|
|||
// 없으면 원본과 같은 회사에 복사
|
||||
const targetCompanyCode = copyData.targetCompanyCode || sourceScreen.company_code;
|
||||
|
||||
// 3. 화면 코드 중복 체크 (대상 회사 기준)
|
||||
// 3. 화면 코드 중복 체크 (대상 회사 기준, 삭제되지 않은 화면만)
|
||||
const existingScreens = await client.query<any>(
|
||||
`SELECT screen_id FROM screen_definitions
|
||||
WHERE screen_code = $1 AND company_code = $2
|
||||
WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
|
||||
LIMIT 1`,
|
||||
[copyData.screenCode, targetCompanyCode]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,19 @@ export default function ScreenManagementPage() {
|
|||
loadScreens();
|
||||
}, [loadScreens]);
|
||||
|
||||
// 화면 목록 새로고침 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleScreenListRefresh = () => {
|
||||
console.log("🔄 화면 목록 새로고침 이벤트 수신");
|
||||
loadScreens();
|
||||
};
|
||||
|
||||
window.addEventListener("screen-list-refresh", handleScreenListRefresh);
|
||||
return () => {
|
||||
window.removeEventListener("screen-list-refresh", handleScreenListRefresh);
|
||||
};
|
||||
}, [loadScreens]);
|
||||
|
||||
// URL 쿼리 파라미터로 화면 디자이너 자동 열기
|
||||
useEffect(() => {
|
||||
const openDesignerId = searchParams.get("openDesigner");
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -315,7 +315,11 @@ export function ScreenGroupModal({
|
|||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => {
|
||||
setFormData({ ...formData, parent_group_id: null });
|
||||
setFormData({
|
||||
...formData,
|
||||
parent_group_id: null,
|
||||
// 대분류 선택 시 현재 회사 코드 유지
|
||||
});
|
||||
setIsParentGroupSelectOpen(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
|
|
@ -335,7 +339,13 @@ export function ScreenGroupModal({
|
|||
key={parentGroup.id}
|
||||
value={`${parentGroup.group_name} ${getGroupPath(parentGroup.id)}`}
|
||||
onSelect={() => {
|
||||
setFormData({ ...formData, parent_group_id: parentGroup.id });
|
||||
// 상위 그룹의 company_code로 자동 설정
|
||||
const parentCompanyCode = parentGroup.company_code || formData.target_company_code;
|
||||
setFormData({
|
||||
...formData,
|
||||
parent_group_id: parentGroup.id,
|
||||
target_company_code: parentCompanyCode,
|
||||
});
|
||||
setIsParentGroupSelectOpen(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import {
|
|||
Edit,
|
||||
Trash2,
|
||||
FolderInput,
|
||||
Copy,
|
||||
FolderTree,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import {
|
||||
|
|
@ -73,7 +76,9 @@ import {
|
|||
} from "@/components/ui/popover";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { ScreenGroupModal } from "./ScreenGroupModal";
|
||||
import CopyScreenModal from "./CopyScreenModal";
|
||||
import { toast } from "sonner";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
|
||||
interface ScreenGroupTreeViewProps {
|
||||
screens: ScreenDefinition[];
|
||||
|
|
@ -115,15 +120,41 @@ export function ScreenGroupTreeView({
|
|||
// 삭제 확인 다이얼로그 상태
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [deletingGroup, setDeletingGroup] = useState<ScreenGroup | null>(null);
|
||||
const [deleteScreensWithGroup, setDeleteScreensWithGroup] = useState(false); // 화면도 함께 삭제 체크박스
|
||||
const [isDeleting, setIsDeleting] = useState(false); // 삭제 진행 중 상태
|
||||
const [deleteProgress, setDeleteProgress] = useState({ current: 0, total: 0, message: "" }); // 삭제 진행 상태
|
||||
|
||||
// 화면 이동 메뉴 상태
|
||||
const [movingScreen, setMovingScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [isMoveMenuOpen, setIsMoveMenuOpen] = useState(false);
|
||||
// 단일 화면 삭제 상태
|
||||
const [isScreenDeleteDialogOpen, setIsScreenDeleteDialogOpen] = useState(false);
|
||||
const [deletingScreen, setDeletingScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [isScreenDeleting, setIsScreenDeleting] = useState(false); // 화면 삭제 진행 중
|
||||
|
||||
// 화면 수정 모달 상태 (이름 변경 + 그룹 이동 통합)
|
||||
const [editingScreen, setEditingScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [isEditScreenModalOpen, setIsEditScreenModalOpen] = useState(false);
|
||||
const [editScreenName, setEditScreenName] = useState<string>("");
|
||||
const [selectedGroupForMove, setSelectedGroupForMove] = useState<number | null>(null);
|
||||
const [screenRole, setScreenRole] = useState<string>("");
|
||||
const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false);
|
||||
const [displayOrder, setDisplayOrder] = useState<number>(1);
|
||||
|
||||
// 화면 복제 모달 상태 (CopyScreenModal 사용)
|
||||
const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
|
||||
const [copyingScreen, setCopyingScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [copyTargetGroupId, setCopyTargetGroupId] = useState<number | null>(null);
|
||||
const [copyMode, setCopyMode] = useState<"screen" | "group">("screen");
|
||||
|
||||
// 그룹 복제 모달 상태 (CopyScreenModal 그룹 모드 사용)
|
||||
const [copyingGroup, setCopyingGroup] = useState<ScreenGroup | null>(null);
|
||||
|
||||
// 컨텍스트 메뉴 상태 (화면용)
|
||||
const [contextMenuScreen, setContextMenuScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
// 그룹 컨텍스트 메뉴 상태
|
||||
const [contextMenuGroup, setContextMenuGroup] = useState<ScreenGroup | null>(null);
|
||||
const [contextMenuGroupPosition, setContextMenuGroupPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
// 그룹 목록 및 그룹별 화면 로드
|
||||
useEffect(() => {
|
||||
loadGroupsData();
|
||||
|
|
@ -219,21 +250,110 @@ export function ScreenGroupTreeView({
|
|||
};
|
||||
|
||||
// 그룹 삭제 버튼 클릭
|
||||
const handleDeleteGroup = (group: ScreenGroup, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const handleDeleteGroup = (group: ScreenGroup, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
setDeletingGroup(group);
|
||||
setDeleteScreensWithGroup(false); // 기본값: 화면 삭제 안함
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 그룹과 모든 하위 그룹의 화면을 재귀적으로 수집
|
||||
const getAllScreensInGroupRecursively = (groupId: number): ScreenDefinition[] => {
|
||||
const result: ScreenDefinition[] = [];
|
||||
|
||||
// 현재 그룹의 화면들
|
||||
const currentGroupScreens = getScreensInGroup(groupId);
|
||||
result.push(...currentGroupScreens);
|
||||
|
||||
// 하위 그룹들 찾기
|
||||
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
|
||||
for (const childGroup of childGroups) {
|
||||
const childScreens = getAllScreensInGroupRecursively(childGroup.id);
|
||||
result.push(...childScreens);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 모든 하위 그룹 ID를 재귀적으로 수집 (삭제 순서: 자식 → 부모)
|
||||
const getAllChildGroupIds = (groupId: number): number[] => {
|
||||
const result: number[] = [];
|
||||
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
|
||||
|
||||
for (const childGroup of childGroups) {
|
||||
// 자식의 자식들을 먼저 수집 (깊은 곳부터)
|
||||
const grandChildIds = getAllChildGroupIds(childGroup.id);
|
||||
result.push(...grandChildIds);
|
||||
result.push(childGroup.id);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 그룹 삭제 확인
|
||||
const confirmDeleteGroup = async () => {
|
||||
if (!deletingGroup) return;
|
||||
|
||||
// 삭제 전 통계 수집 (화면 수는 삭제 전에 계산)
|
||||
const totalScreensToDelete = getAllScreensInGroupRecursively(deletingGroup.id).length;
|
||||
const childGroupIds = getAllChildGroupIds(deletingGroup.id);
|
||||
|
||||
// 총 작업 수 계산 (화면 + 하위 그룹 + 현재 그룹)
|
||||
const totalSteps = totalScreensToDelete + childGroupIds.length + 1;
|
||||
let currentStep = 0;
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
setDeleteProgress({ current: 0, total: totalSteps, message: "삭제 준비 중..." });
|
||||
|
||||
// 화면도 함께 삭제하는 경우
|
||||
if (deleteScreensWithGroup) {
|
||||
// 현재 그룹 + 모든 하위 그룹의 화면을 재귀적으로 수집
|
||||
const allScreens = getAllScreensInGroupRecursively(deletingGroup.id);
|
||||
if (allScreens.length > 0) {
|
||||
const { screenApi } = await import("@/lib/api/screen");
|
||||
|
||||
// 화면을 하나씩 삭제하면서 진행률 업데이트
|
||||
for (let i = 0; i < allScreens.length; i++) {
|
||||
const screen = allScreens[i];
|
||||
currentStep++;
|
||||
setDeleteProgress({
|
||||
current: currentStep,
|
||||
total: totalSteps,
|
||||
message: `화면 삭제 중: ${screen.screenName}`
|
||||
});
|
||||
await screenApi.deleteScreen(screen.screenId, "그룹 삭제와 함께 삭제");
|
||||
}
|
||||
console.log(`✅ 그룹 및 하위 그룹 내 화면 ${allScreens.length}개 삭제 완료`);
|
||||
}
|
||||
}
|
||||
|
||||
// 하위 그룹들을 먼저 삭제 (자식 → 부모 순서)
|
||||
for (let i = 0; i < childGroupIds.length; i++) {
|
||||
const childId = childGroupIds[i];
|
||||
const childGroup = groups.find(g => g.id === childId);
|
||||
currentStep++;
|
||||
setDeleteProgress({
|
||||
current: currentStep,
|
||||
total: totalSteps,
|
||||
message: `하위 그룹 삭제 중: ${childGroup?.group_name || childId}`
|
||||
});
|
||||
await deleteScreenGroup(childId);
|
||||
console.log(`✅ 하위 그룹 ${childId} 삭제 완료`);
|
||||
}
|
||||
|
||||
// 최종적으로 대상 그룹 삭제
|
||||
currentStep++;
|
||||
setDeleteProgress({ current: currentStep, total: totalSteps, message: "그룹 삭제 완료 중..." });
|
||||
const response = await deleteScreenGroup(deletingGroup.id);
|
||||
if (response.success) {
|
||||
toast.success("그룹이 삭제되었습니다");
|
||||
loadGroupsData();
|
||||
toast.success(
|
||||
deleteScreensWithGroup
|
||||
? `그룹과 화면 ${totalScreensToDelete}개가 삭제되었습니다`
|
||||
: "그룹이 삭제되었습니다"
|
||||
);
|
||||
await loadGroupsData();
|
||||
window.dispatchEvent(new CustomEvent("screen-list-refresh"));
|
||||
} else {
|
||||
toast.error(response.message || "그룹 삭제에 실패했습니다");
|
||||
}
|
||||
|
|
@ -241,16 +361,45 @@ export function ScreenGroupTreeView({
|
|||
console.error("그룹 삭제 실패:", error);
|
||||
toast.error("그룹 삭제에 실패했습니다");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setDeleteProgress({ current: 0, total: 0, message: "" });
|
||||
setIsDeleteDialogOpen(false);
|
||||
setDeletingGroup(null);
|
||||
setDeleteScreensWithGroup(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 화면 이동 메뉴 열기
|
||||
const handleMoveScreen = (screen: ScreenDefinition, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setMovingScreen(screen);
|
||||
// 단일 화면 삭제 버튼 클릭
|
||||
const handleDeleteScreen = (screen: ScreenDefinition) => {
|
||||
setDeletingScreen(screen);
|
||||
setIsScreenDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 단일 화면 삭제 확인
|
||||
const confirmDeleteScreen = async () => {
|
||||
if (!deletingScreen) return;
|
||||
|
||||
try {
|
||||
setIsScreenDeleting(true);
|
||||
const { screenApi } = await import("@/lib/api/screen");
|
||||
await screenApi.deleteScreen(deletingScreen.screenId, "사용자 요청으로 삭제");
|
||||
toast.success(`"${deletingScreen.screenName}" 화면이 삭제되었습니다`);
|
||||
await loadGroupsData();
|
||||
window.dispatchEvent(new CustomEvent("screen-list-refresh"));
|
||||
} catch (error) {
|
||||
console.error("화면 삭제 실패:", error);
|
||||
toast.error("화면 삭제에 실패했습니다");
|
||||
} finally {
|
||||
setIsScreenDeleting(false);
|
||||
setIsScreenDeleteDialogOpen(false);
|
||||
setDeletingScreen(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 화면 수정 모달 열기 (이름 변경 + 그룹 이동)
|
||||
const handleOpenEditScreenModal = (screen: ScreenDefinition) => {
|
||||
setEditingScreen(screen);
|
||||
setEditScreenName(screen.screenName);
|
||||
|
||||
// 현재 화면이 속한 그룹 정보 찾기
|
||||
let currentGroupId: number | null = null;
|
||||
|
|
@ -273,17 +422,92 @@ export function ScreenGroupTreeView({
|
|||
setSelectedGroupForMove(currentGroupId);
|
||||
setScreenRole(currentScreenRole);
|
||||
setDisplayOrder(currentDisplayOrder);
|
||||
setIsMoveMenuOpen(true);
|
||||
setIsEditScreenModalOpen(true);
|
||||
};
|
||||
|
||||
// 화면을 특정 그룹으로 이동
|
||||
const moveScreenToGroup = async (targetGroupId: number | null) => {
|
||||
if (!movingScreen) return;
|
||||
// 화면 복제 모달 열기 (CopyScreenModal 사용)
|
||||
const handleOpenCopyModal = (screen: ScreenDefinition) => {
|
||||
// 현재 화면이 속한 그룹 찾기 (기본값으로 설정)
|
||||
let currentGroupId: number | null = null;
|
||||
for (const group of groups) {
|
||||
if (group.screens && Array.isArray(group.screens)) {
|
||||
const found = group.screens.find((s: any) => Number(s.screen_id) === Number(screen.screenId));
|
||||
if (found) {
|
||||
currentGroupId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCopyingScreen(screen);
|
||||
setCopyTargetGroupId(currentGroupId);
|
||||
setCopyMode("screen");
|
||||
setIsCopyModalOpen(true);
|
||||
setContextMenuPosition(null); // 컨텍스트 메뉴 닫기
|
||||
};
|
||||
|
||||
// 그룹 복제 모달 열기 (CopyScreenModal 그룹 모드 사용)
|
||||
const handleOpenGroupCopyModal = (group: ScreenGroup) => {
|
||||
setCopyingGroup(group);
|
||||
setCopyMode("group");
|
||||
setIsCopyModalOpen(true);
|
||||
closeGroupContextMenu(); // 그룹 컨텍스트 메뉴 닫기
|
||||
};
|
||||
|
||||
// 복제 성공 콜백
|
||||
const handleCopySuccess = async () => {
|
||||
console.log("🔄 복제 성공 - 새로고침 시작");
|
||||
// 그룹 목록 새로고침
|
||||
await loadGroupsData();
|
||||
console.log("✅ 그룹 목록 새로고침 완료");
|
||||
// 화면 목록 새로고침
|
||||
window.dispatchEvent(new CustomEvent("screen-list-refresh"));
|
||||
console.log("✅ 화면 목록 새로고침 이벤트 발송 완료");
|
||||
};
|
||||
|
||||
// 컨텍스트 메뉴 열기
|
||||
const handleContextMenu = (e: React.MouseEvent, screen: ScreenDefinition) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenuScreen(screen);
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
// 컨텍스트 메뉴 닫기
|
||||
const closeContextMenu = () => {
|
||||
setContextMenuPosition(null);
|
||||
setContextMenuScreen(null);
|
||||
};
|
||||
|
||||
// 그룹 컨텍스트 메뉴 열기
|
||||
const handleGroupContextMenu = (e: React.MouseEvent, group: ScreenGroup) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenuGroup(group);
|
||||
setContextMenuGroupPosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
// 그룹 컨텍스트 메뉴 닫기
|
||||
const closeGroupContextMenu = () => {
|
||||
setContextMenuGroupPosition(null);
|
||||
setContextMenuGroup(null);
|
||||
};
|
||||
|
||||
// 화면 수정 저장 (이름 변경 + 그룹 이동)
|
||||
const saveScreenEdit = async () => {
|
||||
if (!editingScreen) return;
|
||||
|
||||
try {
|
||||
// 현재 그룹에서 제거
|
||||
// 1. 화면 이름이 변경되었으면 업데이트
|
||||
if (editScreenName.trim() && editScreenName !== editingScreen.screenName) {
|
||||
await screenApi.updateScreen(editingScreen.screenId, {
|
||||
screenName: editScreenName.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 현재 그룹에서 제거
|
||||
const currentGroupId = Array.from(groupScreensMap.entries()).find(([_, screenIds]) =>
|
||||
screenIds.includes(movingScreen.screenId)
|
||||
screenIds.includes(editingScreen.screenId)
|
||||
)?.[0];
|
||||
|
||||
if (currentGroupId) {
|
||||
|
|
@ -291,7 +515,7 @@ export function ScreenGroupTreeView({
|
|||
const currentGroup = groups.find((g) => g.id === currentGroupId);
|
||||
if (currentGroup && currentGroup.screens) {
|
||||
const screenGroupScreen = currentGroup.screens.find(
|
||||
(s: any) => s.screen_id === movingScreen.screenId
|
||||
(s: any) => s.screen_id === editingScreen.screenId
|
||||
);
|
||||
if (screenGroupScreen) {
|
||||
await removeScreenFromGroup(screenGroupScreen.id);
|
||||
|
|
@ -299,25 +523,27 @@ export function ScreenGroupTreeView({
|
|||
}
|
||||
}
|
||||
|
||||
// 새 그룹에 추가 (미분류가 아닌 경우)
|
||||
if (targetGroupId !== null) {
|
||||
// 3. 새 그룹에 추가 (미분류가 아닌 경우)
|
||||
if (selectedGroupForMove !== null) {
|
||||
await addScreenToGroup({
|
||||
group_id: targetGroupId,
|
||||
screen_id: movingScreen.screenId,
|
||||
group_id: selectedGroupForMove,
|
||||
screen_id: editingScreen.screenId,
|
||||
screen_role: screenRole,
|
||||
display_order: displayOrder,
|
||||
is_default: "N",
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("화면이 이동되었습니다");
|
||||
toast.success("화면이 수정되었습니다");
|
||||
loadGroupsData();
|
||||
window.dispatchEvent(new CustomEvent("screen-list-refresh"));
|
||||
} catch (error) {
|
||||
console.error("화면 이동 실패:", error);
|
||||
toast.error("화면 이동에 실패했습니다");
|
||||
console.error("화면 수정 실패:", error);
|
||||
toast.error("화면 수정에 실패했습니다");
|
||||
} finally {
|
||||
setIsMoveMenuOpen(false);
|
||||
setMovingScreen(null);
|
||||
setIsEditScreenModalOpen(false);
|
||||
setEditingScreen(null);
|
||||
setEditScreenName("");
|
||||
setSelectedGroupForMove(null);
|
||||
setScreenRole("");
|
||||
setDisplayOrder(1);
|
||||
|
|
@ -444,6 +670,7 @@ export function ScreenGroupTreeView({
|
|||
"text-sm font-medium group/item"
|
||||
)}
|
||||
onClick={() => toggleGroup(groupId)}
|
||||
onContextMenu={(e) => handleGroupContextMenu(e, group)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -507,6 +734,7 @@ export function ScreenGroupTreeView({
|
|||
"text-xs font-medium group/item"
|
||||
)}
|
||||
onClick={() => toggleGroup(childGroupId)}
|
||||
onContextMenu={(e) => handleGroupContextMenu(e, childGroup)}
|
||||
>
|
||||
{isChildExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -566,6 +794,7 @@ export function ScreenGroupTreeView({
|
|||
"text-xs group/item"
|
||||
)}
|
||||
onClick={() => toggleGroup(grandChildId)}
|
||||
onContextMenu={(e) => handleGroupContextMenu(e, grandChild)}
|
||||
>
|
||||
{isGrandExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -626,7 +855,7 @@ export function ScreenGroupTreeView({
|
|||
)}
|
||||
onClick={() => handleScreenClickInGroup(screen, grandChild)}
|
||||
onDoubleClick={() => handleScreenDoubleClick(screen)}
|
||||
onContextMenu={(e) => handleMoveScreen(screen, e)}
|
||||
onContextMenu={(e) => handleContextMenu(e, screen)}
|
||||
>
|
||||
<Monitor className="h-3 w-3 shrink-0 text-blue-500" />
|
||||
<span className="truncate flex-1">{screen.screenName}</span>
|
||||
|
|
@ -662,7 +891,7 @@ export function ScreenGroupTreeView({
|
|||
)}
|
||||
onClick={() => handleScreenClickInGroup(screen, childGroup)}
|
||||
onDoubleClick={() => handleScreenDoubleClick(screen)}
|
||||
onContextMenu={(e) => handleMoveScreen(screen, e)}
|
||||
onContextMenu={(e) => handleContextMenu(e, screen)}
|
||||
>
|
||||
<Monitor className="h-3 w-3 shrink-0 text-blue-500" />
|
||||
<span className="truncate flex-1">{screen.screenName}</span>
|
||||
|
|
@ -698,7 +927,7 @@ export function ScreenGroupTreeView({
|
|||
)}
|
||||
onClick={() => handleScreenClickInGroup(screen, group)}
|
||||
onDoubleClick={() => handleScreenDoubleClick(screen)}
|
||||
onContextMenu={(e) => handleMoveScreen(screen, e)}
|
||||
onContextMenu={(e) => handleContextMenu(e, screen)}
|
||||
>
|
||||
<Monitor className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<span className="truncate flex-1">{screen.screenName}</span>
|
||||
|
|
@ -748,7 +977,7 @@ export function ScreenGroupTreeView({
|
|||
)}
|
||||
onClick={() => handleScreenClick(screen)}
|
||||
onDoubleClick={() => handleScreenDoubleClick(screen)}
|
||||
onContextMenu={(e) => handleMoveScreen(screen, e)}
|
||||
onContextMenu={(e) => handleContextMenu(e, screen)}
|
||||
>
|
||||
<Monitor className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<span className="truncate flex-1">{screen.screenName}</span>
|
||||
|
|
@ -781,6 +1010,23 @@ export function ScreenGroupTreeView({
|
|||
group={editingGroup}
|
||||
/>
|
||||
|
||||
{/* 화면/그룹 복제 모달 (CopyScreenModal 사용) */}
|
||||
<CopyScreenModal
|
||||
isOpen={isCopyModalOpen}
|
||||
onClose={() => {
|
||||
setIsCopyModalOpen(false);
|
||||
setCopyingScreen(null);
|
||||
setCopyingGroup(null);
|
||||
}}
|
||||
sourceScreen={copyMode === "screen" ? copyingScreen : null}
|
||||
onCopySuccess={handleCopySuccess}
|
||||
mode={copyMode}
|
||||
sourceGroup={copyMode === "group" ? copyingGroup : null}
|
||||
groups={groups}
|
||||
targetGroupId={copyTargetGroupId}
|
||||
allScreens={screens}
|
||||
/>
|
||||
|
||||
{/* 그룹 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
|
|
@ -789,34 +1035,167 @@ export function ScreenGroupTreeView({
|
|||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingGroup?.group_name}" 그룹을 삭제하시겠습니까?
|
||||
<br />
|
||||
그룹에 속한 화면들은 미분류로 이동됩니다.
|
||||
{deleteScreensWithGroup
|
||||
? <span className="text-destructive font-medium">그룹에 속한 화면들도 함께 삭제됩니다.</span>
|
||||
: "그룹에 속한 화면들은 미분류로 이동됩니다."
|
||||
}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{/* 그룹 정보 표시 */}
|
||||
{deletingGroup && (
|
||||
<div className="rounded-md border bg-muted/50 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">하위 그룹 수:</span>
|
||||
<span className="font-medium">{getAllChildGroupIds(deletingGroup.id).length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">총 화면 수 (하위 포함):</span>
|
||||
<span className="font-medium">{getAllScreensInGroupRecursively(deletingGroup.id).length}개</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 화면도 함께 삭제 체크박스 */}
|
||||
{deletingGroup && getAllScreensInGroupRecursively(deletingGroup.id).length > 0 && (
|
||||
<div className="flex items-center space-x-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="deleteScreensWithGroup"
|
||||
checked={deleteScreensWithGroup}
|
||||
onChange={(e) => setDeleteScreensWithGroup(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-destructive focus:ring-destructive"
|
||||
/>
|
||||
<label
|
||||
htmlFor="deleteScreensWithGroup"
|
||||
className="text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
화면도 함께 삭제 ({getAllScreensInGroupRecursively(deletingGroup.id).length}개)
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 로딩 오버레이 */}
|
||||
{isDeleting && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center rounded-lg bg-background/90 backdrop-blur-sm">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-destructive" />
|
||||
<p className="mt-4 text-sm font-medium">{deleteProgress.message}</p>
|
||||
{deleteProgress.total > 0 && (
|
||||
<>
|
||||
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full bg-destructive transition-all duration-300"
|
||||
style={{ width: `${Math.round((deleteProgress.current / deleteProgress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{deleteProgress.current} / {deleteProgress.total}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||
<AlertDialogCancel
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
취소
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteGroup}
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // 자동 닫힘 방지
|
||||
confirmDeleteGroup();
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
"삭제"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 화면 이동 메뉴 (다이얼로그) */}
|
||||
<Dialog open={isMoveMenuOpen} onOpenChange={setIsMoveMenuOpen}>
|
||||
{/* 단일 화면 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isScreenDeleteDialogOpen} onOpenChange={setIsScreenDeleteDialogOpen}>
|
||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
{/* 로딩 오버레이 */}
|
||||
{isScreenDeleting && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center rounded-lg bg-background/90 backdrop-blur-sm">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-destructive" />
|
||||
<p className="mt-3 text-sm font-medium">화면 삭제 중...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">화면 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
"{deletingScreen?.screenName}" 화면을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 화면은 휴지통으로 이동됩니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||
<AlertDialogCancel
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
disabled={isScreenDeleting}
|
||||
>
|
||||
취소
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // 자동 닫힘 방지
|
||||
confirmDeleteScreen();
|
||||
}}
|
||||
disabled={isScreenDeleting}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{isScreenDeleting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
"삭제"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 화면 수정 모달 (이름 변경 + 그룹 이동) */}
|
||||
<Dialog open={isEditScreenModalOpen} onOpenChange={setIsEditScreenModalOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">화면 그룹 설정</DialogTitle>
|
||||
<DialogTitle className="text-base sm:text-lg">화면 수정</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
"{movingScreen?.screenName}"의 그룹과 역할을 설정하세요
|
||||
화면 정보를 수정하세요
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* 화면 이름 */}
|
||||
<div>
|
||||
<Label htmlFor="edit-screen-name" className="text-xs sm:text-sm">
|
||||
화면 이름 *
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-screen-name"
|
||||
value={editScreenName}
|
||||
onChange={(e) => setEditScreenName(e.target.value)}
|
||||
placeholder="화면 이름을 입력하세요"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 그룹 선택 (트리 구조 + 검색) */}
|
||||
<div>
|
||||
<Label htmlFor="target-group" className="text-xs sm:text-sm">
|
||||
|
|
@ -949,8 +1328,9 @@ export function ScreenGroupTreeView({
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsMoveMenuOpen(false);
|
||||
setMovingScreen(null);
|
||||
setIsEditScreenModalOpen(false);
|
||||
setEditingScreen(null);
|
||||
setEditScreenName("");
|
||||
setSelectedGroupForMove(null);
|
||||
setScreenRole("");
|
||||
setDisplayOrder(1);
|
||||
|
|
@ -960,14 +1340,125 @@ export function ScreenGroupTreeView({
|
|||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => moveScreenToGroup(selectedGroupForMove)}
|
||||
onClick={saveScreenEdit}
|
||||
disabled={!editScreenName.trim()}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
이동
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 커스텀 컨텍스트 메뉴 */}
|
||||
{contextMenuPosition && contextMenuScreen && (
|
||||
<>
|
||||
{/* 백드롭 - 클릭 시 메뉴 닫기 */}
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={closeContextMenu}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
closeContextMenu();
|
||||
}}
|
||||
/>
|
||||
{/* 컨텍스트 메뉴 */}
|
||||
<div
|
||||
className="fixed z-50 min-w-[150px] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
style={{
|
||||
left: contextMenuPosition.x,
|
||||
top: contextMenuPosition.y,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
|
||||
onClick={() => {
|
||||
handleOpenCopyModal(contextMenuScreen);
|
||||
}}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
복제
|
||||
</div>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
|
||||
onClick={() => {
|
||||
handleOpenEditScreenModal(contextMenuScreen);
|
||||
closeContextMenu();
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
수정
|
||||
</div>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-destructive hover:bg-accent"
|
||||
onClick={() => {
|
||||
handleDeleteScreen(contextMenuScreen);
|
||||
closeContextMenu();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
삭제
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 그룹 컨텍스트 메뉴 */}
|
||||
{contextMenuGroupPosition && contextMenuGroup && (
|
||||
<>
|
||||
{/* 백드롭 - 클릭 시 메뉴 닫기 */}
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={closeGroupContextMenu}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
closeGroupContextMenu();
|
||||
}}
|
||||
/>
|
||||
{/* 컨텍스트 메뉴 */}
|
||||
<div
|
||||
className="fixed z-50 min-w-[150px] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
style={{
|
||||
left: contextMenuGroupPosition.x,
|
||||
top: contextMenuGroupPosition.y,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
|
||||
onClick={() => handleOpenGroupCopyModal(contextMenuGroup)}
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
그룹 복제
|
||||
</div>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
|
||||
onClick={() => {
|
||||
setEditingGroup(contextMenuGroup);
|
||||
setIsGroupModalOpen(true);
|
||||
closeGroupContextMenu();
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
수정
|
||||
</div>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<div
|
||||
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm text-destructive hover:bg-accent"
|
||||
onClick={() => {
|
||||
handleDeleteGroup(contextMenuGroup);
|
||||
closeGroupContextMenu();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
그룹 삭제
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -105,6 +105,18 @@ export const screenApi = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
// 화면 수정 (이름, 설명 등)
|
||||
updateScreen: async (
|
||||
screenId: number,
|
||||
data: {
|
||||
screenName?: string;
|
||||
description?: string;
|
||||
tableName?: string;
|
||||
}
|
||||
): Promise<void> => {
|
||||
await apiClient.put(`/screen-management/screens/${screenId}`, data);
|
||||
},
|
||||
|
||||
// 화면 삭제 (휴지통으로 이동)
|
||||
deleteScreen: async (screenId: number, deleteReason?: string, force?: boolean): Promise<void> => {
|
||||
await apiClient.delete(`/screen-management/screens/${screenId}`, {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
|
|
@ -1715,6 +1716,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu": {
|
||||
"version": "2.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
|
||||
"integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-menu": "2.1.16",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
|
|
|
|||
Loading…
Reference in New Issue