diff --git a/docs/화면관리_시스템_설계.md b/docs/화면관리_시스템_설계.md index 1c72c952..67e82fba 100644 --- a/docs/화면관리_시스템_설계.md +++ b/docs/화면관리_시스템_설계.md @@ -30,6 +30,17 @@ - **실시간 미리보기**: 설계한 화면을 실제 화면과 동일하게 확인 가능 - **메뉴 연동**: 각 회사의 메뉴에 화면 할당 및 관리 +### 🆕 최근 업데이트 (요약) + +- **픽셀 기반 자유 이동**: 격자 스냅을 제거하고 커서를 따라 정확히 이동하도록 구현. 그리드 라인은 시각적 가이드만 유지 +- **멀티 선택 강화**: Shift+클릭 + 드래그 박스(마키)로 다중선택 가능. 그룹 컨테이너는 선택에서 자동 제외 +- **다중 드래그 이동**: 다중선택 항목을 함께 이동(상대 위치 유지). 스크롤/그랩 오프셋 반영으로 튐 현상 제거 +- **그룹 UI 간소화**: 그룹 헤더/테두리 박스 제거(투명 컨테이너). 그룹 내부에만 집중 +- **그룹 내 정렬/분배 툴**: 좌/가로중앙/우, 상/세로중앙/하 정렬 + 가로/세로 균등 분배 추가(아이콘 UI) +- **왼쪽 목록 UX**: 검색·페이징 도입으로 대량 테이블 로딩 지연 완화 +- **Undo/Redo**: 최대 50단계, 단축키(Ctrl/Cmd+Z, Ctrl/Cmd+Y) +- **위젯 타입 렌더링 보강**: code/entity/file 포함 실제 위젯 형태로 표시 + ### 🎯 **현재 테이블 구조와 100% 호환** **기존 테이블 타입관리 시스템과 완벽 연계:** @@ -571,6 +582,8 @@ size: { width: number; height: number }; ### 2. 컴포넌트 배치 로직 +현재 배치 로직은 **픽셀 기반 자유 위치**로 동작합니다. 마우스 그랩 오프셋과 스크롤 오프셋을 반영하여 커서를 정확히 추적합니다. 아래 그리드 기반 예시는 참고용이며, 실제 런타임에서는 스냅을 적용하지 않습니다. + ```typescript // 그리드 기반 배치 function calculateGridPosition( @@ -1171,6 +1184,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }, [undo, redo]); ``` +#### 선택/이동 UX (현행) + +- Shift+클릭으로 다중선택 가능 +- 캔버스 빈 영역 드래그로 **마키 선택** 가능(Shift 누르면 기존 선택에 추가) +- 다중선택 상태에서 드래그 시 전체가 함께 이동(상대 좌표 유지) +- 그룹 컨테이너는 선택/정렬 대상에서 자동 제외 + // 컴포넌트 추가 const addComponent = (component: ComponentData) => { setLayout((prev) => ({ @@ -2265,26 +2285,31 @@ export class TableTypeIntegrationService { - **직관적 인터페이스**: 드래그앤드롭 기반 화면 설계 - **실시간 피드백**: 컴포넌트 배치 즉시 미리보기 표시 -- **키보드 지원**: Ctrl+Z/Ctrl+Y 단축키로 빠른 작업 +- **다중선택**: Shift+클릭 및 마키 선택 지원, 다중 드래그 이동 +- **정렬/분배**: 그룹 내 좌/중앙/우·상/중앙/하 정렬 및 균등 분배 +- **키보드 지원**: Ctrl/Cmd+Z, Ctrl/Cmd+Y 단축키 - **반응형 UI**: 전체 화면 활용한 효율적인 레이아웃 ## 🚀 다음 단계 계획 ### 1. 컴포넌트 그룹화 기능 -- [ ] 여러 위젯을 컨테이너로 그룹화 -- [ ] 부모-자식 관계 설정 -- [ ] 그룹 단위 이동/삭제 기능 +- [x] 여러 위젯을 컨테이너로 그룹화 +- [x] 부모-자식 관계 설정(parentId) +- [x] 그룹 단위 이동 +- [x] 그룹 UI 단순화(헤더/박스 제거) +- [x] 그룹 내 정렬/균등 분배 도구(아이콘 UI) +- [ ] 그룹 단위 삭제/복사/붙여넣기 ### 2. 레이아웃 저장/로드 -- [ ] 설계한 화면을 데이터베이스에 저장 +- [ ] 설계한 화면을 데이터베이스에 저장 (프론트 통합 진행 필요) - [ ] 저장된 화면 불러오기 기능 - [ ] 버전 관리 시스템 ### 3. 데이터 바인딩 -- [ ] 실제 데이터베이스와 연결 +- [ ] 실제 데이터베이스와 연결 (메타데이터 연동은 완료) - [ ] 폼 제출 및 데이터 저장 - [ ] 유효성 검증 시스템 diff --git a/frontend/components/screen/GroupingToolbar.tsx b/frontend/components/screen/GroupingToolbar.tsx index 24084853..642928a8 100644 --- a/frontend/components/screen/GroupingToolbar.tsx +++ b/frontend/components/screen/GroupingToolbar.tsx @@ -14,7 +14,19 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Group, Ungroup, Palette, Settings, X, Check } from "lucide-react"; +import { + Group, + Ungroup, + Palette, + Settings, + X, + Check, + AlignLeft, + AlignCenter, + AlignRight, + StretchHorizontal, + StretchVertical, +} from "lucide-react"; import { GroupState, ComponentData, ComponentStyle } from "@/types/screen"; import { createGroupStyle } from "@/lib/utils/groupingUtils"; @@ -25,6 +37,8 @@ interface GroupingToolbarProps { onGroupUngroup: (groupId: string) => void; selectedComponents: ComponentData[]; allComponents: ComponentData[]; + onGroupAlign?: (mode: "left" | "centerX" | "right" | "top" | "centerY" | "bottom") => void; + onGroupDistribute?: (orientation: "horizontal" | "vertical") => void; } export const GroupingToolbar: React.FC = ({ @@ -34,6 +48,8 @@ export const GroupingToolbar: React.FC = ({ onGroupUngroup, selectedComponents, allComponents, + onGroupAlign, + onGroupDistribute, }) => { const [showCreateDialog, setShowCreateDialog] = useState(false); const [groupTitle, setGroupTitle] = useState("새 그룹"); @@ -102,6 +118,9 @@ export const GroupingToolbar: React.FC = ({ {selectedComponents.length > 0 && ( {selectedComponents.length}개 선택됨 + {selectedComponents.length > 1 && ( + (Shift+클릭으로 다중선택, 드래그로 함께 이동) + )} )} @@ -147,6 +166,49 @@ export const GroupingToolbar: React.FC = ({ )} + + {/* 정렬/분배 도구 */} + {selectedComponents.length > 1 && ( +
+ 정렬 + + + + + + +
+ 균등 + + +
+ )}
diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 4a839817..400de969 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -27,7 +27,7 @@ import { interface RealtimePreviewProps { component: ComponentData; isSelected?: boolean; - onClick?: () => void; + onClick?: (e?: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기 @@ -36,16 +36,22 @@ interface RealtimePreviewProps { // 웹 타입에 따른 위젯 렌더링 const renderWidget = (component: ComponentData) => { - const { widgetType, label, placeholder, required, readonly, columnName } = component; + const { widgetType, label, placeholder, required, readonly, columnName, style } = component; // 디버깅: 실제 widgetType 값 확인 console.log("RealtimePreview - widgetType:", widgetType, "columnName:", columnName); + // 사용자가 테두리를 설정했는지 확인 + const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border); + + // 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기 + const borderClass = hasCustomBorder ? "!border-0" : ""; + const commonProps = { placeholder: placeholder || `입력하세요...`, disabled: readonly, required: required, - className: "w-full h-full", + className: `w-full h-full ${borderClass}`, }; switch (widgetType) { @@ -68,7 +74,9 @@ const renderWidget = (component: ComponentData) => {