# 화면관리 시스템 설계 문서 ## 📋 목차 1. [시스템 개요](#시스템-개요) 2. [아키텍처 구조](#아키텍처-구조) 3. [핵심 기능](#핵심-기능) 4. [데이터베이스 설계](#데이터베이스-설계) 5. [화면 구성 요소](#화면-구성-요소) 6. [드래그앤드롭 설계](#드래그앤드롭-설계) 7. [테이블 타입 연계](#테이블-타입-연계) 8. [API 설계](#api-설계) 9. [프론트엔드 구현](#프론트엔드-구현) 10. [백엔드 구현](#백엔드-구현) 11. [사용 시나리오](#사용-시나리오) 12. [개발 계획](#개발-계획) ## 🎯 시스템 개요 ### 화면관리 시스템이란? 화면관리 시스템은 사용자가 속한 회사에 맞춰 화면을 드래그앤드롭으로 설계하고 관리할 수 있는 시스템입니다. 테이블 타입관리와 연계하여 각 필드가 웹에서 어떻게 표시될지를 정의하고, 사용자가 직관적으로 화면을 구성할 수 있습니다. ### 주요 특징 - **회사별 화면 관리**: 사용자 회사 코드에 따른 화면 접근 제어 - **드래그앤드롭 인터페이스**: 직관적인 화면 설계 - **컨테이너 그룹화**: 컴포넌트를 깔끔하게 정렬하는 그룹 기능 - **테이블 타입 연계**: 컬럼의 웹 타입에 따른 자동 위젯 생성 - **실시간 미리보기**: 설계한 화면을 실제 화면과 동일하게 확인 가능 - **메뉴 연동**: 각 회사의 메뉴에 화면 할당 및 관리 ### 🆕 최근 업데이트 (2024.12) #### ✅ 완료된 주요 기능들 - **컴포넌트 관리 시스템**: 드래그앤드롭, 다중 선택, 그룹 드래그, 실시간 위치 업데이트 - **⚡ 실시간 속성 편집 시스템**: 로컬 상태 기반 즉시 반영, 완벽한 입력/체크박스 실시간 업데이트 - **속성 편집 시스템**: 라벨 관리 (텍스트, 폰트, 색상, 여백), 필수 입력 시 주황색 \* 표시 - **격자 시스템**: 동적 격자 설정, 컴포넌트 스냅 및 크기 조정 - **패널 관리**: 플로팅 패널, 수동 크기 조정, 위치 기억 - **웹타입 지원**: text, number, decimal, date, datetime, select, dropdown, textarea, boolean, checkbox, radio, code, entity, file - **데이터 테이블 컴포넌트**: 완전한 실시간 설정 시스템, 컬럼 관리, 필터링, 페이징 - **🆕 실시간 데이터 테이블**: 실제 PostgreSQL 데이터 조회, 웹타입별 검색 필터, 페이지네이션, 데이터 포맷팅 - **🆕 화면 저장 후 메뉴 할당**: 저장 완료 시 자동 메뉴 할당 모달, 기존 화면 교체 확인, 시각적 피드백 및 자동 목록 복귀 #### 🔧 해결된 기술적 문제들 - **⚡ 실시간 속성 편집 완성**: 로컬 상태 기반 이중 관리 시스템으로 완벽한 실시간 반영 - **체크박스 실시간 업데이트**: 모든 체크박스의 즉시 상태 변경 및 유지 - **동적 컴포넌트 상태 관리**: ID 기반 컬럼별 개별 상태 관리 및 동기화 - **라벨 하단 여백 동적 적용**: 여백값에 따른 정확한 위치 계산 - **스타일 속성 개별 업데이트**: 초기화 방지를 위한 `style.propertyName` 방식 적용 - **다중 드래그 최적화**: 지연 없는 실시간 미리보기, 선택 해제 방지 - **입력값 보존 시스템**: 패널 재오픈해도 사용자 입력값 완벽 유지 #### 🎯 개발 진행 상황 - **현재 완성도**: 98% (실시간 편집 시스템 완성, 핵심 기능 완료) - **기술 스택**: Next.js 15.4.4, TypeScript, Tailwind CSS, Shadcn/ui - **⚡ 상태 관리**: **완성된 실시간 속성 편집 패턴** - 로컬 상태 + 글로벌 상태 이중 관리 - **드래그앤드롭**: HTML5 Drag & Drop API 기반 고도화된 시스템 - **🎯 표준화**: 모든 속성 편집 컴포넌트에 실시간 패턴 적용 완료 ### 🎯 **현재 테이블 구조와 100% 호환** **기존 테이블 타입관리 시스템과 완벽 연계:** - ✅ **`table_labels`**: 테이블 메타데이터와 연계 - ✅ **`column_labels`**: 컬럼 웹 타입 및 상세 설정과 연계 - ✅ **웹 타입 지원**: text, number, date, code, entity, textarea, select, checkbox, radio, file - ✅ **상세 설정**: JSON 형태로 유연한 설정 저장 - ✅ **코드 연계**: 공통코드 시스템과 완벽 연동 - ✅ **엔티티 참조**: 참조 테이블 시스템 완벽 지원 **별도의 테이블 구조 변경 없이 바로 개발 가능!** 🚀 ### 🏢 **회사별 화면 관리 시스템** **사용자 권한에 따른 화면 접근 제어:** - ✅ **일반 사용자**: 자신이 속한 회사의 화면만 제작/수정 가능 - ✅ **관리자 (회사코드 '\*')**: 모든 회사의 화면을 제어 가능 - ✅ **회사별 메뉴 할당**: 각 회사의 메뉴에만 화면 할당 가능 - ✅ **권한 격리**: 회사 간 화면 데이터 완전 분리 ### 지원하는 웹 타입 테이블 타입관리에서 각 컬럼별로 설정할 수 있는 웹 타입입니다: - **text**: 일반 텍스트 입력 - **number**: 숫자 입력 - **date**: 날짜 선택기 - **code**: 코드 선택 (공통코드) - **entity**: 엔티티 참조 (참조테이블) - **textarea**: 여러 줄 텍스트 - **select**: 드롭다운 선택 - **checkbox**: 체크박스 - **radio**: 라디오 버튼 - **file**: 파일 업로드 > **구현 완료**: 테이블 타입관리 시스템에서 위의 모든 웹 타입을 지정할 수 있으며, 웹 타입별로 적절한 상세 설정을 자동으로 제공합니다. ### 웹 타입 관리 기능 #### 1. 웹 타입 설정 - **자동 상세 설정**: 웹 타입 선택 시 해당 타입에 맞는 기본 상세 설정을 자동으로 제공 - **실시간 저장**: 웹 타입 변경 시 즉시 백엔드 데이터베이스에 저장 - **오류 복구**: 저장 실패 시 원래 상태로 자동 복원 - **상세 설정 편집**: 웹 타입별 상세 설정을 모달에서 JSON 형태로 편집 가능 #### 2. 웹 타입별 상세 설정 | 웹 타입 | 기본 상세 설정 | 추가 설정 옵션 | | ------------ | ---------------------------- | --------------------- | | **text** | 최대 길이: 255자 | 사용자 정의 설정 | | **number** | 숫자 입력 (정수/실수) | 범위, 정밀도 설정 | | **date** | 날짜 형식: YYYY-MM-DD | 날짜 범위, 형식 설정 | | **textarea** | 여러 줄 텍스트 (최대 1000자) | 행 수, 최대 길이 설정 | | **select** | 드롭다운 선택 옵션 | 옵션 목록 설정 | | **checkbox** | 체크박스 (Y/N) | 기본값, 라벨 설정 | | **radio** | 라디오 버튼 그룹 | 옵션 그룹 설정 | | **file** | 파일 업로드 (최대 10MB) | 파일 형식, 크기 설정 | | **code** | 공통코드 선택 | 코드 카테고리 지정 | | **entity** | 엔티티 참조 | 참조 테이블/컬럼 지정 | #### 3. 사용 방법 1. **테이블 선택**: 테이블 타입관리에서 관리할 테이블 선택 2. **컬럼 확인**: 해당 테이블의 모든 컬럼 정보 표시 3. **웹 타입 설정**: 각 컬럼의 웹 타입을 드롭다운에서 선택 4. **자동 저장**: 선택 즉시 백엔드에 저장되고 상세 설정 자동 적용 5. **상세 설정 편집**: "상세 설정 편집" 버튼을 클릭하여 JSON 형태로 추가 설정 수정 6. **설정 저장**: 수정된 상세 설정을 저장하여 완료 ## 🏗️ 아키텍처 구조 ### 전체 구조도 ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Frontend │ │ Backend │ │ Database │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ Screen │ │ │ │ Screen │ │ │ │ screen_ │ │ │ │ Designer │ │ │ │ Management │ │ │ │ definitions │ │ │ │ (React) │ │ │ │ Controller │ │ │ │ Table │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ Drag & │ │ │ │ Screen │ │ │ │ screen_ │ │ │ │ Drop │ │ │ │ Management │ │ │ │ layouts │ │ │ │ Components │ │ │ │ Service │ │ │ │ Table │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ Preview │ │ │ │ Table │ │ │ │ table_ │ │ │ │ Generator │ │ │ │ Type │ │ │ │ labels │ │ │ │ (Runtime) │ │ │ │ Service │ │ │ │ Table │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` ### 회사별 권한 관리 구조 ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 사용자 │ │ 권한 검증 │ │ 화면 데이터 │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ 회사코드 │ │───▶│ │ 권한 검증 │ │───▶│ │ 회사별 │ │ │ │ (company_ │ │ │ │ 미들웨어 │ │ │ │ 화면 │ │ │ │ code) │ │ │ │ │ │ │ │ 격리 │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ 권한 레벨 │ │ │ │ 회사별 │ │ │ │ 메뉴 할당 │ │ │ │ (admin: '*')│ │ │ │ 데이터 │ │ │ │ 제한 │ │ │ └─────────────┘ │ │ │ 필터링 │ │ │ └─────────────┘ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` ### 데이터 흐름 1. **테이블 타입 정의**: 테이블 타입관리에서 컬럼의 웹 타입 설정 2. **화면 설계**: 드래그앤드롭으로 화면 레이아웃 구성 3. **위젯 매핑**: 컬럼과 화면 위젯을 연결 4. **설정 저장**: 화면 정의를 데이터베이스에 저장 5. **런타임 생성**: 실제 서비스 화면을 동적으로 생성 ## ⚡ 실시간 속성 편집 시스템 ### 개요 화면관리 시스템의 핵심 기능 중 하나인 실시간 속성 편집은 사용자가 컴포넌트의 속성을 수정할 때 즉시 화면에 반영되는 시스템입니다. 이 시스템은 **로컬 상태 기반 입력 관리**와 **실시간 업데이트 패턴**을 통해 구현되었습니다. ### 🎯 핵심 아키텍처 패턴 #### 1. 로컬 상태 + 글로벌 상태 이중 관리 ```typescript // 1단계: 로컬 상태 정의 (실시간 표시용) const [localInputs, setLocalInputs] = useState({ title: component.title || "", placeholder: component.placeholder || "", // 모든 입력 필드의 현재 값 }); const [localValues, setLocalValues] = useState({ showButton: component.showButton ?? true, enabled: component.enabled ?? false, // 모든 체크박스의 현재 상태 }); // 2단계: 컴포넌트 변경 시 자동 동기화 useEffect(() => { setLocalInputs({ title: component.title || "", placeholder: component.placeholder || "", }); setLocalValues({ showButton: component.showButton ?? true, enabled: component.enabled ?? false, }); }, [component.title, component.placeholder, component.showButton]); ``` #### 2. 실시간 입력 처리 패턴 ```typescript // 텍스트 입력 - 즉시 반영 { const newValue = e.target.value; // 1) 로컬 상태 즉시 업데이트 (화면 반영) setLocalInputs(prev => ({ ...prev, title: newValue })); // 2) 글로벌 상태 업데이트 (데이터 저장) onUpdateProperty("title", newValue); }} /> // 체크박스 - 즉시 반영 { // 1) 로컬 상태 즉시 업데이트 setLocalValues(prev => ({ ...prev, showButton: checked as boolean })); // 2) 글로벌 상태 업데이트 onUpdateProperty("showButton", checked); }} /> ``` #### 3. 동적 컴포넌트별 상태 관리 ```typescript // 컬럼별 개별 상태 관리 (ID 기반) const [localColumnInputs, setLocalColumnInputs] = useState< Record >({}); const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState< Record >({}); // 기존 값 보존하면서 새 항목만 추가 useEffect(() => { setLocalColumnInputs((prev) => { const newInputs = { ...prev }; component.columns?.forEach((col) => { if (!(col.id in newInputs)) { // 기존 입력값 보존 newInputs[col.id] = col.label; } }); return newInputs; }); }, [component.columns]); // 동적 입력 처리 { const newValue = e.target.value; setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue })); updateColumn(column.id, { label: newValue }); }} />; ``` ### 🔧 구현 표준 가이드라인 #### 필수 구현 패턴 1. **로컬 우선 원칙**: 모든 입력은 로컬 상태를 먼저 업데이트 2. **즉시 반영**: 로컬 상태 업데이트와 동시에 컴포넌트 속성 업데이트 3. **기존값 보존**: useEffect에서 기존 로컬 입력값이 있으면 덮어쓰지 않음 4. **완전한 정리**: 항목 삭제 시 관련된 모든 로컬 상태도 함께 정리 5. **타입 안전성**: 모든 상태에 정확한 TypeScript 타입 지정 #### 항목 추가/삭제 시 상태 관리 ```typescript // 추가 시 const addItem = useCallback( (newItem) => { // 로컬 상태에 즉시 추가 setLocalColumnInputs((prev) => ({ ...prev, [newItem.id]: newItem.label, })); setLocalColumnCheckboxes((prev) => ({ ...prev, [newItem.id]: { visible: true, sortable: true, searchable: true }, })); // 실제 컴포넌트 업데이트 onUpdateComponent({ items: [...component.items, newItem] }); }, [component.items, onUpdateComponent] ); // 삭제 시 const removeItem = useCallback( (itemId) => { // 로컬 상태에서 제거 setLocalColumnInputs((prev) => { const newInputs = { ...prev }; delete newInputs[itemId]; return newInputs; }); setLocalColumnCheckboxes((prev) => { const newCheckboxes = { ...prev }; delete newCheckboxes[itemId]; return newCheckboxes; }); // 실제 컴포넌트 업데이트 const updatedItems = component.items.filter((item) => item.id !== itemId); onUpdateComponent({ items: updatedItems }); }, [component.items, onUpdateComponent] ); ``` ### 📊 적용 범위 이 패턴은 화면관리 시스템의 다음 컴포넌트들에 적용되었습니다: - **PropertiesPanel**: 기본 속성 편집 (위치, 크기, 라벨 등) - **DataTableConfigPanel**: 데이터 테이블 상세 설정 - **DateTypeConfigPanel**: 날짜 타입 상세 설정 - **NumberTypeConfigPanel**: 숫자 타입 상세 설정 - **SelectTypeConfigPanel**: 선택박스 타입 상세 설정 - **TextTypeConfigPanel**: 텍스트 타입 상세 설정 - **기타 모든 웹타입별 설정 패널들** ### 🎯 사용자 경험 향상 효과 - **🚀 즉시 피드백**: 타이핑하는 순간 화면에 바로 반영 - **🔄 상태 일관성**: 패널을 닫았다 열어도 입력한 값이 정확히 유지 - **⚡ 빠른 반응성**: 지연 없는 실시간 UI 업데이트 - **🛡️ 안정성**: 메모리 누수 없는 완전한 상태 관리 ## 🚀 핵심 기능 ### 1. 화면 설계기 (Screen Designer) - **드래그앤드롭 인터페이스**: 컴포넌트를 캔버스에 배치 - **그리드 시스템**: 12컬럼 그리드 기반 레이아웃 - **컨테이너 그룹화**: 컴포넌트를 깔끔하게 정렬하는 그룹 기능 - **실시간 미리보기**: 설계한 화면을 실제 화면과 동일하게 확인 ### 2. 컴포넌트 라이브러리 - **입력 컴포넌트**: text, number, date, textarea 등 - **선택 컴포넌트**: select, checkbox, radio 등 - **표시 컴포넌트**: label, display, image 등 - **레이아웃 컴포넌트**: container, row, column, group 등 - **컨테이너 컴포넌트**: 컴포넌트들을 그룹으로 묶는 기능 ### 3. 회사별 권한 관리 - **회사 코드 기반 접근 제어**: 사용자 회사 코드에 따른 화면 접근 - **관리자 권한**: 회사 코드 '\*'인 사용자는 모든 회사 화면 제어 - **회사별 메뉴 할당**: 각 회사의 메뉴에만 화면 할당 가능 - **데이터 격리**: 회사 간 화면 데이터 완전 분리 ### 4. 테이블 연계 시스템 - **자동 위젯 생성**: 컬럼의 웹 타입에 따른 위젯 자동 생성 - **데이터 바인딩**: 컬럼과 위젯의 자동 연결 - **유효성 검증**: 컬럼 설정에 따른 자동 검증 규칙 적용 ### 5. 템플릿 시스템 - **기본 템플릿**: CRUD, 목록, 상세 등 기본 패턴 - **사용자 정의 템플릿**: 자주 사용하는 레이아웃 저장 - **템플릿 공유**: 팀원 간 템플릿 공유 및 재사용 ### 6. 메뉴 연동 시스템 - **회사별 메뉴 할당**: 각 회사의 메뉴에만 화면 할당 - **메뉴-화면 연결**: 메뉴와 화면의 1:1 또는 1:N 연결 - **권한 기반 메뉴 표시**: 사용자 권한에 따른 메뉴 표시 제어 - **🆕 저장 후 자동 할당**: 화면 저장 완료 시 메뉴 할당 모달 자동 팝업 - **🆕 기존 화면 교체**: 이미 할당된 화면이 있을 때 교체 확인 및 안전한 처리 - **🆕 완전한 워크플로우**: 저장 → 할당 → 목록 복귀의 자연스러운 흐름 ## 🗄️ 데이터베이스 설계 ### 1. 기존 테이블 구조 (테이블 타입관리) #### table_labels (테이블 메타데이터) ```sql CREATE TABLE table_labels ( table_name varchar(100) NOT NULL, table_label varchar(200) NULL, description text NULL, created_date timestamp DEFAULT now() NULL, updated_date timestamp DEFAULT now() NULL, CONSTRAINT table_labels_pkey PRIMARY KEY (table_name) ); ``` #### column_labels (컬럼 메타데이터 + 웹 타입) ```sql CREATE TABLE column_labels ( id serial4 NOT NULL, table_name varchar(100) NULL, column_name varchar(100) NULL, column_label varchar(200) NULL, web_type varchar(50) NULL, -- 🎯 핵심: 웹 타입 정의 detail_settings text NULL, -- 🎯 핵심: 웹 타입별 상세 설정 (JSON) description text NULL, display_order int4 DEFAULT 0 NULL, is_visible bool DEFAULT true NULL, code_category varchar(100) NULL, -- 🎯 code 타입용: 공통코드 카테고리 code_value varchar(100) NULL, reference_table varchar(100) NULL, -- 🎯 entity 타입용: 참조 테이블 reference_column varchar(100) NULL, -- 🎯 entity 타입용: 참조 컬럼 created_date timestamp DEFAULT now() NULL, updated_date timestamp DEFAULT now() NULL, CONSTRAINT column_labels_pkey PRIMARY KEY (id), CONSTRAINT column_labels_table_name_column_name_key UNIQUE (table_name, column_name) ); -- 외래키 제약조건 ALTER TABLE column_labels ADD CONSTRAINT column_labels_table_name_fkey FOREIGN KEY (table_name) REFERENCES table_labels(table_name); ``` **권장 인덱스 추가:** ```sql -- 자주 조회되는 컬럼 조합에 인덱스 추가 CREATE INDEX idx_column_labels_web_type ON column_labels(web_type); CREATE INDEX idx_column_labels_code_category ON column_labels(code_category); CREATE INDEX idx_column_labels_reference ON column_labels(reference_table, reference_column); -- JSON 필드 검색을 위한 GIN 인덱스 (PostgreSQL) CREATE INDEX idx_column_labels_detail_settings ON column_labels USING GIN (detail_settings); ``` ### 2. 화면관리 시스템 테이블 구조 #### screen_definitions (화면 정의) ```sql CREATE TABLE screen_definitions ( screen_id SERIAL PRIMARY KEY, screen_name VARCHAR(100) NOT NULL, screen_code VARCHAR(50) UNIQUE NOT NULL, table_name VARCHAR(100) NOT NULL, -- 🎯 table_labels와 연계 company_code VARCHAR(50) NOT NULL, -- 🎯 회사 코드 (권한 관리용) description TEXT, is_active CHAR(1) DEFAULT 'Y', created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(50), updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_by VARCHAR(50), -- 외래키 제약조건 CONSTRAINT fk_screen_definitions_table_name FOREIGN KEY (table_name) REFERENCES table_labels(table_name) ); -- 회사 코드 인덱스 추가 CREATE INDEX idx_screen_definitions_company_code ON screen_definitions(company_code); ``` #### screen_layouts (화면 레이아웃) ```sql CREATE TABLE screen_layouts ( layout_id SERIAL PRIMARY KEY, screen_id INTEGER REFERENCES screen_definitions(screen_id), component_type VARCHAR(50) NOT NULL, -- container, row, column, widget component_id VARCHAR(100) UNIQUE NOT NULL, parent_id VARCHAR(100), -- 부모 컴포넌트 ID position_x INTEGER NOT NULL, -- X 좌표 (그리드) position_y INTEGER NOT NULL, -- Y 좌표 (그리드) width INTEGER NOT NULL, -- 너비 (그리드 컬럼 수: 1-12) height INTEGER NOT NULL, -- 높이 (픽셀) properties JSONB, -- 컴포넌트별 속성 display_order INTEGER DEFAULT 0, created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` #### screen_widgets (화면 위젯) ```sql CREATE TABLE screen_widgets ( widget_id SERIAL PRIMARY KEY, layout_id INTEGER REFERENCES screen_layouts(layout_id), table_name VARCHAR(100) NOT NULL, -- 🎯 column_labels와 연계 column_name VARCHAR(100) NOT NULL, -- 🎯 column_labels와 연계 widget_type VARCHAR(50) NOT NULL, -- 🎯 column_labels.web_type과 동기화 label VARCHAR(200), -- 표시 라벨 (column_labels.column_label 사용) placeholder VARCHAR(200), -- 플레이스홀더 is_required BOOLEAN DEFAULT FALSE, is_readonly BOOLEAN DEFAULT FALSE, validation_rules JSONB, -- 유효성 검증 규칙 (column_labels.detail_settings에서 자동 생성) display_properties JSONB, -- 표시 속성 (column_labels.detail_settings에서 자동 생성) created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 외래키 제약조건 CONSTRAINT fk_screen_widgets_column_labels FOREIGN KEY (table_name, column_name) REFERENCES column_labels(table_name, column_name) ); ``` #### screen_templates (화면 템플릿) ```sql CREATE TABLE screen_templates ( template_id SERIAL PRIMARY KEY, template_name VARCHAR(100) NOT NULL, template_type VARCHAR(50) NOT NULL, -- CRUD, LIST, DETAIL 등 company_code VARCHAR(50) NOT NULL, -- 🎯 회사 코드 (권한 관리용) description TEXT, layout_data JSONB, -- 레이아웃 데이터 is_public BOOLEAN DEFAULT FALSE, -- 공개 여부 created_by VARCHAR(50), created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 회사 코드 인덱스 추가 CREATE INDEX idx_screen_templates_company_code ON screen_templates(company_code); ``` #### screen_menu_assignments (화면-메뉴 할당) ```sql CREATE TABLE screen_menu_assignments ( assignment_id SERIAL PRIMARY KEY, screen_id INTEGER NOT NULL, menu_id INTEGER NOT NULL, company_code VARCHAR(50) NOT NULL, -- 🎯 회사 코드 (권한 관리용) display_order INTEGER DEFAULT 0, is_active CHAR(1) DEFAULT 'Y', created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by VARCHAR(50), -- 외래키 제약조건 CONSTRAINT fk_screen_menu_assignments_screen_id FOREIGN KEY (screen_id) REFERENCES screen_definitions(screen_id), CONSTRAINT fk_screen_menu_assignments_menu_id FOREIGN KEY (menu_id) REFERENCES menu_info(menu_id), -- 유니크 제약조건 (한 메뉴에 같은 화면 중복 할당 방지) CONSTRAINT uk_screen_menu_company UNIQUE (screen_id, menu_id, company_code) ); -- 회사 코드 인덱스 추가 CREATE INDEX idx_screen_menu_assignments_company_code ON screen_menu_assignments(company_code); ``` ### 3. 테이블 간 연계 관계 ``` table_labels (테이블 메타데이터) ↓ (1:N) column_labels (컬럼 메타데이터 + 웹 타입) ↓ (1:N) screen_definitions (화면 정의) ↓ (1:N) screen_layouts (화면 레이아웃) ↓ (1:N) screen_widgets (화면 위젯) ↓ (1:N) screen_menu_assignments (화면-메뉴 할당) ``` **핵심 연계 포인트:** - ✅ `screen_definitions.table_name` ↔ `table_labels.table_name` - ✅ `screen_widgets.table_name, column_name` ↔ `column_labels.table_name, column_name` - ✅ `screen_widgets.widget_type` ↔ `column_labels.web_type` (자동 동기화) - ✅ `screen_definitions.company_code` ↔ 사용자 회사 코드 (권한 관리) - ✅ `screen_menu_assignments.company_code` ↔ 메뉴 회사 코드 (메뉴 할당 제한) ## 🎨 화면 구성 요소 ### 1. 레이아웃 컴포넌트 #### Container (컨테이너) ```typescript interface ContainerProps { id: string; type: "container"; width: number; // 1-12 height: number; padding: number; margin: number; backgroundColor?: string; border?: string; borderRadius?: number; shadow?: string; } ``` #### Group (그룹) ```typescript interface GroupProps { id: string; type: "group"; title?: string; width: number; // 1-12 height: number; padding: number; margin: number; backgroundColor?: string; border?: string; borderRadius?: number; shadow?: string; collapsible?: boolean; // 접을 수 있는 그룹 collapsed?: boolean; // 접힌 상태 children: string[]; // 포함된 컴포넌트 ID 목록 } ``` #### Row (행) ```typescript interface RowProps { id: string; type: "row"; columns: number; // 1-12 gap: number; alignItems: "start" | "center" | "end"; justifyContent: "start" | "center" | "end" | "space-between"; } ``` #### Column (컬럼) ```typescript interface ColumnProps { id: string; type: "column"; width: number; // 1-12 offset: number; // 오프셋 order: number; // 순서 } ``` ### 2. 입력 위젯 #### Text Input ```typescript interface TextInputProps { id: string; type: "text"; label: string; placeholder?: string; required: boolean; maxLength?: number; pattern?: string; columnName: string; // 연결된 테이블 컬럼 } ``` #### Select Dropdown ```typescript interface SelectProps { id: string; type: "select"; label: string; options: Array<{ value: string; label: string }>; multiple: boolean; searchable: boolean; columnName: string; } ``` #### Date Picker ```typescript interface DatePickerProps { id: string; type: "date"; label: string; format: string; // YYYY-MM-DD, MM/DD/YYYY 등 minDate?: string; maxDate?: string; columnName: string; } ``` ### 3. 표시 위젯 #### Label ```typescript interface LabelProps { id: string; type: "label"; text: string; fontSize: number; fontWeight: "normal" | "bold"; color: string; alignment: "left" | "center" | "right"; } ``` #### Display Field ```typescript interface DisplayFieldProps { id: string; type: "display"; label: string; columnName: string; format?: string; // 날짜, 숫자 포맷 alignment: "left" | "center" | "right"; } ``` ## 🖱️ 드래그앤드롭 설계 ### 1. 드래그앤드롭 아키텍처 ```typescript // 드래그 상태 관리 interface DragState { isDragging: boolean; draggedItem: ComponentData | null; dragSource: "toolbox" | "canvas"; dropTarget: string | null; dropZone?: DropZone; // 드롭 가능한 영역 정보 } // 그룹화 상태 관리 interface GroupState { isGrouping: boolean; selectedComponents: string[]; groupTarget: string | null; groupMode: "create" | "add" | "remove"; } ``` // 드롭 영역 정의 interface DropZone { id: string; accepts: string[]; // 허용되는 컴포넌트 타입 position: { x: number; y: number }; size: { width: number; height: number }; } ```` ### 2. 컴포넌트 배치 로직 현재 배치 로직은 **픽셀 기반 자유 위치**로 동작합니다. 마우스 그랩 오프셋과 스크롤 오프셋을 반영하여 커서를 정확히 추적합니다. 아래 그리드 기반 예시는 참고용이며, 실제 런타임에서는 스냅을 적용하지 않습니다. ```typescript // 그리드 기반 배치 function calculateGridPosition( mouseX: number, mouseY: number, gridSize: number ): GridPosition { return { x: Math.floor(mouseX / gridSize), y: Math.floor(mouseY / gridSize), }; } // 컴포넌트 크기 조정 function resizeComponent( component: ComponentData, newWidth: number, newHeight: number ): ComponentData { return { ...component, width: Math.max(1, Math.min(12, newWidth)), height: Math.max(50, newHeight), }; } ```` ### 3. 실시간 미리보기 ```typescript // 캔버스 상태를 실제 컴포넌트로 변환 function generatePreview(layout: LayoutData): React.ReactElement { return (
{layout.components.map((component) => renderComponent(component))}
); } // 그룹 컴포넌트 렌더링 function renderGroupComponent( group: GroupProps, components: ComponentData[] ): React.ReactElement { const groupChildren = components.filter((c) => group.children.includes(c.id)); return (
{group.title &&
{group.title}
}
{groupChildren.map((component) => renderComponent(component))}
); } ``` // 컴포넌트 렌더링 function renderComponent(component: ComponentData): React.ReactElement { switch (component.type) { case "text": return ; case "select": return { e.stopPropagation(); // 이벤트 전파 방지 setSearchTerm(e.target.value); }} onKeyDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} className="h-8 pr-8 pl-10 text-sm" /> {searchTerm && ( )} {/* 메뉴 옵션들 */}
{getMenuOptions()}
``` ### 3. 기존 화면 감지 및 교체 시스템 ```typescript // 메뉴 선택 시 기존 할당된 화면 확인 const handleMenuSelect = async (menuId: string) => { const menu = menus.find((m) => m.objid?.toString() === menuId); setSelectedMenu(menu || null); if (menu) { try { const menuObjid = parseInt(menu.objid?.toString() || "0"); const screens = await menuScreenApi.getScreensByMenu(menuObjid); setExistingScreens(screens); } catch (error) { console.error("할당된 화면 조회 실패:", error); } } }; // 할당 시 기존 화면 확인 const handleAssignScreen = async () => { if (existingScreens.length > 0) { // 이미 같은 화면이 할당되어 있는지 확인 const alreadyAssigned = existingScreens.some( (screen) => screen.screenId === screenInfo.screenId ); if (alreadyAssigned) { toast.info("이미 해당 메뉴에 할당된 화면입니다."); return; } // 다른 화면이 할당되어 있으면 교체 확인 setShowReplaceDialog(true); return; } // 기존 화면이 없으면 바로 할당 await performAssignment(); }; ``` ### 4. 화면 교체 확인 대화상자 **시각적 구분:** - 🔴 **제거될 화면**: 빨간색 배경으로 표시 - 🟢 **새로 할당될 화면**: 초록색 배경으로 표시 - 🟠 **주의 메시지**: 작업이 되돌릴 수 없음을 명확히 안내 **안전한 교체 프로세스:** 1. 기존 화면들을 하나씩 제거 2. 새 화면 할당 3. 성공/실패 로그 출력 ```typescript // 기존 화면 교체인 경우 기존 화면들 먼저 제거 if (replaceExisting && existingScreens.length > 0) { for (const existingScreen of existingScreens) { try { await menuScreenApi.unassignScreenFromMenu( existingScreen.screenId, menuObjid ); console.log(`기존 화면 "${existingScreen.screenName}" 제거 완료`); } catch (error) { console.error( `기존 화면 "${existingScreen.screenName}" 제거 실패:`, error ); } } } // 새 화면 할당 await menuScreenApi.assignScreenToMenu(screenInfo.screenId, menuObjid); ``` ### 5. 성공 피드백 및 자동 이동 **성공 화면 구성:** - ✅ **체크마크 아이콘**: 성공을 나타내는 녹색 체크마크 - 🎯 **성공 메시지**: 구체적인 할당 완료 메시지 - ⏱️ **자동 이동 안내**: "3초 후 자동으로 화면 목록으로 이동합니다..." - 🔵 **로딩 애니메이션**: 3개의 점이 순차적으로 바운스하는 애니메이션 ```typescript // 성공 상태 설정 setAssignmentSuccess(true); setAssignmentMessage(successMessage); // 3초 후 자동으로 화면 목록으로 이동 setTimeout(() => { if (onBackToList) { onBackToList(); } else { onClose(); } }, 3000); ``` **성공 화면 UI:** ```jsx {assignmentSuccess ? ( // 성공 화면 <>
화면 할당 완료

{assignmentMessage}

3초 후 자동으로 화면 목록으로 이동합니다...

{/* 로딩 애니메이션 */}
) : ( // 기본 할당 화면 // ... )} ``` ### 6. 사용자 경험 개선사항 1. **선택적 할당**: 필수가 아닌 선택적 기능으로 "나중에 할당" 가능 2. **직관적 UI**: 저장된 화면 정보를 모달에서 바로 확인 가능 3. **검색 기능**: 많은 메뉴 중에서 쉽게 찾을 수 있음 4. **상태 표시**: 메뉴 활성/비활성 상태, 기존 할당된 화면 정보 표시 5. **완전한 워크플로우**: 저장 → 할당 → 목록 복귀의 자연스러운 흐름 ## 🌐 API 설계 ### 1. 메뉴-화면 할당 API #### 화면을 메뉴에 할당 ```typescript POST /screen-management/screens/:screenId/assign-menu Request: { menuObjid: number; displayOrder?: number; } ``` #### 메뉴별 할당된 화면 목록 조회 ```typescript GET /screen-management/menus/:menuObjid/screens Response: { success: boolean; data: ScreenDefinition[]; } ``` #### 화면-메뉴 할당 해제 ```typescript DELETE /screen-management/screens/:screenId/menus/:menuObjid Response: { success: boolean; message: string; } ``` ### 2. 화면 정의 API #### 화면 목록 조회 (회사별) ```typescript GET /api/screen-management/screens Query: { companyCode?: string; // 회사 코드 (관리자는 생략 가능) page?: number; size?: number; } Response: { success: boolean; data: ScreenDefinition[]; total: number; } ``` #### 화면 생성 (회사별) ```typescript POST /api/screen-management/screens Body: { screenName: string; screenCode: string; tableName: string; companyCode: string; // 사용자 회사 코드 자동 설정 description?: string; } ``` #### 화면 생성 ```typescript POST /api/screen-management/screens Body: { screenName: string; screenCode: string; tableName: string; description?: string; } ``` #### 화면 수정 ```typescript PUT /api/screen-management/screens/:screenId Body: { screenName?: string; description?: string; isActive?: boolean; } ``` #### 화면 삭제 ```typescript DELETE /api/screen-management/screens/:screenId ``` ### 2. 레이아웃 API #### 레이아웃 조회 ```typescript GET /api/screen-management/screens/:screenId/layout Response: { success: boolean; data: LayoutData; } ``` #### 레이아웃 저장 ```typescript POST /api/screen-management/screens/:screenId/layout Body: { components: ComponentData[]; gridSettings: GridSettings; } ``` #### 레이아웃 복사 ```typescript POST /api/screen-management/screens/:screenId/layout/copy Body: { targetScreenId: number; } ``` ### 3. 위젯 API #### 위젯 속성 조회 ```typescript GET /api/screen-management/widgets/:widgetId/properties Response: { success: boolean; data: WidgetProperties; } ``` #### 위젯 속성 수정 ```typescript PUT /api/screen-management/widgets/:widgetId/properties Body: { label?: string; placeholder?: string; required?: boolean; readonly?: boolean; validationRules?: ValidationRule[]; } ``` ### 4. 템플릿 API #### 템플릿 목록 조회 (회사별) ```typescript GET /api/screen-management/templates Query: { companyCode?: string; // 회사 코드 (관리자는 생략 가능) type?: string; isPublic?: boolean; createdBy?: string; } ``` ### 5. 메뉴 할당 API #### 화면-메뉴 할당 ```typescript POST /api/screen-management/screens/:screenId/assign-menu Body: { menuObjid: number; displayOrder?: number; // companyCode는 JWT에서 자동 추출 } ``` #### 메뉴별 화면 목록 조회 ```typescript GET /api/screen-management/menus/:menuObjid/screens Response: { success: boolean; data: ScreenDefinition[]; } ``` #### 화면-메뉴 할당 해제 ```typescript DELETE /api/screen-management/screens/:screenId/menus/:menuObjid Response: { success: boolean; message: string; } ``` #### 화면 코드 자동 생성 ```typescript GET /api/screen-management/generate-screen-code/:companyCode Response: { success: boolean; data: { screenCode: string; // 예: "COMP_001" }; } ``` #### 단일 화면 조회 ```typescript GET /api/screen-management/screens/:id Response: { success: boolean; data: ScreenDefinition; } ``` #### 템플릿 적용 ```typescript POST /api/screen-management/screens/:screenId/apply-template Body: { templateId: number; overrideExisting: boolean; } ``` ## 🎭 프론트엔드 구현 ### 1. 화면 설계기 컴포넌트 (구현 완료) ```typescript // ScreenDesigner.tsx - 현재 구현된 버전 export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenDesignerProps) { const [layout, setLayout] = useState({ components: [] }); const [selectedComponent, setSelectedComponent] = useState(null); // 실행취소/다시실행을 위한 히스토리 상태 const [history, setHistory] = useState([{ components: [], gridSettings: { columns: 12, gap: 16, padding: 16 }, }]); const [historyIndex, setHistoryIndex] = useState(0); // 히스토리에 상태 저장 const saveToHistory = useCallback((newLayout: LayoutData) => { setHistory(prevHistory => { const newHistory = prevHistory.slice(0, historyIndex + 1); newHistory.push(JSON.parse(JSON.stringify(newLayout))); // 깊은 복사 return newHistory.slice(-50); // 최대 50개 히스토리 유지 }); setHistoryIndex(prevIndex => Math.min(prevIndex + 1, 49)); }, [historyIndex]); // 실행취소/다시실행 함수 const undo = useCallback(() => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); setLayout(JSON.parse(JSON.stringify(history[newIndex]))); setSelectedComponent(null); } }, [historyIndex, history]); const redo = useCallback(() => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); setLayout(JSON.parse(JSON.stringify(history[newIndex]))); setSelectedComponent(null); } }, [historyIndex, history]); // 키보드 단축키 지원 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'z': e.preventDefault(); if (e.shiftKey) { redo(); // Ctrl+Shift+Z 또는 Cmd+Shift+Z } else { undo(); // Ctrl+Z 또는 Cmd+Z } break; case 'y': e.preventDefault(); redo(); // Ctrl+Y 또는 Cmd+Y break; } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [undo, redo]); ``` #### 선택/이동 UX (현행) - Shift+클릭으로 다중선택 가능 - 캔버스 빈 영역 드래그로 **마키 선택** 가능(Shift 누르면 기존 선택에 추가) - 다중선택 상태에서 드래그 시 전체가 함께 이동(상대 좌표 유지) - 그룹 컨테이너는 선택/정렬 대상에서 자동 제외 // 컴포넌트 추가 const addComponent = (component: ComponentData) => { setLayout((prev) => ({ ...prev, components: [...prev.components, component], })); }; // 컴포넌트 삭제 const removeComponent = (componentId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.filter((c) => c.id !== componentId), })); }; // 컴포넌트 이동 const moveComponent = (componentId: string, newPosition: Position) => { setLayout((prev) => ({ ...prev, components: prev.components.map((c) => c.id === componentId ? { ...c, position: newPosition } : c ), })); }; // 컴포넌트 그룹화 const groupComponents = (componentIds: string[], groupTitle?: string) => { const groupId = generateId(); const groupComponent: GroupProps = { id: groupId, type: "group", title: groupTitle || "그룹", width: 12, height: 200, padding: 16, margin: 8, backgroundColor: "#f8f9fa", border: "1px solid #dee2e6", borderRadius: 8, shadow: "0 2px 4px rgba(0,0,0,0.1)", collapsible: true, collapsed: false, children: componentIds, }; setLayout((prev) => ({ ...prev, components: [...prev.components, groupComponent], })); }; // 그룹에서 컴포넌트 제거 const ungroupComponent = (componentId: string, groupId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.map((c) => { if (c.id === groupId && c.type === "group") { return { ...c, children: c.children.filter((id) => id !== componentId), }; } return c; }), })); }; return (
c.id === selectedComponent)} onPropertyChange={updateComponentProperty} onGroupCreate={groupComponents} onGroupRemove={ungroupComponent} />
); } ```` ### 2. 드래그앤드롭 구현 ```typescript // useDragAndDrop.ts export function useDragAndDrop() { const [dragState, setDragState] = useState({ isDragging: false, draggedItem: null, dragSource: "toolbox", dropTarget: null, }); const startDrag = (item: ComponentData, source: "toolbox" | "canvas") => { setDragState({ isDragging: true, draggedItem: item, dragSource: source, dropTarget: null, }); }; const endDrag = () => { setDragState({ isDragging: false, draggedItem: null, dragSource: "toolbox", dropTarget: null, }); }; const updateDropTarget = (targetId: string | null) => { setDragState((prev) => ({ ...prev, dropTarget: targetId })); }; return { dragState, startDrag, endDrag, updateDropTarget, }; } ```` ### 3. 실시간 미리보기 시스템 (구현 완료) ```typescript // RealtimePreview.tsx - 현재 구현된 버전 export const RealtimePreview: React.FC = ({ component, isSelected = false, onClick, onDragStart, onDragEnd, }) => { const { type, label, tableName, columnName, widgetType, size, style } = component; return (
{type === "container" && (
{label}
{tableName}
)} {type === "widget" && (
{/* 위젯 헤더 */}
{getWidgetIcon(widgetType)}
{/* 위젯 본체 */}
{renderWidget(component)}
{/* 위젯 정보 */}
{columnName} ({widgetType})
)}
); }; // 웹 타입에 따른 위젯 렌더링 const renderWidget = (component: ComponentData) => { const { widgetType, label, placeholder, required, readonly, columnName } = component; const commonProps = { placeholder: placeholder || `입력하세요...`, disabled: readonly, required: required, className: "w-full", }; switch (widgetType) { case "text": case "email": case "tel": return ( ); case "number": case "decimal": return ( ); case "date": case "datetime": return ( ); case "select": case "dropdown": return ( ); case "textarea": case "text_area": return