diff --git a/docs/리포트_관리_시스템_구현_진행상황.md b/docs/리포트_관리_시스템_구현_진행상황.md index 7590260f..df7c5d50 100644 --- a/docs/리포트_관리_시스템_구현_진행상황.md +++ b/docs/리포트_관리_시스템_구현_진행상황.md @@ -168,45 +168,71 @@ - `frontend/components/report/designer/ReportDesignerRightPanel.tsx` - `frontend/components/report/designer/CanvasComponent.tsx` +### 12. 레이아웃 도구 (완료!) + +- [x] **Grid Snap**: 10px 단위 그리드에 자동 정렬 +- [x] **정렬 가이드라인**: 드래그 시 빨간색 가이드라인 표시 +- [x] **복사/붙여넣기**: Ctrl+C/V로 컴포넌트 복사 (20px 오프셋) +- [x] **Undo/Redo**: 히스토리 관리 (Ctrl+Z / Ctrl+Shift+Z) +- [x] **컴포넌트 정렬**: 좌/우/상/하/가로중앙/세로중앙 정렬 +- [x] **컴포넌트 배치**: 가로/세로 균등 배치 (3개 이상) +- [x] **크기 조정**: 같은 너비/높이/크기로 조정 (2개 이상) +- [x] **화살표 키 이동**: 1px 이동, Shift+화살표 10px 이동 +- [x] **레이어 관리**: 맨 앞/뒤, 한 단계 앞/뒤 (Z-Index 조정) +- [x] **컴포넌트 잠금**: 편집/이동/삭제 방지, 🔒 표시 +- [x] **눈금자 표시**: 가로/세로 mm 단위 눈금자 +- [x] **컴포넌트 그룹화**: 여러 컴포넌트를 그룹으로 묶어 함께 이동, 👥 표시 + +**파일**: + +- `frontend/contexts/ReportDesignerContext.tsx` (레이아웃 도구 로직) +- `frontend/components/report/designer/ReportDesignerToolbar.tsx` (버튼 UI) +- `frontend/components/report/designer/ReportDesignerCanvas.tsx` (Grid, 가이드라인) +- `frontend/components/report/designer/CanvasComponent.tsx` (잠금, 그룹) +- `frontend/components/report/designer/Ruler.tsx` (눈금자 컴포넌트) + --- ## 진행 중인 작업 🚧 -없음 (현재 모든 핵심 기능 구현 완료) +없음 (모든 레이아웃 도구 구현 완료!) --- ## 남은 작업 (우선순위순) 📋 -### Phase 1: 사용성 개선 (권장) +### Phase 1: 추가 컴포넌트 ⬅️ 다음 권장 작업 -1. **레이아웃 도구** ⬅️ 다음 권장 작업 +1. **이미지 컴포넌트** - - 격자 스냅 (Grid Snap) - - 정렬 가이드라인 - - 컴포넌트 그룹화 - - 실행 취소/다시 실행 (Undo/Redo) + - 이미지 업로드 및 URL 입력 + - 크기 조절 및 정렬 + - 로고, 서명, 도장 등에 활용 -2. **쿼리 관리 개선** - - 쿼리 미리보기 개선 (테이블 형태) - - 쿼리 저장/불러오기 - - 쿼리 템플릿 +2. **구분선 컴포넌트 (Divider)** -### Phase 2: 추가 컴포넌트 + - 가로/세로 구분선 + - 두께, 색상, 스타일(실선/점선) 설정 -4. **다양한 컴포넌트 추가** +3. **차트 컴포넌트** (선택사항) + - 막대 차트 + - 선 차트 + - 원형 차트 + - 쿼리 데이터 연동 - - 이미지 컴포넌트 - - 차트 컴포넌트 (막대, 선, 원형) - - 바코드/QR코드 (선택사항) - - 구분선 (Divider) - - 체크박스/라디오 버튼 +### Phase 2: 고급 기능 + +4. **조건부 서식** -5. **조건부 서식** - 특정 조건에 따른 스타일 변경 - 값 범위에 따른 색상 표시 - 수식 기반 표시/숨김 +5. **쿼리 관리 개선** + - 쿼리 미리보기 개선 (테이블 형태) + - 쿼리 저장/불러오기 + - 쿼리 템플릿 + ### Phase 3: 성능 및 보안 6. **성능 최적화** @@ -313,4 +339,4 @@ **최종 업데이트**: 2025-10-01 **작성자**: AI Assistant -**상태**: PDF/WORD 내보내기 완료 (95% 완료) +**상태**: 레이아웃 도구 완료 (Phase 1 완료, 약 98% 완료) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index cf58991c..af2af362 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -10,6 +10,7 @@ interface CanvasComponentProps { export function CanvasComponent({ component }: CanvasComponentProps) { const { + components, selectedComponentId, selectedComponentIds, selectComponent, @@ -28,6 +29,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) { const isSelected = selectedComponentId === component.id; const isMultiSelected = selectedComponentIds.includes(component.id); const isLocked = component.locked === true; + const isGrouped = !!component.groupId; // 드래그 시작 const handleMouseDown = (e: React.MouseEvent) => { @@ -48,7 +50,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) { // Ctrl/Cmd 키 감지 (다중 선택) const isMultiSelect = e.ctrlKey || e.metaKey; - selectComponent(component.id, isMultiSelect); + + // 그룹화된 컴포넌트 클릭 시: 같은 그룹의 모든 컴포넌트 선택 + if (isGrouped && !isMultiSelect) { + const groupMembers = components.filter((c) => c.groupId === component.groupId); + const groupMemberIds = groupMembers.map((c) => c.id); + // 첫 번째 컴포넌트를 선택하고, 나머지를 다중 선택에 추가 + selectComponent(groupMemberIds[0], false); + groupMemberIds.slice(1).forEach((id) => selectComponent(id, true)); + } else { + selectComponent(component.id, isMultiSelect); + } setIsDragging(true); setDragStart({ @@ -89,11 +101,27 @@ export function CanvasComponent({ component }: CanvasComponentProps) { // 정렬 가이드라인 계산 calculateAlignmentGuides(component.id, snappedX, snappedY, component.width, component.height); - // Grid Snap 적용 + // 이동 거리 계산 + const deltaX = snappedX - component.x; + const deltaY = snappedY - component.y; + + // 현재 컴포넌트 이동 updateComponent(component.id, { x: snappedX, y: snappedY, }); + + // 그룹화된 경우: 같은 그룹의 다른 컴포넌트도 함께 이동 + if (isGrouped) { + components.forEach((c) => { + if (c.groupId === component.groupId && c.id !== component.id) { + updateComponent(c.id, { + x: c.x + deltaX, + y: c.y + deltaY, + }); + } + }); + } } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; @@ -131,8 +159,13 @@ export function CanvasComponent({ component }: CanvasComponentProps) { resizeStart.width, resizeStart.height, component.id, + component.x, + component.y, component.width, component.height, + component.groupId, + isGrouped, + components, updateComponent, snapValueToGrid, calculateAlignmentGuides, @@ -324,6 +357,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
🔒
)} + {/* 그룹화 표시 */} + {isGrouped && !isLocked && ( +
👥
+ )} + {/* 리사이즈 핸들 (선택된 경우만, 잠금 안 된 경우만) */} {isSelected && !isLocked && (
= 2; const canDistribute = selectedComponentIds && selectedComponentIds.length >= 3; const hasSelection = selectedComponentIds && selectedComponentIds.length >= 1; + const canGroup = selectedComponentIds && selectedComponentIds.length >= 2; // 템플릿 저장 가능 여부: 컴포넌트가 있어야 함 const canSaveAsTemplate = components.length > 0; @@ -406,6 +411,32 @@ export function ReportDesignerToolbar() { + {/* 그룹화 드롭다운 */} + + + + + + + + 그룹화 (2개 이상) + + + + 그룹 해제 + + + +