테이블 템플릿 제작

This commit is contained in:
kjs
2025-09-03 15:23:12 +09:00
parent 55a7e1dc89
commit 4a0c42d80c
14 changed files with 3757 additions and 42 deletions

View File

@@ -35,25 +35,31 @@
#### ✅ 완료된 주요 기능들
- **컴포넌트 관리 시스템**: 드래그앤드롭, 다중 선택, 그룹 드래그, 실시간 위치 업데이트
- **속성 편집 시스템**: 실시간 속성 편집, 라벨 관리 (텍스트, 폰트, 색상, 여백), 필수 입력 시 주황색 \* 표시
- **⚡ 실시간 속성 편집 시스템**: 로컬 상태 기반 즉시 반영, 완벽한 입력/체크박스 실시간 업데이트
- **속성 편집 시스템**: 라벨 관리 (텍스트, 폰트, 색상, 여백), 필수 입력 시 주황색 \* 표시
- **격자 시스템**: 동적 격자 설정, 컴포넌트 스냅 및 크기 조정
- **패널 관리**: 플로팅 패널, 수동 크기 조정, 위치 기억
- **웹타입 지원**: text, number, decimal, date, datetime, select, dropdown, textarea, boolean, checkbox, radio, code, entity, file
- **데이터 테이블 컴포넌트**: 완전한 실시간 설정 시스템, 컬럼 관리, 필터링, 페이징
- **🆕 실시간 데이터 테이블**: 실제 PostgreSQL 데이터 조회, 웹타입별 검색 필터, 페이지네이션, 데이터 포맷팅
#### 🔧 해결된 기술적 문제들
- **⚡ 실시간 속성 편집 완성**: 로컬 상태 기반 이중 관리 시스템으로 완벽한 실시간 반영
- **체크박스 실시간 업데이트**: 모든 체크박스의 즉시 상태 변경 및 유지
- **동적 컴포넌트 상태 관리**: ID 기반 컬럼별 개별 상태 관리 및 동기화
- **라벨 하단 여백 동적 적용**: 여백값에 따른 정확한 위치 계산
- **스타일 속성 개별 업데이트**: 초기화 방지를 위한 `style.propertyName` 방식 적용
- **체크박스 실시간 반영**: 로컬 상태 + 실제 속성 동시 업데이트
- **다중 드래그 최적화**: 지연 없는 실시간 미리보기, 선택 해제 방지
- **입력 필드 실시간 적용**: debounce 제거, 즉시 반영 시스템
- **입력값 보존 시스템**: 패널 재오픈해도 사용자 입력값 완벽 유지
#### 🎯 개발 진행 상황
- **현재 완성도**: 95% (핵심 기능 완료)
- **현재 완성도**: 98% (실시간 편집 시스템 완성, 핵심 기능 완료)
- **기술 스택**: Next.js 15.4.4, TypeScript, Tailwind CSS, Shadcn/ui
- **상태 관리**: React Hooks 기반 로컬 상태 + 실시간 업데이트 패턴
- **상태 관리**: **완성된 실시간 속성 편집 패턴** - 로컬 상태 + 글로벌 상태 이중 관리
- **드래그앤드롭**: HTML5 Drag & Drop API 기반 고도화된 시스템
- **🎯 표준화**: 모든 속성 편집 컴포넌트에 실시간 패턴 적용 완료
### 🎯 **현재 테이블 구조와 100% 호환**
@@ -182,6 +188,187 @@
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
// 텍스트 입력 - 즉시 반영
<Input
value={localInputs.title}
onChange={(e) => {
const newValue = e.target.value;
// 1) 로컬 상태 즉시 업데이트 (화면 반영)
setLocalInputs(prev => ({ ...prev, title: newValue }));
// 2) 글로벌 상태 업데이트 (데이터 저장)
onUpdateProperty("title", newValue);
}}
/>
// 체크박스 - 즉시 반영
<Checkbox
checked={localValues.showButton}
onCheckedChange={(checked) => {
// 1) 로컬 상태 즉시 업데이트
setLocalValues(prev => ({ ...prev, showButton: checked as boolean }));
// 2) 글로벌 상태 업데이트
onUpdateProperty("showButton", checked);
}}
/>
```
#### 3. 동적 컴포넌트별 상태 관리
```typescript
// 컬럼별 개별 상태 관리 (ID 기반)
const [localColumnInputs, setLocalColumnInputs] = useState<
Record<string, string>
>({});
const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState<
Record<string, { visible: boolean; sortable: boolean; searchable: boolean }>
>({});
// 기존 값 보존하면서 새 항목만 추가
useEffect(() => {
setLocalColumnInputs((prev) => {
const newInputs = { ...prev };
component.columns?.forEach((col) => {
if (!(col.id in newInputs)) {
// 기존 입력값 보존
newInputs[col.id] = col.label;
}
});
return newInputs;
});
}, [component.columns]);
// 동적 입력 처리
<Input
value={
localColumnInputs[column.id] !== undefined
? localColumnInputs[column.id]
: column.label
}
onChange={(e) => {
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)
@@ -1760,6 +1947,271 @@ export default function GridItem({
## ⚙️ 백엔드 구현
### 🆕 데이터 테이블 실시간 조회 API (2025.09 추가)
#### 1. 테이블 데이터 조회 API
**라우트 설정**
```typescript
// tableManagementRoutes.ts
/**
* 테이블 데이터 조회 (페이징 + 검색)
* POST /api/table-management/tables/:tableName/data
*/
router.post("/tables/:tableName/data", getTableData);
```
**컨트롤러 구현**
```typescript
// tableManagementController.ts
export async function getTableData(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const {
page = 1,
size = 10,
search = {},
sortBy,
sortOrder = "asc",
} = req.body;
logger.info(`=== 테이블 데이터 조회 시작: ${tableName} ===`);
logger.info(`페이징: page=${page}, size=${size}`);
logger.info(`검색 조건:`, search);
if (!tableName) {
const response: ApiResponse<null> = {
success: false,
message: "테이블명이 필요합니다.",
error: { code: "MISSING_TABLE_NAME" },
};
res.status(400).json(response);
return;
}
const tableManagementService = new TableManagementService();
// 데이터 조회
const result = await tableManagementService.getTableData(tableName, {
page: parseInt(page),
size: parseInt(size),
search,
sortBy,
sortOrder,
});
const response: ApiResponse<any> = {
success: true,
message: "테이블 데이터를 성공적으로 조회했습니다.",
data: result,
};
res.status(200).json(response);
} catch (error) {
logger.error("테이블 데이터 조회 중 오류 발생:", error);
// 오류 응답 처리
}
}
```
**서비스 로직**
```typescript
// tableManagementService.ts
export class TableManagementService {
/**
* 테이블 데이터 조회 (페이징 + 검색)
*/
async getTableData(
tableName: string,
options: {
page: number;
size: number;
search?: Record<string, any>;
sortBy?: string;
sortOrder?: string;
}
): Promise<{
data: any[];
total: number;
page: number;
size: number;
totalPages: number;
}> {
try {
const { page, size, search = {}, sortBy, sortOrder = "asc" } = options;
const offset = (page - 1) * size;
// WHERE 조건 구성 (SQL 인젝션 방지)
let whereConditions: string[] = [];
let searchValues: any[] = [];
let paramIndex = 1;
if (search && Object.keys(search).length > 0) {
for (const [column, value] of Object.entries(search)) {
if (value !== null && value !== undefined && value !== "") {
// 안전한 컬럼명 검증
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
if (typeof value === "string") {
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
searchValues.push(`%${value}%`);
} else {
whereConditions.push(`${safeColumn} = $${paramIndex}`);
searchValues.push(value);
}
paramIndex++;
}
}
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// ORDER BY 조건 구성
let orderClause = "";
if (sortBy) {
const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, "");
const safeSortOrder =
sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC";
orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`;
}
// 안전한 테이블명 검증
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
// 전체 개수 조회
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
const countResult = await prisma.$queryRawUnsafe<any[]>(
countQuery,
...searchValues
);
const total = parseInt(countResult[0].count);
// 데이터 조회
const dataQuery = `
SELECT * FROM ${safeTableName}
${whereClause}
${orderClause}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const data = await prisma.$queryRawUnsafe<any[]>(
dataQuery,
...searchValues,
size,
offset
);
const totalPages = Math.ceil(total / size);
return {
data,
total,
page,
size,
totalPages,
};
} catch (error) {
logger.error(`테이블 데이터 조회 오류: ${tableName}`, error);
throw error;
}
}
}
```
#### 2. API 응답 형식
**성공 응답**
```json
{
"success": true,
"message": "테이블 데이터를 성공적으로 조회했습니다.",
"data": {
"data": [
{
"objid": 1,
"target_objid": 12345,
"approval_seq": "A001",
"regdate": "2025-09-03T05:39:14.000Z",
"status": "pending"
}
],
"total": 1,
"page": 1,
"size": 5,
"totalPages": 1
}
}
```
**요청 형식**
```json
{
"page": 1,
"size": 10,
"search": {
"approval_seq": "A001",
"status": "pending"
},
"sortBy": "regdate",
"sortOrder": "desc"
}
```
#### 3. 보안 및 성능 최적화
**SQL 인젝션 방지**
- 정규표현식을 통한 안전한 컬럼명/테이블명 검증
- 파라미터 바인딩 사용 (`$queryRawUnsafe` with parameters)
- 사용자 입력값 필터링
**성능 최적화**
- 페이징 처리로 대용량 데이터 대응
- COUNT 쿼리와 데이터 쿼리 분리
- 인덱스 기반 정렬 지원
**에러 처리**
- 상세한 로깅 시스템
- 사용자 친화적 오류 메시지
- HTTP 상태 코드 준수
#### 4. 도커 환경 통합
**개발 환경 설정**
```bash
# 백엔드 컨테이너 재빌드 (새 API 반영)
docker-compose -f docker/dev/docker-compose.backend.mac.yml down backend
docker-compose -f docker/dev/docker-compose.backend.mac.yml up --build -d backend
# API 테스트
curl -X POST http://localhost:8080/api/table-management/tables/approval/data \
-H "Content-Type: application/json" \
-d '{"page": 1, "size": 5}'
```
**환경 변수**
```env
# backend-node/.env
PORT=8080
DATABASE_URL=postgresql://postgres:password@localhost:5432/ilshin
NODE_ENV=development
```
### 1. 화면 관리 서비스
```typescript
@@ -2376,6 +2828,350 @@ export class TableTypeIntegrationService {
- **저장 기능**: 입력된 데이터를 수집하여 저장 처리
- **메뉴 연동**: 메뉴 클릭 시 할당된 인터랙티브 화면으로 자동 이동
### 🆕 6. 실시간 데이터 테이블 (2025.09 추가)
#### **InteractiveDataTable 컴포넌트**
실제 화면에서 동작하는 완전한 데이터 테이블 구현
**핵심 기능**
- **실시간 데이터 조회**: PostgreSQL 데이터베이스에서 직접 데이터 로드
- **페이지네이션**: 대용량 데이터 효율적 탐색 (페이지당 항목 수 설정 가능)
- **다중 검색 필터**: 웹타입별 맞춤형 검색 UI (text, number, date, select 등)
- **정렬 기능**: 컬럼별 오름차순/내림차순 정렬 지원
- **반응형 레이아웃**: 격자 시스템 기반 컬럼 너비 조정
**구현 코드**
```typescript
// InteractiveDataTable.tsx
export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
component,
className = "",
style = {},
}) => {
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [total, setTotal] = useState(0);
// 데이터 로드 함수
const loadData = useCallback(
async (page: number = 1, searchParams: Record<string, any> = {}) => {
if (!component.tableName) return;
setLoading(true);
try {
const result = await tableTypeApi.getTableData(component.tableName, {
page,
size: component.pagination?.pageSize || 10,
search: searchParams,
});
setData(result.data);
setTotal(result.total);
setTotalPages(result.totalPages);
setCurrentPage(result.page);
} catch (error) {
console.error("❌ 테이블 데이터 조회 실패:", error);
setData([]);
} finally {
setLoading(false);
}
},
[component.tableName, component.pagination?.pageSize]
);
// 검색 실행
const handleSearch = useCallback(() => {
loadData(1, searchValues);
}, [searchValues, loadData]);
return (
<div className={`bg-white ${className}`} style={style}>
{/* 헤더: 제목, 로딩 상태, 검색 버튼 */}
<div className="border-b bg-gray-50 px-4 py-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900">
{component.title || component.label}
{loading && (
<Badge variant="secondary" className="ml-2">
로딩중...
</Badge>
)}
</h3>
<div className="flex items-center space-x-2">
{component.showSearchButton && (
<Button size="sm" onClick={handleSearch} disabled={loading}>
{component.searchButtonText || "검색"}
</Button>
)}
</div>
</div>
{/* 검색 필터 */}
{component.filters?.length > 0 && (
<div className="mt-3 space-y-2">
<div className="text-xs font-medium text-gray-700">검색 필터</div>
<div
className="grid gap-2"
style={{
gridTemplateColumns: component.filters
.map((filter) => `${filter.gridColumns || 3}fr`)
.join(" "),
}}
>
{component.filters.map((filter) => renderSearchFilter(filter))}
</div>
</div>
)}
</div>
{/* 테이블 데이터 */}
<div className="flex-1 overflow-auto p-4">
{/* 헤더 행 */}
<div
className="gap-2 border-b pb-2 text-xs font-medium text-gray-700"
style={{
display: "grid",
gridTemplateColumns: component.columns
?.filter((col) => col.visible)
.map((col) => `${col.gridColumns || 2}fr`)
.join(" "),
}}
>
{component.columns
?.filter((col) => col.visible)
.map((column) => (
<div key={column.id} className="truncate">
{column.label}
</div>
))}
</div>
{/* 데이터 행들 */}
{loading ? (
<div className="py-8 text-center text-xs text-gray-500">
데이터를 불러오는 중...
</div>
) : data.length > 0 ? (
data.map((row, rowIndex) => (
<div
key={rowIndex}
className="gap-2 py-1 text-xs text-gray-600 hover:bg-gray-50"
style={{
display: "grid",
gridTemplateColumns: component.columns
?.filter((col) => col.visible)
.map((col) => `${col.gridColumns || 2}fr`)
.join(" "),
}}
>
{component.columns
?.filter((col) => col.visible)
.map((column) => (
<div key={column.id} className="truncate">
{formatCellValue(row[column.columnName], column)}
</div>
))}
</div>
))
) : (
<div className="py-8 text-center text-xs text-gray-500">
검색 결과가 없습니다
</div>
)}
</div>
{/* 페이지네이션 */}
{component.pagination?.enabled && totalPages > 1 && (
<div className="border-t bg-gray-50 px-4 py-2">
<div className="flex items-center justify-between text-xs text-gray-600">
{component.pagination.showPageInfo && (
<div>
총 {total.toLocaleString()}개 중{" "}
{((currentPage - 1) * pageSize + 1).toLocaleString()}-
{Math.min(currentPage * pageSize, total).toLocaleString()}
</div>
)}
<div className="flex items-center space-x-1">
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
이전
</Button>
<span className="px-2">
{currentPage} / {totalPages}
</span>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
다음
</Button>
</div>
</div>
</div>
)}
</div>
);
};
```
**데이터 포맷팅**
```typescript
// 셀 값 포맷팅
const formatCellValue = (value: any, column: DataTableColumn) => {
if (value === null || value === undefined) return "";
switch (column.widgetType) {
case "date":
if (value) {
try {
const date = new Date(value);
return date.toLocaleDateString("ko-KR");
} catch {
return value;
}
}
break;
case "datetime":
if (value) {
try {
const date = new Date(value);
return date.toLocaleString("ko-KR");
} catch {
return value;
}
}
break;
case "number":
case "decimal":
if (typeof value === "number") {
return value.toLocaleString();
}
break;
default:
return String(value);
}
return String(value);
};
```
**InteractiveScreenViewer 통합**
```typescript
// InteractiveScreenViewer.tsx
const renderInteractiveWidget = (comp: ComponentData) => {
// 데이터 테이블 컴포넌트 처리
if (comp.type === "datatable") {
return (
<InteractiveDataTable
component={comp as DataTableComponent}
className="h-full w-full"
style={{
width: "100%",
height: "100%",
}}
/>
);
}
// 기존 위젯 처리...
};
```
#### **사용자 경험 (UX)**
**1. 관리자 (화면 설계)**
1. 드래그앤드롭으로 데이터 테이블 컴포넌트 배치
2. 속성 패널에서 테이블 선택 및 컬럼 설정
3. 검색 필터 추가 및 페이지네이션 설정
4. 실시간 미리보기로 결과 확인
**2. 사용자 (실제 화면)**
1. 메뉴를 통해 할당된 화면 접근
2. 검색 필터로 원하는 데이터 필터링
3. 페이지네이션으로 대용량 데이터 탐색
4. 실시간 데이터 로딩 및 정렬
#### **🎨 Shadcn UI 기반 모던 디자인**
**핵심 컴포넌트**
- **Card**: 전체 테이블을 감싸는 메인 컨테이너
- **Table**: Shadcn Table 컴포넌트로 표준화된 테이블 UI
- **Badge**: 로딩 상태 및 필터 개수 표시
- **Button**: 일관된 액션 버튼 디자인
- **Separator**: 섹션 구분선
**디자인 특징**
```typescript
// 메인 카드 레이아웃
<Card className="h-full flex flex-col">
<CardHeader className="pb-3">
{/* 아이콘 + 제목 + 액션 버튼들 */}
<div className="flex items-center space-x-2">
<Database className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-lg">{title}</CardTitle>
{loading && (
<Badge variant="secondary" className="flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
로딩중...
</Badge>
)}
</div>
</CardHeader>
{/* Shadcn Table 사용 */}
<CardContent className="flex-1 overflow-hidden p-0">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead className="font-semibold">{column.label}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>{/* 로딩, 데이터, 빈 상태 처리 */}</TableBody>
</Table>
</CardContent>
</Card>
```
**시각적 개선사항**
- **아이콘 통합**: Lucide React 아이콘으로 시각적 일관성
- **로딩 애니메이션**: 스피너 아이콘으로 실시간 피드백
- **상태별 메시지**: 빈 데이터, 로딩, 에러 상태별 적절한 UI
- **호버 효과**: 테이블 행 호버 시 시각적 피드백
- **반응형 버튼**: 아이콘 + 텍스트 조합으로 명확한 액션 표시
#### **기술적 특징**
- **성능 최적화**: React.useMemo를 활용한 메모이제이션
- **보안**: SQL 인젝션 방지 및 입력값 검증
- **확장성**: 웹타입별 검색 필터 및 데이터 포맷터
- **반응형**: CSS Grid 기반 유연한 레이아웃
- **접근성**: Shadcn UI의 WAI-ARIA 표준 준수
- **타입 안전성**: TypeScript 완전 지원
## 🚀 다음 단계 계획
### 1. 웹타입별 상세 설정 기능 구현 (진행 예정)
@@ -2437,7 +3233,7 @@ interface SelectTypeConfig {
컴포넌트 데이터에 webTypeConfig 포함하여 레이아웃 저장 시 설정값도 함께 저장
### 2. 컴포넌트 그룹화 기능 (완료)
### 2. 컴포넌트 그룹화 기능 (완료)0
- [x] 여러 위젯을 컨테이너로 그룹화
- [x] 부모-자식 관계 설정(parentId)
@@ -2533,24 +3329,100 @@ interface SelectTypeConfig {
## 🔧 핵심 기술적 구현 패턴
### 1. 상태 관리 패턴
### 1. ⚡ 실시간 속성 편집 패턴 (핵심 표준)
#### 로컬 상태 + 실시간 업데이트 패턴
#### 완성된 로컬 상태 + 글로벌 상태 이중 관리 시스템
PropertiesPanel에서 사용하는 입력 필드 관리 방식:
**모든 속성 편집 컴포넌트의 표준 패턴:**
```typescript
// 1단계: 로컬 상태 정의 (실시간 표시용)
const [localInputs, setLocalInputs] = useState({
placeholder: selectedComponent?.placeholder || "",
// ... 기타 필드들
title: component.title || "",
placeholder: component.placeholder || "",
// 모든 입력 필드의 현재 값
});
// 입력 시 로컬 상태 + 실제 컴포넌트 동시 업데이트
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, fieldName: newValue }));
onUpdateProperty("fieldName", newValue);
}}
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]);
// 3단계: 실시간 입력 처리 - 즉시 반영
<Input
value={localInputs.title}
onChange={(e) => {
const newValue = e.target.value;
// 1) 로컬 상태 즉시 업데이트 (화면 반영)
setLocalInputs(prev => ({ ...prev, title: newValue }));
// 2) 글로벌 상태 업데이트 (데이터 저장)
onUpdateProperty("title", newValue);
}}
/>
<Checkbox
checked={localValues.showButton}
onCheckedChange={(checked) => {
// 1) 로컬 상태 즉시 업데이트
setLocalValues(prev => ({ ...prev, showButton: checked as boolean }));
// 2) 글로벌 상태 업데이트
onUpdateProperty("showButton", checked);
}}
/>
```
#### 동적 컴포넌트별 상태 관리 (ID 기반)
```typescript
// 컬럼별 개별 상태 관리
const [localColumnInputs, setLocalColumnInputs] = useState<
Record<string, string>
>({});
const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState<
Record<string, { visible: boolean; sortable: boolean; searchable: boolean }>
>({});
// 기존 값 보존하면서 새 항목만 추가
useEffect(() => {
setLocalColumnInputs((prev) => {
const newInputs = { ...prev };
component.columns?.forEach((col) => {
if (!(col.id in newInputs)) {
// 기존 입력값 보존
newInputs[col.id] = col.label;
}
});
return newInputs;
});
}, [component.columns]);
// 동적 입력 처리
<Input
value={
localColumnInputs[column.id] !== undefined
? localColumnInputs[column.id]
: column.label
}
onChange={(e) => {
const newValue = e.target.value;
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
updateColumn(column.id, { label: newValue });
}}
/>;
```
#### 스타일 속성 개별 업데이트 패턴
@@ -2565,6 +3437,18 @@ onUpdateProperty("style", { ...selectedComponent.style, newProp: value });
onUpdateProperty("style.labelFontSize", value);
```
### 📋 적용된 컴포넌트 목록
이 패턴이 완벽하게 적용된 컴포넌트들:
- **PropertiesPanel**: 기본 속성 편집
- **DataTableConfigPanel**: 데이터 테이블 상세 설정
- **DateTypeConfigPanel**: 날짜 타입 상세 설정
- **NumberTypeConfigPanel**: 숫자 타입 상세 설정
- **SelectTypeConfigPanel**: 선택박스 타입 상세 설정
- **TextTypeConfigPanel**: 텍스트 타입 상세 설정
- **기타 모든 웹타입별 설정 패널들**
### 2. 드래그앤드롭 패턴
#### 다중 컴포넌트 드래그 처리