Merge branch 'feature/screen-management' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -0,0 +1,591 @@
|
||||
# 리포트 디자이너 그리드 시스템 구현 계획
|
||||
|
||||
## 개요
|
||||
|
||||
현재 자유 배치 방식의 리포트 디자이너를 **그리드 기반 스냅 시스템**으로 전환합니다.
|
||||
안드로이드 홈 화면의 위젯 배치 방식과 유사하게, 모든 컴포넌트는 그리드에 맞춰서만 배치 및 크기 조절이 가능합니다.
|
||||
|
||||
## 목표
|
||||
|
||||
1. **정렬된 레이아웃**: 그리드 기반으로 요소들이 자동 정렬
|
||||
2. **Word/PDF 변환 개선**: 그리드 정보를 활용하여 정확한 문서 변환
|
||||
3. **직관적인 UI**: 그리드 시각화를 통한 명확한 배치 가이드
|
||||
4. **사용자 제어**: 그리드 크기, 가시성 등 사용자 설정 가능
|
||||
|
||||
## 핵심 개념
|
||||
|
||||
### 그리드 시스템
|
||||
|
||||
```typescript
|
||||
interface GridConfig {
|
||||
// 그리드 설정
|
||||
cellWidth: number; // 그리드 셀 너비 (px)
|
||||
cellHeight: number; // 그리드 셀 높이 (px)
|
||||
rows: number; // 세로 그리드 수 (계산값: pageHeight / cellHeight)
|
||||
columns: number; // 가로 그리드 수 (계산값: pageWidth / cellWidth)
|
||||
|
||||
// 표시 설정
|
||||
visible: boolean; // 그리드 표시 여부
|
||||
snapToGrid: boolean; // 그리드 스냅 활성화 여부
|
||||
|
||||
// 시각적 설정
|
||||
gridColor: string; // 그리드 선 색상
|
||||
gridOpacity: number; // 그리드 투명도 (0-1)
|
||||
}
|
||||
```
|
||||
|
||||
### 컴포넌트 위치/크기 (그리드 기반)
|
||||
|
||||
```typescript
|
||||
interface ComponentPosition {
|
||||
// 그리드 좌표 (셀 단위)
|
||||
gridX: number; // 시작 열 (0부터 시작)
|
||||
gridY: number; // 시작 행 (0부터 시작)
|
||||
gridWidth: number; // 차지하는 열 수
|
||||
gridHeight: number; // 차지하는 행 수
|
||||
|
||||
// 실제 픽셀 좌표 (계산값)
|
||||
x: number; // gridX * cellWidth
|
||||
y: number; // gridY * cellHeight
|
||||
width: number; // gridWidth * cellWidth
|
||||
height: number; // gridHeight * cellHeight
|
||||
}
|
||||
```
|
||||
|
||||
## 구현 단계
|
||||
|
||||
### Phase 1: 그리드 시스템 기반 구조
|
||||
|
||||
#### 1.1 타입 정의
|
||||
|
||||
- **파일**: `frontend/types/report.ts`
|
||||
- **내용**:
|
||||
- `GridConfig` 인터페이스 추가
|
||||
- `ComponentConfig`에 `gridX`, `gridY`, `gridWidth`, `gridHeight` 추가
|
||||
- `ReportPage`에 `gridConfig` 추가
|
||||
|
||||
#### 1.2 Context 확장
|
||||
|
||||
- **파일**: `frontend/contexts/ReportDesignerContext.tsx`
|
||||
- **내용**:
|
||||
- `gridConfig` 상태 추가
|
||||
- `updateGridConfig()` 함수 추가
|
||||
- `snapToGrid()` 유틸리티 함수 추가
|
||||
- 컴포넌트 추가/이동/리사이즈 시 그리드 스냅 적용
|
||||
|
||||
#### 1.3 그리드 계산 유틸리티
|
||||
|
||||
- **파일**: `frontend/lib/utils/gridUtils.ts` (신규)
|
||||
- **내용**:
|
||||
|
||||
```typescript
|
||||
// 픽셀 좌표 → 그리드 좌표 변환
|
||||
export function pixelToGrid(pixel: number, cellSize: number): number;
|
||||
|
||||
// 그리드 좌표 → 픽셀 좌표 변환
|
||||
export function gridToPixel(grid: number, cellSize: number): number;
|
||||
|
||||
// 컴포넌트 위치/크기를 그리드에 스냅
|
||||
export function snapComponentToGrid(
|
||||
component: ComponentConfig,
|
||||
gridConfig: GridConfig
|
||||
): ComponentConfig;
|
||||
|
||||
// 그리드 충돌 감지
|
||||
export function detectGridCollision(
|
||||
component: ComponentConfig,
|
||||
otherComponents: ComponentConfig[]
|
||||
): boolean;
|
||||
```
|
||||
|
||||
### Phase 2: 그리드 시각화
|
||||
|
||||
#### 2.1 그리드 레이어 컴포넌트
|
||||
|
||||
- **파일**: `frontend/components/report/designer/GridLayer.tsx` (신규)
|
||||
- **내용**:
|
||||
- Canvas 위에 그리드 선 렌더링
|
||||
- SVG 또는 Canvas API 사용
|
||||
- 그리드 크기/색상/투명도 적용
|
||||
- 줌/스크롤 시에도 정확한 위치 유지
|
||||
|
||||
```tsx
|
||||
interface GridLayerProps {
|
||||
gridConfig: GridConfig;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
}
|
||||
|
||||
export function GridLayer({
|
||||
gridConfig,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
}: GridLayerProps) {
|
||||
if (!gridConfig.visible) return null;
|
||||
|
||||
// SVG로 그리드 선 렌더링
|
||||
return (
|
||||
<svg className="absolute inset-0 pointer-events-none">
|
||||
{/* 세로 선 */}
|
||||
{Array.from({ length: gridConfig.columns + 1 }).map((_, i) => (
|
||||
<line
|
||||
key={`v-${i}`}
|
||||
x1={i * gridConfig.cellWidth}
|
||||
y1={0}
|
||||
x2={i * gridConfig.cellWidth}
|
||||
y2={pageHeight}
|
||||
stroke={gridConfig.gridColor}
|
||||
strokeOpacity={gridConfig.opacity}
|
||||
/>
|
||||
))}
|
||||
{/* 가로 선 */}
|
||||
{Array.from({ length: gridConfig.rows + 1 }).map((_, i) => (
|
||||
<line
|
||||
key={`h-${i}`}
|
||||
x1={0}
|
||||
y1={i * gridConfig.cellHeight}
|
||||
x2={pageWidth}
|
||||
y2={i * gridConfig.cellHeight}
|
||||
stroke={gridConfig.gridColor}
|
||||
strokeOpacity={gridConfig.opacity}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Canvas 통합
|
||||
|
||||
- **파일**: `frontend/components/report/designer/ReportDesignerCanvas.tsx`
|
||||
- **내용**:
|
||||
- `<GridLayer />` 추가
|
||||
- 컴포넌트 렌더링 시 그리드 기반 위치 사용
|
||||
|
||||
### Phase 3: 드래그 앤 드롭 스냅
|
||||
|
||||
#### 3.1 드래그 시 그리드 스냅
|
||||
|
||||
- **파일**: `frontend/components/report/designer/ReportDesignerCanvas.tsx`
|
||||
- **내용**:
|
||||
- `useDrop` 훅 수정
|
||||
- 드롭 위치를 그리드에 스냅
|
||||
- 실시간 스냅 가이드 표시
|
||||
|
||||
```typescript
|
||||
const [, drop] = useDrop({
|
||||
accept: ["TEXT", "LABEL", "TABLE", "SIGNATURE", "STAMP"],
|
||||
drop: (item: any, monitor) => {
|
||||
const offset = monitor.getClientOffset();
|
||||
if (!offset) return;
|
||||
|
||||
// 캔버스 상대 좌표 계산
|
||||
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!canvasRect) return;
|
||||
|
||||
let x = offset.x - canvasRect.left;
|
||||
let y = offset.y - canvasRect.top;
|
||||
|
||||
// 그리드 스냅 적용
|
||||
if (gridConfig.snapToGrid) {
|
||||
const gridX = Math.round(x / gridConfig.cellWidth);
|
||||
const gridY = Math.round(y / gridConfig.cellHeight);
|
||||
x = gridX * gridConfig.cellWidth;
|
||||
y = gridY * gridConfig.cellHeight;
|
||||
}
|
||||
|
||||
// 컴포넌트 추가
|
||||
addComponent({ type: item.type, x, y });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### 3.2 리사이즈 시 그리드 스냅
|
||||
|
||||
- **파일**: `frontend/components/report/designer/ComponentWrapper.tsx`
|
||||
- **내용**:
|
||||
- `react-resizable` 또는 `react-rnd`의 `snap` 설정 활용
|
||||
- 리사이즈 핸들 드래그 시 그리드 단위로만 크기 조절
|
||||
|
||||
```typescript
|
||||
<Rnd
|
||||
position={{ x: component.x, y: component.y }}
|
||||
size={{ width: component.width, height: component.height }}
|
||||
onDragStop={(e, d) => {
|
||||
let newX = d.x;
|
||||
let newY = d.y;
|
||||
|
||||
if (gridConfig.snapToGrid) {
|
||||
const gridX = Math.round(newX / gridConfig.cellWidth);
|
||||
const gridY = Math.round(newY / gridConfig.cellHeight);
|
||||
newX = gridX * gridConfig.cellWidth;
|
||||
newY = gridY * gridConfig.cellHeight;
|
||||
}
|
||||
|
||||
updateComponent(component.id, { x: newX, y: newY });
|
||||
}}
|
||||
onResizeStop={(e, direction, ref, delta, position) => {
|
||||
let newWidth = parseInt(ref.style.width);
|
||||
let newHeight = parseInt(ref.style.height);
|
||||
|
||||
if (gridConfig.snapToGrid) {
|
||||
const gridWidth = Math.round(newWidth / gridConfig.cellWidth);
|
||||
const gridHeight = Math.round(newHeight / gridConfig.cellHeight);
|
||||
newWidth = gridWidth * gridConfig.cellWidth;
|
||||
newHeight = gridHeight * gridConfig.cellHeight;
|
||||
}
|
||||
|
||||
updateComponent(component.id, {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
...position,
|
||||
});
|
||||
}}
|
||||
grid={
|
||||
gridConfig.snapToGrid
|
||||
? [gridConfig.cellWidth, gridConfig.cellHeight]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### Phase 4: 그리드 설정 UI
|
||||
|
||||
#### 4.1 그리드 설정 패널
|
||||
|
||||
- **파일**: `frontend/components/report/designer/GridSettingsPanel.tsx` (신규)
|
||||
- **내용**:
|
||||
- 그리드 크기 조절 (cellWidth, cellHeight)
|
||||
- 그리드 표시/숨김 토글
|
||||
- 스냅 활성화/비활성화 토글
|
||||
- 그리드 색상/투명도 조절
|
||||
|
||||
```tsx
|
||||
export function GridSettingsPanel() {
|
||||
const { gridConfig, updateGridConfig } = useReportDesigner();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">그리드 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 그리드 표시 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>그리드 표시</Label>
|
||||
<Switch
|
||||
checked={gridConfig.visible}
|
||||
onCheckedChange={(visible) => updateGridConfig({ visible })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 스냅 활성화 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>그리드 스냅</Label>
|
||||
<Switch
|
||||
checked={gridConfig.snapToGrid}
|
||||
onCheckedChange={(snapToGrid) => updateGridConfig({ snapToGrid })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 셀 크기 */}
|
||||
<div className="space-y-2">
|
||||
<Label>셀 너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={gridConfig.cellWidth}
|
||||
onChange={(e) =>
|
||||
updateGridConfig({ cellWidth: parseInt(e.target.value) })
|
||||
}
|
||||
min={10}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>셀 높이 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={gridConfig.cellHeight}
|
||||
onChange={(e) =>
|
||||
updateGridConfig({ cellHeight: parseInt(e.target.value) })
|
||||
}
|
||||
min={10}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 프리셋 */}
|
||||
<div className="space-y-2">
|
||||
<Label>프리셋</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
const presets: Record<
|
||||
string,
|
||||
{ cellWidth: number; cellHeight: number }
|
||||
> = {
|
||||
fine: { cellWidth: 10, cellHeight: 10 },
|
||||
medium: { cellWidth: 20, cellHeight: 20 },
|
||||
coarse: { cellWidth: 50, cellHeight: 50 },
|
||||
};
|
||||
updateGridConfig(presets[value]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="그리드 크기 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fine">세밀 (10x10)</SelectItem>
|
||||
<SelectItem value="medium">중간 (20x20)</SelectItem>
|
||||
<SelectItem value="coarse">넓음 (50x50)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 툴바에 그리드 토글 추가
|
||||
|
||||
- **파일**: `frontend/components/report/designer/ReportDesignerToolbar.tsx`
|
||||
- **내용**:
|
||||
- 그리드 표시/숨김 버튼
|
||||
- 그리드 설정 모달 열기 버튼
|
||||
- 키보드 단축키 (`G` 키로 그리드 토글)
|
||||
|
||||
### Phase 5: Word 변환 개선
|
||||
|
||||
#### 5.1 그리드 기반 레이아웃 변환
|
||||
|
||||
- **파일**: `frontend/components/report/designer/ReportPreviewModal.tsx`
|
||||
- **내용**:
|
||||
- 그리드 정보를 활용하여 더 정확한 테이블 레이아웃 생성
|
||||
- 그리드 행/열을 Word 테이블의 행/열로 매핑
|
||||
|
||||
```typescript
|
||||
const handleDownloadWord = async () => {
|
||||
// 그리드 기반으로 컴포넌트 배치 맵 생성
|
||||
const gridMap: (ComponentConfig | null)[][] = Array(gridConfig.rows)
|
||||
.fill(null)
|
||||
.map(() => Array(gridConfig.columns).fill(null));
|
||||
|
||||
// 각 컴포넌트를 그리드 맵에 배치
|
||||
for (const component of components) {
|
||||
const gridX = Math.round(component.x / gridConfig.cellWidth);
|
||||
const gridY = Math.round(component.y / gridConfig.cellHeight);
|
||||
const gridWidth = Math.round(component.width / gridConfig.cellWidth);
|
||||
const gridHeight = Math.round(component.height / gridConfig.cellHeight);
|
||||
|
||||
// 컴포넌트가 차지하는 모든 셀에 참조 저장
|
||||
for (let y = gridY; y < gridY + gridHeight; y++) {
|
||||
for (let x = gridX; x < gridX + gridWidth; x++) {
|
||||
if (y < gridConfig.rows && x < gridConfig.columns) {
|
||||
gridMap[y][x] = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 그리드 맵을 Word 테이블로 변환
|
||||
const tableRows: TableRow[] = [];
|
||||
|
||||
for (let y = 0; y < gridConfig.rows; y++) {
|
||||
const cells: TableCell[] = [];
|
||||
let x = 0;
|
||||
|
||||
while (x < gridConfig.columns) {
|
||||
const component = gridMap[y][x];
|
||||
|
||||
if (!component) {
|
||||
// 빈 셀
|
||||
cells.push(new TableCell({ children: [new Paragraph("")] }));
|
||||
x++;
|
||||
} else {
|
||||
// 컴포넌트 셀
|
||||
const gridWidth = Math.round(component.width / gridConfig.cellWidth);
|
||||
const gridHeight = Math.round(component.height / gridConfig.cellHeight);
|
||||
|
||||
const cell = createTableCell(component, gridWidth, gridHeight);
|
||||
if (cell) cells.push(cell);
|
||||
|
||||
x += gridWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (cells.length > 0) {
|
||||
tableRows.push(new TableRow({ children: cells }));
|
||||
}
|
||||
}
|
||||
|
||||
// ... Word 문서 생성
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 6: 데이터 마이그레이션
|
||||
|
||||
#### 6.1 기존 레이아웃 자동 변환
|
||||
|
||||
- **파일**: `frontend/lib/utils/layoutMigration.ts` (신규)
|
||||
- **내용**:
|
||||
- 기존 절대 위치 데이터를 그리드 기반으로 변환
|
||||
- 가장 가까운 그리드 셀에 스냅
|
||||
- 마이그레이션 로그 생성
|
||||
|
||||
```typescript
|
||||
export function migrateLayoutToGrid(
|
||||
layout: ReportLayoutConfig,
|
||||
gridConfig: GridConfig
|
||||
): ReportLayoutConfig {
|
||||
return {
|
||||
...layout,
|
||||
pages: layout.pages.map((page) => ({
|
||||
...page,
|
||||
gridConfig,
|
||||
components: page.components.map((component) => {
|
||||
// 픽셀 좌표를 그리드 좌표로 변환
|
||||
const gridX = Math.round(component.x / gridConfig.cellWidth);
|
||||
const gridY = Math.round(component.y / gridConfig.cellHeight);
|
||||
const gridWidth = Math.max(
|
||||
1,
|
||||
Math.round(component.width / gridConfig.cellWidth)
|
||||
);
|
||||
const gridHeight = Math.max(
|
||||
1,
|
||||
Math.round(component.height / gridConfig.cellHeight)
|
||||
);
|
||||
|
||||
return {
|
||||
...component,
|
||||
gridX,
|
||||
gridY,
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
x: gridX * gridConfig.cellWidth,
|
||||
y: gridY * gridConfig.cellHeight,
|
||||
width: gridWidth * gridConfig.cellWidth,
|
||||
height: gridHeight * gridConfig.cellHeight,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 마이그레이션 UI
|
||||
|
||||
- **파일**: `frontend/components/report/designer/MigrationModal.tsx` (신규)
|
||||
- **내용**:
|
||||
- 기존 리포트 로드 시 마이그레이션 필요 여부 체크
|
||||
- 마이그레이션 전/후 미리보기
|
||||
- 사용자 확인 후 적용
|
||||
|
||||
## 데이터베이스 스키마 변경
|
||||
|
||||
### report_layout_pages 테이블
|
||||
|
||||
```sql
|
||||
ALTER TABLE report_layout_pages
|
||||
ADD COLUMN grid_cell_width INTEGER DEFAULT 20,
|
||||
ADD COLUMN grid_cell_height INTEGER DEFAULT 20,
|
||||
ADD COLUMN grid_visible BOOLEAN DEFAULT true,
|
||||
ADD COLUMN grid_snap_enabled BOOLEAN DEFAULT true,
|
||||
ADD COLUMN grid_color VARCHAR(7) DEFAULT '#e5e7eb',
|
||||
ADD COLUMN grid_opacity DECIMAL(3,2) DEFAULT 0.5;
|
||||
```
|
||||
|
||||
### report_layout_components 테이블
|
||||
|
||||
```sql
|
||||
ALTER TABLE report_layout_components
|
||||
ADD COLUMN grid_x INTEGER,
|
||||
ADD COLUMN grid_y INTEGER,
|
||||
ADD COLUMN grid_width INTEGER,
|
||||
ADD COLUMN grid_height INTEGER;
|
||||
|
||||
-- 기존 데이터 마이그레이션
|
||||
UPDATE report_layout_components
|
||||
SET
|
||||
grid_x = ROUND(position_x / 20.0),
|
||||
grid_y = ROUND(position_y / 20.0),
|
||||
grid_width = GREATEST(1, ROUND(width / 20.0)),
|
||||
grid_height = GREATEST(1, ROUND(height / 20.0))
|
||||
WHERE grid_x IS NULL;
|
||||
```
|
||||
|
||||
## 테스트 계획
|
||||
|
||||
### 단위 테스트
|
||||
|
||||
- `gridUtils.ts`의 모든 함수 테스트
|
||||
- 그리드 좌표 ↔ 픽셀 좌표 변환 정확성
|
||||
- 충돌 감지 로직
|
||||
|
||||
### 통합 테스트
|
||||
|
||||
- 드래그 앤 드롭 시 그리드 스냅 동작
|
||||
- 리사이즈 시 그리드 스냅 동작
|
||||
- 그리드 크기 변경 시 컴포넌트 재배치
|
||||
|
||||
### E2E 테스트
|
||||
|
||||
- 새 리포트 생성 및 그리드 설정
|
||||
- 기존 리포트 마이그레이션
|
||||
- Word 다운로드 시 레이아웃 정확성
|
||||
|
||||
## 예상 개발 일정
|
||||
|
||||
- **Phase 1**: 그리드 시스템 기반 구조 (2일)
|
||||
- **Phase 2**: 그리드 시각화 (1일)
|
||||
- **Phase 3**: 드래그 앤 드롭 스냅 (2일)
|
||||
- **Phase 4**: 그리드 설정 UI (1일)
|
||||
- **Phase 5**: Word 변환 개선 (2일)
|
||||
- **Phase 6**: 데이터 마이그레이션 (1일)
|
||||
- **테스트 및 디버깅**: (2일)
|
||||
|
||||
**총 예상 기간**: 11일
|
||||
|
||||
## 기술적 고려사항
|
||||
|
||||
### 성능 최적화
|
||||
|
||||
- 그리드 렌더링: SVG 대신 Canvas API 고려 (많은 셀의 경우)
|
||||
- 메모이제이션: 그리드 계산 결과 캐싱
|
||||
- 가상화: 큰 페이지에서 보이는 영역만 렌더링
|
||||
|
||||
### 사용자 경험
|
||||
|
||||
- 실시간 스냅 가이드: 드래그 중 스냅될 위치 미리 표시
|
||||
- 키보드 단축키: 방향키로 그리드 단위 이동, Shift+방향키로 픽셀 단위 미세 조정
|
||||
- 언두/리두: 그리드 스냅 적용 전/후 상태 저장
|
||||
|
||||
### 하위 호환성
|
||||
|
||||
- 기존 리포트는 자동 마이그레이션 제공
|
||||
- 마이그레이션 옵션: 자동 / 수동 선택 가능
|
||||
- 레거시 모드: 그리드 없이 자유 배치 가능 (옵션)
|
||||
|
||||
## 추가 기능 (향후 확장)
|
||||
|
||||
### 스마트 가이드
|
||||
|
||||
- 다른 컴포넌트와 정렬 시 가이드 라인 표시
|
||||
- 균등 간격 가이드
|
||||
|
||||
### 그리드 템플릿
|
||||
|
||||
- 자주 사용하는 그리드 레이아웃 템플릿 제공
|
||||
- 문서 종류별 프리셋 (계약서, 보고서, 송장 등)
|
||||
|
||||
### 그리드 병합
|
||||
|
||||
- 여러 그리드 셀을 하나로 병합
|
||||
- 복잡한 레이아웃 지원
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- Android Home Screen Widget System
|
||||
- Microsoft Word Table Layout
|
||||
- CSS Grid Layout
|
||||
- Figma Auto Layout
|
||||
@@ -0,0 +1,358 @@
|
||||
# 리포트 관리 시스템 구현 진행 상황
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
동적 리포트 디자이너 시스템 구현
|
||||
|
||||
- 사용자가 드래그 앤 드롭으로 리포트 레이아웃 설계
|
||||
- SQL 쿼리 연동으로 실시간 데이터 표시
|
||||
- 미리보기 및 인쇄 기능
|
||||
|
||||
---
|
||||
|
||||
## 완료된 작업 ✅
|
||||
|
||||
### 1. 데이터베이스 설계 및 구축
|
||||
|
||||
- [x] `report_template` 테이블 생성 (18개 초기 템플릿)
|
||||
- [x] `report_master` 테이블 생성 (리포트 메타 정보)
|
||||
- [x] `report_layout` 테이블 생성 (레이아웃 JSON)
|
||||
- [x] `report_query` 테이블 생성 (쿼리 정의)
|
||||
|
||||
**파일**: `db/report_schema.sql`, `db/report_query_schema.sql`
|
||||
|
||||
### 2. 백엔드 API 구현
|
||||
|
||||
- [x] 리포트 CRUD API (생성, 조회, 수정, 삭제)
|
||||
- [x] 템플릿 조회 API
|
||||
- [x] 레이아웃 저장/조회 API
|
||||
- [x] 쿼리 실행 API (파라미터 지원)
|
||||
- [x] 리포트 복사 API
|
||||
- [x] Raw SQL 기반 구현 (Prisma 대신 pg 사용)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `backend-node/src/types/report.ts`
|
||||
- `backend-node/src/services/reportService.ts`
|
||||
- `backend-node/src/controllers/reportController.ts`
|
||||
- `backend-node/src/routes/reportRoutes.ts`
|
||||
|
||||
### 3. 프론트엔드 - 리포트 목록 페이지
|
||||
|
||||
- [x] 리포트 리스트 조회 및 표시
|
||||
- [x] 검색 기능
|
||||
- [x] 페이지네이션
|
||||
- [x] 새 리포트 생성 (디자이너로 이동)
|
||||
- [x] 수정/복사/삭제 액션 버튼
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/app/(main)/admin/report/page.tsx`
|
||||
- `frontend/components/report/ReportListTable.tsx`
|
||||
- `frontend/hooks/useReportList.ts`
|
||||
|
||||
### 4. 프론트엔드 - 리포트 디자이너 기본 구조
|
||||
|
||||
- [x] Context 기반 상태 관리 (`ReportDesignerContext`)
|
||||
- [x] 툴바 (저장, 미리보기, 초기화, 뒤로가기)
|
||||
- [x] 3단 레이아웃 (좌측 팔레트 / 중앙 캔버스 / 우측 속성)
|
||||
- [x] "new" 리포트 처리 (저장 시 생성)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/contexts/ReportDesignerContext.tsx`
|
||||
- `frontend/app/(main)/admin/report/designer/[reportId]/page.tsx`
|
||||
- `frontend/components/report/designer/ReportDesignerToolbar.tsx`
|
||||
|
||||
### 5. 컴포넌트 팔레트 및 캔버스
|
||||
|
||||
- [x] 드래그 가능한 컴포넌트 목록 (텍스트, 레이블, 테이블)
|
||||
- [x] 드래그 앤 드롭으로 캔버스에 컴포넌트 배치
|
||||
- [x] 컴포넌트 이동 (드래그)
|
||||
- [x] 컴포넌트 크기 조절 (리사이즈 핸들)
|
||||
- [x] 컴포넌트 선택 및 삭제
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/ComponentPalette.tsx`
|
||||
- `frontend/components/report/designer/ReportDesignerCanvas.tsx`
|
||||
- `frontend/components/report/designer/CanvasComponent.tsx`
|
||||
|
||||
### 6. 쿼리 관리 시스템
|
||||
|
||||
- [x] 쿼리 추가/수정/삭제 (마스터/디테일)
|
||||
- [x] SQL 파라미터 자동 감지 ($1, $2 등)
|
||||
- [x] 파라미터 타입 선택 (text, number, date)
|
||||
- [x] 파라미터 입력값 검증
|
||||
- [x] 쿼리 실행 및 결과 표시
|
||||
- [x] "new" 리포트에서도 쿼리 실행 가능
|
||||
- [x] 실행 결과를 Context에 저장
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/QueryManager.tsx`
|
||||
- `frontend/contexts/ReportDesignerContext.tsx` (QueryResult 관리)
|
||||
|
||||
### 7. 데이터 바인딩 시스템
|
||||
|
||||
- [x] 속성 패널에서 컴포넌트-쿼리 연결
|
||||
- [x] 텍스트/레이블: 쿼리 + 필드 선택
|
||||
- [x] 테이블: 쿼리 선택 (모든 필드 자동 표시)
|
||||
- [x] 캔버스에서 실제 데이터 표시 (바인딩된 필드의 값)
|
||||
- [x] 실행 결과가 없으면 `{필드명}` 표시
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/ReportDesignerRightPanel.tsx`
|
||||
- `frontend/components/report/designer/CanvasComponent.tsx`
|
||||
|
||||
### 8. 미리보기 및 내보내기
|
||||
|
||||
- [x] 미리보기 모달
|
||||
- [x] 실제 쿼리 데이터로 렌더링
|
||||
- [x] 편집용 UI 제거 (순수 데이터만 표시)
|
||||
- [x] 브라우저 인쇄 기능
|
||||
- [x] PDF 다운로드 (브라우저 네이티브 인쇄 기능)
|
||||
- [x] WORD 다운로드 (docx 라이브러리)
|
||||
- [x] 파일명 자동 생성 (리포트명\_날짜)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/ReportPreviewModal.tsx`
|
||||
|
||||
**사용 라이브러리**:
|
||||
|
||||
- `docx`: WORD 문서 생성 (PDF는 브라우저 기본 기능 사용)
|
||||
|
||||
### 9. 템플릿 시스템
|
||||
|
||||
- [x] 시스템 템플릿 적용 (발주서, 청구서, 기본)
|
||||
- [x] 템플릿별 기본 컴포넌트 자동 배치
|
||||
- [x] 템플릿별 기본 쿼리 자동 생성
|
||||
- [x] 사용자 정의 템플릿 저장 기능
|
||||
- [x] 사용자 정의 템플릿 목록 조회
|
||||
- [x] 사용자 정의 템플릿 삭제
|
||||
- [x] 사용자 정의 템플릿 적용 (백엔드 연동)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/contexts/ReportDesignerContext.tsx` (템플릿 적용 로직)
|
||||
- `frontend/components/report/designer/TemplatePalette.tsx`
|
||||
- `frontend/components/report/designer/SaveAsTemplateModal.tsx`
|
||||
- `backend-node/src/services/reportService.ts` (createTemplateFromLayout)
|
||||
|
||||
### 10. 외부 DB 연동
|
||||
|
||||
- [x] 쿼리별 외부 DB 연결 선택
|
||||
- [x] 외부 DB 연결 목록 조회 API
|
||||
- [x] 쿼리 실행 시 외부 DB 지원
|
||||
- [x] 내부/외부 DB 선택 UI
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/QueryManager.tsx`
|
||||
- `backend-node/src/services/reportService.ts` (executeQuery with external DB)
|
||||
|
||||
### 11. 컴포넌트 스타일링
|
||||
|
||||
- [x] 폰트 크기 설정
|
||||
- [x] 폰트 색상 설정 (컬러피커)
|
||||
- [x] 폰트 굵기 (보통/굵게)
|
||||
- [x] 텍스트 정렬 (좌/중/우)
|
||||
- [x] 배경색 설정 (투명 옵션 포함)
|
||||
- [x] 테두리 설정 (두께, 색상)
|
||||
- [x] 캔버스 및 미리보기에 스타일 반영
|
||||
|
||||
**파일**:
|
||||
|
||||
- `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: 추가 컴포넌트 ✅ 완료!
|
||||
|
||||
1. **이미지 컴포넌트** ✅
|
||||
|
||||
- [x] 파일 업로드 (multer, 10MB 제한)
|
||||
- [x] 회사별 디렉토리 분리 저장
|
||||
- [x] 맞춤 방식 (contain/cover/fill/none)
|
||||
- [x] CORS 설정으로 이미지 로딩
|
||||
- [x] 캔버스 및 미리보기 렌더링
|
||||
- 로고, 서명, 도장 등에 활용
|
||||
|
||||
2. **구분선 컴포넌트 (Divider)** ✅
|
||||
|
||||
- [x] 가로/세로 방향 선택
|
||||
- [x] 선 두께 (lineWidth) 독립 속성
|
||||
- [x] 선 색상 (lineColor) 독립 속성
|
||||
- [x] 선 스타일 (solid/dashed/dotted/double)
|
||||
- [x] 캔버스 및 미리보기 렌더링
|
||||
|
||||
**파일**:
|
||||
- `backend-node/src/controllers/reportController.ts` (uploadImage)
|
||||
- `backend-node/src/routes/reportRoutes.ts` (multer 설정)
|
||||
- `frontend/types/report.ts` (이미지/구분선 속성)
|
||||
- `frontend/components/report/designer/ComponentPalette.tsx`
|
||||
- `frontend/components/report/designer/CanvasComponent.tsx`
|
||||
- `frontend/components/report/designer/ReportDesignerRightPanel.tsx`
|
||||
- `frontend/components/report/designer/ReportPreviewModal.tsx`
|
||||
- `frontend/lib/api/client.ts` (getFullImageUrl)
|
||||
|
||||
3. **차트 컴포넌트** (선택사항) ⬅️ 다음 권장 작업
|
||||
- 막대 차트
|
||||
- 선 차트
|
||||
- 원형 차트
|
||||
- 쿼리 데이터 연동
|
||||
|
||||
### Phase 2: 고급 기능
|
||||
|
||||
4. **조건부 서식**
|
||||
|
||||
- 특정 조건에 따른 스타일 변경
|
||||
- 값 범위에 따른 색상 표시
|
||||
- 수식 기반 표시/숨김
|
||||
|
||||
5. **쿼리 관리 개선**
|
||||
- 쿼리 미리보기 개선 (테이블 형태)
|
||||
- 쿼리 저장/불러오기
|
||||
- 쿼리 템플릿
|
||||
|
||||
### Phase 3: 성능 및 보안
|
||||
|
||||
6. **성능 최적화**
|
||||
|
||||
- 쿼리 결과 캐싱
|
||||
- 대용량 데이터 페이징
|
||||
- 렌더링 최적화
|
||||
- 이미지 레이지 로딩
|
||||
|
||||
7. **권한 관리**
|
||||
- 리포트별 접근 권한
|
||||
- 수정 권한 분리
|
||||
- 템플릿 공유
|
||||
- 사용자별 리포트 목록 필터링
|
||||
|
||||
---
|
||||
|
||||
## 기술 스택
|
||||
|
||||
### 백엔드
|
||||
|
||||
- Node.js + TypeScript
|
||||
- Express.js
|
||||
- PostgreSQL (raw SQL)
|
||||
- pg (node-postgres)
|
||||
|
||||
### 프론트엔드
|
||||
|
||||
- Next.js 14 (App Router)
|
||||
- React 18
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Shadcn UI
|
||||
- react-dnd (드래그 앤 드롭)
|
||||
|
||||
---
|
||||
|
||||
## 주요 아키텍처 결정
|
||||
|
||||
### 1. Context API 사용
|
||||
|
||||
- 리포트 디자이너의 복잡한 상태를 Context로 중앙 관리
|
||||
- 컴포넌트 간 prop drilling 방지
|
||||
|
||||
### 2. Raw SQL 사용
|
||||
|
||||
- Prisma 대신 직접 SQL 작성
|
||||
- 복잡한 쿼리와 트랜잭션 처리에 유리
|
||||
- 데이터베이스 제어 수준 향상
|
||||
|
||||
### 3. JSON 기반 레이아웃 저장
|
||||
|
||||
- 레이아웃을 JSONB로 DB에 저장
|
||||
- 버전 관리 용이
|
||||
- 유연한 스키마
|
||||
|
||||
### 4. 쿼리 실행 결과 메모리 관리
|
||||
|
||||
- Context에 쿼리 결과 저장
|
||||
- 컴포넌트에서 실시간 참조
|
||||
- 불필요한 API 호출 방지
|
||||
|
||||
---
|
||||
|
||||
## 참고 문서
|
||||
|
||||
- [리포트*관리*시스템\_설계.md](./리포트_관리_시스템_설계.md) - 초기 설계 문서
|
||||
- [레포트드자이너.html](../레포트드자이너.html) - 참조 프로토타입
|
||||
|
||||
---
|
||||
|
||||
## 다음 작업: 리포트 복사/삭제 테스트 및 검증
|
||||
|
||||
### 테스트 항목
|
||||
|
||||
1. **복사 기능 테스트**
|
||||
|
||||
- 리포트 복사 버튼 클릭
|
||||
- 복사된 리포트명 확인 (원본명 + "\_copy")
|
||||
- 복사된 리포트의 레이아웃 확인
|
||||
- 복사된 리포트의 쿼리 확인
|
||||
- 목록 자동 새로고침 확인
|
||||
|
||||
2. **삭제 기능 테스트**
|
||||
|
||||
- 삭제 버튼 클릭 시 확인 다이얼로그 표시
|
||||
- 취소 버튼 동작 확인
|
||||
- 삭제 실행 후 목록에서 제거 확인
|
||||
- Toast 메시지 표시 확인
|
||||
|
||||
3. **에러 처리 테스트**
|
||||
- 존재하지 않는 리포트 삭제 시도
|
||||
- 네트워크 오류 시 Toast 메시지
|
||||
- 로딩 중 버튼 비활성화 확인
|
||||
|
||||
### 추가 개선 사항
|
||||
|
||||
- [ ] 컴포넌트 복사 기능 (Ctrl+C/Ctrl+V)
|
||||
- [ ] 다중 선택 및 정렬 기능
|
||||
- [ ] 실행 취소/다시 실행 (Undo/Redo)
|
||||
- [ ] 사용자 정의 템플릿 저장
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2025-10-01
|
||||
**작성자**: AI Assistant
|
||||
**상태**: 이미지 & 구분선 컴포넌트 완료 (기본 컴포넌트 완료, 약 99% 완료)
|
||||
@@ -0,0 +1,679 @@
|
||||
# 리포트 관리 시스템 설계
|
||||
|
||||
## 1. 프로젝트 개요
|
||||
|
||||
### 1.1 목적
|
||||
|
||||
ERP 시스템에서 다양한 업무 문서(발주서, 청구서, 거래명세서 등)를 동적으로 디자인하고 관리할 수 있는 리포트 관리 시스템을 구축합니다.
|
||||
|
||||
### 1.2 주요 기능
|
||||
|
||||
- 리포트 목록 조회 및 관리
|
||||
- 드래그 앤 드롭 기반 리포트 디자이너
|
||||
- 템플릿 관리 (기본 템플릿 + 사용자 정의 템플릿)
|
||||
- 쿼리 관리 (마스터/디테일)
|
||||
- 외부 DB 연동
|
||||
- 인쇄 및 내보내기 (PDF, WORD)
|
||||
- 미리보기 기능
|
||||
|
||||
## 2. 화면 구성
|
||||
|
||||
### 2.1 리포트 목록 화면 (`/admin/report`)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 리포트 관리 [+ 새 리포트] │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 검색: [____________________] [검색] [초기화] │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ No │ 리포트명 │ 작성자 │ 수정일 │ 액션 │
|
||||
├────┼──────────────┼────────┼───────────┼────────────────────────┤
|
||||
│ 1 │ 발주서 양식 │ 홍길동 │ 2025-10-01 │ 수정 │ 복사 │ 삭제 │
|
||||
│ 2 │ 청구서 기본 │ 김철수 │ 2025-09-28 │ 수정 │ 복사 │ 삭제 │
|
||||
│ 3 │ 거래명세서 │ 이영희 │ 2025-09-25 │ 수정 │ 복사 │ 삭제 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**기능**
|
||||
|
||||
- 리포트 목록 조회 (페이징, 정렬, 검색)
|
||||
- 새 리포트 생성
|
||||
- 기존 리포트 수정
|
||||
- 리포트 복사
|
||||
- 리포트 삭제
|
||||
- 리포트 미리보기
|
||||
|
||||
### 2.2 리포트 디자이너 화면
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 리포트 디자이너 [저장] [미리보기] [초기화] [목록으로] │
|
||||
├──────┬────────────────────────────────────────────────┬──────────┤
|
||||
│ │ │ │
|
||||
│ 템플릿│ 작업 영역 (캔버스) │ 속성 패널 │
|
||||
│ │ │ │
|
||||
│ 컴포넌트│ [드래그 앤 드롭] │ 쿼리 관리 │
|
||||
│ │ │ │
|
||||
│ │ │ DB 연동 │
|
||||
└──────┴────────────────────────────────────────────────┴──────────┘
|
||||
```
|
||||
|
||||
### 2.3 미리보기 모달
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 미리보기 [닫기] │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [리포트 내용 미리보기] │
|
||||
│ │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ [인쇄] [PDF] [WORD] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. 데이터베이스 설계
|
||||
|
||||
### 3.1 테이블 구조
|
||||
|
||||
#### REPORT_TEMPLATE (리포트 템플릿)
|
||||
|
||||
```sql
|
||||
CREATE TABLE report_template (
|
||||
template_id VARCHAR(50) PRIMARY KEY, -- 템플릿 ID
|
||||
template_name_kor VARCHAR(100) NOT NULL, -- 템플릿명 (한국어)
|
||||
template_name_eng VARCHAR(100), -- 템플릿명 (영어)
|
||||
template_type VARCHAR(30) NOT NULL, -- 템플릿 타입 (ORDER, INVOICE, STATEMENT, etc)
|
||||
is_system CHAR(1) DEFAULT 'N', -- 시스템 기본 템플릿 여부 (Y/N)
|
||||
thumbnail_url VARCHAR(500), -- 썸네일 이미지 경로
|
||||
description TEXT, -- 템플릿 설명
|
||||
layout_config TEXT, -- 레이아웃 설정 (JSON)
|
||||
default_queries TEXT, -- 기본 쿼리 (JSON)
|
||||
use_yn CHAR(1) DEFAULT 'Y', -- 사용 여부
|
||||
sort_order INTEGER DEFAULT 0, -- 정렬 순서
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
updated_at TIMESTAMP,
|
||||
updated_by VARCHAR(50)
|
||||
);
|
||||
```
|
||||
|
||||
#### REPORT_MASTER (리포트 마스터)
|
||||
|
||||
```sql
|
||||
CREATE TABLE report_master (
|
||||
report_id VARCHAR(50) PRIMARY KEY, -- 리포트 ID
|
||||
report_name_kor VARCHAR(100) NOT NULL, -- 리포트명 (한국어)
|
||||
report_name_eng VARCHAR(100), -- 리포트명 (영어)
|
||||
template_id VARCHAR(50), -- 템플릿 ID (FK)
|
||||
report_type VARCHAR(30) NOT NULL, -- 리포트 타입
|
||||
company_code VARCHAR(20), -- 회사 코드
|
||||
description TEXT, -- 설명
|
||||
use_yn CHAR(1) DEFAULT 'Y', -- 사용 여부
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
updated_at TIMESTAMP,
|
||||
updated_by VARCHAR(50),
|
||||
FOREIGN KEY (template_id) REFERENCES report_template(template_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### REPORT_LAYOUT (리포트 레이아웃)
|
||||
|
||||
```sql
|
||||
CREATE TABLE report_layout (
|
||||
layout_id VARCHAR(50) PRIMARY KEY, -- 레이아웃 ID
|
||||
report_id VARCHAR(50) NOT NULL, -- 리포트 ID (FK)
|
||||
canvas_width INTEGER DEFAULT 210, -- 캔버스 너비 (mm)
|
||||
canvas_height INTEGER DEFAULT 297, -- 캔버스 높이 (mm)
|
||||
page_orientation VARCHAR(10) DEFAULT 'portrait', -- 페이지 방향 (portrait/landscape)
|
||||
margin_top INTEGER DEFAULT 20, -- 상단 여백 (mm)
|
||||
margin_bottom INTEGER DEFAULT 20, -- 하단 여백 (mm)
|
||||
margin_left INTEGER DEFAULT 20, -- 좌측 여백 (mm)
|
||||
margin_right INTEGER DEFAULT 20, -- 우측 여백 (mm)
|
||||
components TEXT, -- 컴포넌트 배치 정보 (JSON)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
updated_at TIMESTAMP,
|
||||
updated_by VARCHAR(50),
|
||||
FOREIGN KEY (report_id) REFERENCES report_master(report_id)
|
||||
);
|
||||
```
|
||||
|
||||
## 4. 컴포넌트 목록
|
||||
|
||||
### 4.1 기본 컴포넌트
|
||||
|
||||
#### 텍스트 관련
|
||||
|
||||
- **Text Field**: 단일 라인 텍스트 입력/표시
|
||||
- **Text Area**: 여러 줄 텍스트 입력/표시
|
||||
- **Label**: 고정 라벨 텍스트
|
||||
- **Rich Text**: 서식이 있는 텍스트 (굵게, 기울임, 색상)
|
||||
|
||||
#### 숫자/날짜 관련
|
||||
|
||||
- **Number**: 숫자 표시 (통화 형식 지원)
|
||||
- **Date**: 날짜 표시 (형식 지정 가능)
|
||||
- **Date Time**: 날짜 + 시간 표시
|
||||
- **Calculate Field**: 계산 필드 (합계, 평균 등)
|
||||
|
||||
#### 테이블/그리드
|
||||
|
||||
- **Data Table**: 데이터 테이블 (디테일 쿼리 바인딩)
|
||||
- **Summary Table**: 요약 테이블
|
||||
- **Group Table**: 그룹핑 테이블
|
||||
|
||||
#### 이미지/그래픽
|
||||
|
||||
- **Image**: 이미지 표시 (로고, 서명 등)
|
||||
- **Line**: 구분선
|
||||
- **Rectangle**: 사각형 (테두리)
|
||||
|
||||
#### 특수 컴포넌트
|
||||
|
||||
- **Page Number**: 페이지 번호
|
||||
- **Current Date**: 현재 날짜/시간
|
||||
- **Company Info**: 회사 정보 (자동)
|
||||
- **Signature**: 서명란
|
||||
- **Stamp**: 도장란
|
||||
|
||||
### 4.2 컴포넌트 속성
|
||||
|
||||
각 컴포넌트는 다음 공통 속성을 가집니다:
|
||||
|
||||
```typescript
|
||||
interface ComponentBase {
|
||||
id: string; // 컴포넌트 ID
|
||||
type: string; // 컴포넌트 타입
|
||||
x: number; // X 좌표
|
||||
y: number; // Y 좌표
|
||||
width: number; // 너비
|
||||
height: number; // 높이
|
||||
zIndex: number; // Z-인덱스
|
||||
|
||||
// 스타일
|
||||
fontSize?: number; // 글자 크기
|
||||
fontFamily?: string; // 폰트
|
||||
fontWeight?: string; // 글자 굵기
|
||||
fontColor?: string; // 글자 색상
|
||||
backgroundColor?: string; // 배경색
|
||||
borderWidth?: number; // 테두리 두께
|
||||
borderColor?: string; // 테두리 색상
|
||||
borderRadius?: number; // 모서리 둥글기
|
||||
textAlign?: string; // 텍스트 정렬
|
||||
padding?: number; // 내부 여백
|
||||
|
||||
// 데이터 바인딩
|
||||
queryId?: string; // 연결된 쿼리 ID
|
||||
fieldName?: string; // 필드명
|
||||
defaultValue?: string; // 기본값
|
||||
format?: string; // 표시 형식
|
||||
|
||||
// 기타
|
||||
visible?: boolean; // 표시 여부
|
||||
printable?: boolean; // 인쇄 여부
|
||||
conditional?: string; // 조건부 표시 (수식)
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 템플릿 목록
|
||||
|
||||
### 5.1 기본 템플릿 (시스템)
|
||||
|
||||
#### 구매/발주 관련
|
||||
|
||||
- **발주서 (Purchase Order)**: 거래처에 발주하는 문서
|
||||
- **구매요청서 (Purchase Request)**: 내부 구매 요청 문서
|
||||
- **발주 확인서 (PO Confirmation)**: 발주 확인 문서
|
||||
|
||||
#### 판매/청구 관련
|
||||
|
||||
- **청구서 (Invoice)**: 고객에게 청구하는 문서
|
||||
- **견적서 (Quotation)**: 견적 제공 문서
|
||||
- **거래명세서 (Transaction Statement)**: 거래 내역 명세
|
||||
- **세금계산서 (Tax Invoice)**: 세금 계산서
|
||||
- **영수증 (Receipt)**: 영수 증빙 문서
|
||||
|
||||
#### 재고/입출고 관련
|
||||
|
||||
- **입고증 (Goods Receipt)**: 입고 증빙 문서
|
||||
- **출고증 (Delivery Note)**: 출고 증빙 문서
|
||||
- **재고 현황표 (Inventory Report)**: 재고 현황
|
||||
- **이동 전표 (Transfer Note)**: 재고 이동 문서
|
||||
|
||||
#### 생산 관련
|
||||
|
||||
- **작업지시서 (Work Order)**: 생산 작업 지시
|
||||
- **생산 일보 (Production Daily Report)**: 생산 일일 보고
|
||||
- **품질 검사표 (Quality Inspection)**: 품질 검사 기록
|
||||
- **불량 보고서 (Defect Report)**: 불량 보고
|
||||
|
||||
#### 회계/경영 관련
|
||||
|
||||
- **손익 계산서 (Income Statement)**: 손익 현황
|
||||
- **대차대조표 (Balance Sheet)**: 재무 상태
|
||||
- **현금 흐름표 (Cash Flow Statement)**: 현금 흐름
|
||||
- **급여 명세서 (Payroll Slip)**: 급여 내역
|
||||
|
||||
#### 일반 문서
|
||||
|
||||
- **기본 양식 (Basic Template)**: 빈 캔버스
|
||||
- **일반 보고서 (General Report)**: 일반 보고 양식
|
||||
- **목록 양식 (List Template)**: 목록형 양식
|
||||
|
||||
### 5.2 사용자 정의 템플릿
|
||||
|
||||
- 사용자가 직접 생성한 템플릿
|
||||
- 기본 템플릿을 복사하여 수정 가능
|
||||
- 회사별로 관리 가능
|
||||
|
||||
## 6. API 설계
|
||||
|
||||
### 6.1 리포트 목록 API
|
||||
|
||||
#### GET `/api/admin/reports`
|
||||
|
||||
리포트 목록 조회
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface GetReportsRequest {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchText?: string;
|
||||
reportType?: string;
|
||||
useYn?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
// Response
|
||||
interface GetReportsResponse {
|
||||
items: ReportMaster[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/admin/reports/:reportId`
|
||||
|
||||
리포트 상세 조회
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface ReportDetail {
|
||||
report: ReportMaster;
|
||||
layout: ReportLayout;
|
||||
queries: ReportQuery[];
|
||||
components: Component[];
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/admin/reports`
|
||||
|
||||
리포트 생성
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface CreateReportRequest {
|
||||
reportNameKor: string;
|
||||
reportNameEng?: string;
|
||||
templateId?: string;
|
||||
reportType: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Response
|
||||
interface CreateReportResponse {
|
||||
reportId: string;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### PUT `/api/admin/reports/:reportId`
|
||||
|
||||
리포트 수정
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface UpdateReportRequest {
|
||||
reportNameKor?: string;
|
||||
reportNameEng?: string;
|
||||
reportType?: string;
|
||||
description?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### DELETE `/api/admin/reports/:reportId`
|
||||
|
||||
리포트 삭제
|
||||
|
||||
#### POST `/api/admin/reports/:reportId/copy`
|
||||
|
||||
리포트 복사
|
||||
|
||||
### 6.2 템플릿 API
|
||||
|
||||
#### GET `/api/admin/reports/templates`
|
||||
|
||||
템플릿 목록 조회
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
interface GetTemplatesResponse {
|
||||
system: ReportTemplate[]; // 시스템 템플릿
|
||||
custom: ReportTemplate[]; // 사용자 정의 템플릿
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/admin/reports/templates`
|
||||
|
||||
템플릿 생성 (사용자 정의)
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface CreateTemplateRequest {
|
||||
templateNameKor: string;
|
||||
templateNameEng?: string;
|
||||
templateType: string;
|
||||
description?: string;
|
||||
layoutConfig: any;
|
||||
defaultQueries?: any;
|
||||
}
|
||||
```
|
||||
|
||||
#### PUT `/api/admin/reports/templates/:templateId`
|
||||
|
||||
템플릿 수정
|
||||
|
||||
#### DELETE `/api/admin/reports/templates/:templateId`
|
||||
|
||||
템플릿 삭제
|
||||
|
||||
### 6.3 레이아웃 API
|
||||
|
||||
#### GET `/api/admin/reports/:reportId/layout`
|
||||
|
||||
레이아웃 조회
|
||||
|
||||
#### PUT `/api/admin/reports/:reportId/layout`
|
||||
|
||||
레이아웃 저장
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface SaveLayoutRequest {
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
pageOrientation: string;
|
||||
margins: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
};
|
||||
components: Component[];
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 인쇄/내보내기 API
|
||||
|
||||
#### POST `/api/admin/reports/:reportId/preview`
|
||||
|
||||
미리보기 생성
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface PreviewRequest {
|
||||
parameters?: { [key: string]: any };
|
||||
format?: "HTML" | "PDF";
|
||||
}
|
||||
|
||||
// Response
|
||||
interface PreviewResponse {
|
||||
html?: string; // HTML 미리보기
|
||||
pdfUrl?: string; // PDF URL
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/admin/reports/:reportId/print`
|
||||
|
||||
인쇄 (PDF 생성)
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
interface PrintRequest {
|
||||
parameters?: { [key: string]: any };
|
||||
format: "PDF" | "WORD" | "EXCEL";
|
||||
}
|
||||
|
||||
// Response
|
||||
interface PrintResponse {
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 프론트엔드 구조
|
||||
|
||||
### 7.1 페이지 구조
|
||||
|
||||
```
|
||||
/admin/report
|
||||
├── ReportListPage.tsx # 리포트 목록 페이지
|
||||
├── ReportDesignerPage.tsx # 리포트 디자이너 페이지
|
||||
└── components/
|
||||
├── ReportList.tsx # 리포트 목록 테이블
|
||||
├── ReportSearchForm.tsx # 검색 폼
|
||||
├── TemplateSelector.tsx # 템플릿 선택기
|
||||
├── ComponentPalette.tsx # 컴포넌트 팔레트
|
||||
├── Canvas.tsx # 캔버스 영역
|
||||
├── ComponentRenderer.tsx # 컴포넌트 렌더러
|
||||
├── PropertyPanel.tsx # 속성 패널
|
||||
├── QueryManager.tsx # 쿼리 관리
|
||||
├── QueryCard.tsx # 쿼리 카드
|
||||
├── ConnectionManager.tsx # 외부 DB 연결 관리
|
||||
├── PreviewModal.tsx # 미리보기 모달
|
||||
└── PrintOptionsModal.tsx # 인쇄 옵션 모달
|
||||
```
|
||||
|
||||
### 7.2 상태 관리
|
||||
|
||||
```typescript
|
||||
interface ReportDesignerState {
|
||||
// 리포트 기본 정보
|
||||
report: ReportMaster | null;
|
||||
|
||||
// 레이아웃
|
||||
layout: ReportLayout | null;
|
||||
components: Component[];
|
||||
selectedComponentId: string | null;
|
||||
|
||||
// 쿼리
|
||||
queries: ReportQuery[];
|
||||
queryResults: { [queryId: string]: any[] };
|
||||
|
||||
// 외부 연결
|
||||
connections: ReportExternalConnection[];
|
||||
|
||||
// UI 상태
|
||||
isDragging: boolean;
|
||||
isResizing: boolean;
|
||||
showPreview: boolean;
|
||||
showPrintOptions: boolean;
|
||||
|
||||
// 히스토리 (Undo/Redo)
|
||||
history: {
|
||||
past: Component[][];
|
||||
present: Component[];
|
||||
future: Component[][];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 구현 우선순위
|
||||
|
||||
### Phase 1: 기본 기능 (2주)
|
||||
|
||||
- [ ] 데이터베이스 테이블 생성
|
||||
- [ ] 리포트 목록 화면
|
||||
- [ ] 리포트 CRUD API
|
||||
- [ ] 템플릿 목록 조회
|
||||
- [ ] 기본 템플릿 데이터 생성
|
||||
|
||||
### Phase 2: 디자이너 기본 (2주)
|
||||
|
||||
- [ ] 캔버스 구현
|
||||
- [ ] 컴포넌트 드래그 앤 드롭
|
||||
- [ ] 컴포넌트 선택/이동/크기 조절
|
||||
- [ ] 속성 패널 (기본)
|
||||
- [ ] 저장/불러오기
|
||||
|
||||
### Phase 3: 쿼리 관리 (1주)
|
||||
|
||||
- [ ] 쿼리 추가/수정/삭제
|
||||
- [ ] 파라미터 감지 및 입력
|
||||
- [ ] 쿼리 실행 (내부 DB)
|
||||
- [ ] 쿼리 결과를 컴포넌트에 바인딩
|
||||
|
||||
### Phase 4: 쿼리 관리 고급 (1주)
|
||||
|
||||
- [ ] 쿼리 필드 매핑
|
||||
- [ ] 컴포넌트와 데이터 바인딩
|
||||
- [ ] 파라미터 전달 및 처리
|
||||
|
||||
### Phase 5: 미리보기/인쇄 (1주)
|
||||
|
||||
- [ ] HTML 미리보기
|
||||
- [ ] PDF 생성
|
||||
- [ ] WORD 생성
|
||||
- [ ] 브라우저 인쇄
|
||||
|
||||
### Phase 6: 고급 기능 (2주)
|
||||
|
||||
- [ ] 템플릿 생성 기능
|
||||
- [ ] 컴포넌트 추가 (이미지, 서명, 도장)
|
||||
- [ ] 계산 필드
|
||||
- [ ] 조건부 표시
|
||||
- [ ] Undo/Redo
|
||||
- [ ] 다국어 지원
|
||||
|
||||
## 9. 기술 스택
|
||||
|
||||
### Backend
|
||||
|
||||
- **Node.js + TypeScript**: 백엔드 서버
|
||||
- **PostgreSQL**: 데이터베이스
|
||||
- **Prisma**: ORM
|
||||
- **Puppeteer**: PDF 생성
|
||||
- **docx**: WORD 생성
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Next.js + React**: 프론트엔드 프레임워크
|
||||
- **TypeScript**: 타입 안정성
|
||||
- **TailwindCSS**: 스타일링
|
||||
- **react-dnd**: 드래그 앤 드롭
|
||||
- **react-grid-layout**: 레이아웃 관리
|
||||
- **react-to-print**: 인쇄 기능
|
||||
- **react-pdf**: PDF 미리보기
|
||||
|
||||
## 10. 보안 고려사항
|
||||
|
||||
### 10.1 쿼리 실행 보안
|
||||
|
||||
- SELECT 쿼리만 허용 (INSERT, UPDATE, DELETE 금지)
|
||||
- 쿼리 결과 크기 제한 (최대 1000 rows)
|
||||
- 실행 시간 제한 (30초)
|
||||
- SQL 인젝션 방지 (파라미터 바인딩 강제)
|
||||
- 위험한 함수 차단 (DROP, TRUNCATE 등)
|
||||
|
||||
### 10.2 파일 보안
|
||||
|
||||
- 생성된 PDF/WORD 파일은 임시 디렉토리에 저장
|
||||
- 파일은 24시간 후 자동 삭제
|
||||
- 파일 다운로드 시 토큰 검증
|
||||
|
||||
### 10.3 접근 권한
|
||||
|
||||
- 리포트 생성/수정/삭제 권한 체크
|
||||
- 관리자만 템플릿 생성 가능
|
||||
- 사용자별 리포트 접근 제어
|
||||
|
||||
## 11. 성능 최적화
|
||||
|
||||
### 11.1 PDF 생성 최적화
|
||||
|
||||
- 백그라운드 작업으로 처리
|
||||
- 생성된 PDF는 CDN에 캐싱
|
||||
|
||||
### 11.2 프론트엔드 최적화
|
||||
|
||||
- 컴포넌트 가상화 (많은 컴포넌트 처리)
|
||||
- 디바운싱/쓰로틀링 (드래그 앤 드롭)
|
||||
- 이미지 레이지 로딩
|
||||
|
||||
### 11.3 데이터베이스 최적화
|
||||
|
||||
- 레이아웃 데이터는 JSON 형태로 저장
|
||||
- 리포트 목록 조회 시 인덱스 활용
|
||||
- 자주 사용하는 템플릿 캐싱
|
||||
|
||||
## 12. 테스트 계획
|
||||
|
||||
### 12.1 단위 테스트
|
||||
|
||||
- API 엔드포인트 테스트
|
||||
- 쿼리 파싱 테스트
|
||||
- PDF 생성 테스트
|
||||
|
||||
### 12.2 통합 테스트
|
||||
|
||||
- 리포트 생성 → 쿼리 실행 → PDF 생성 전체 플로우
|
||||
- 템플릿 적용 → 데이터 바인딩 테스트
|
||||
|
||||
### 12.3 UI 테스트
|
||||
|
||||
- 드래그 앤 드롭 동작 테스트
|
||||
- 컴포넌트 속성 변경 테스트
|
||||
|
||||
## 13. 향후 확장 계획
|
||||
|
||||
### 13.1 고급 기능
|
||||
|
||||
- 차트/그래프 컴포넌트
|
||||
- 조건부 서식 (색상 변경 등)
|
||||
- 그룹핑 및 집계 함수
|
||||
- 마스터-디테일 관계 자동 설정
|
||||
|
||||
### 13.2 협업 기능
|
||||
|
||||
- 리포트 공유
|
||||
- 버전 관리
|
||||
- 댓글 기능
|
||||
|
||||
### 13.3 자동화
|
||||
|
||||
- 스케줄링 (정기적 리포트 생성)
|
||||
- 이메일 자동 발송
|
||||
- 알림 설정
|
||||
|
||||
## 14. 참고 자료
|
||||
|
||||
### 14.1 유사 솔루션
|
||||
|
||||
- Crystal Reports
|
||||
- JasperReports
|
||||
- BIRT (Business Intelligence and Reporting Tools)
|
||||
- FastReport
|
||||
|
||||
### 14.2 라이브러리
|
||||
|
||||
- [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout)
|
||||
- [react-dnd](https://react-dnd.github.io/react-dnd/)
|
||||
- [puppeteer](https://pptr.dev/)
|
||||
- [docx](https://docx.js.org/)
|
||||
@@ -0,0 +1,371 @@
|
||||
# 리포트 문서 번호 자동 채번 시스템 설계
|
||||
|
||||
## 1. 개요
|
||||
|
||||
리포트 관리 시스템에 체계적인 문서 번호 자동 채번 시스템을 추가하여, 기업 환경에서 문서를 추적하고 관리할 수 있도록 합니다.
|
||||
|
||||
## 2. 문서 번호 형식
|
||||
|
||||
### 2.1 기본 형식
|
||||
|
||||
```
|
||||
{PREFIX}-{YEAR}-{SEQUENCE}
|
||||
예: RPT-2024-0001, INV-2024-0123
|
||||
```
|
||||
|
||||
### 2.2 확장 형식 (선택 사항)
|
||||
|
||||
```
|
||||
{PREFIX}-{DEPT_CODE}-{YEAR}-{SEQUENCE}
|
||||
예: RPT-SALES-2024-0001, INV-FIN-2024-0123
|
||||
```
|
||||
|
||||
### 2.3 구성 요소
|
||||
|
||||
- **PREFIX**: 문서 유형 접두사 (예: RPT, INV, PO, QT)
|
||||
- **DEPT_CODE**: 부서 코드 (선택 사항)
|
||||
- **YEAR**: 연도 (4자리)
|
||||
- **SEQUENCE**: 순차 번호 (0001부터 시작, 자릿수 설정 가능)
|
||||
|
||||
## 3. 데이터베이스 스키마
|
||||
|
||||
### 3.1 문서 번호 규칙 테이블
|
||||
|
||||
```sql
|
||||
-- 문서 번호 규칙 정의
|
||||
CREATE TABLE report_number_rules (
|
||||
rule_id SERIAL PRIMARY KEY,
|
||||
rule_name VARCHAR(100) NOT NULL, -- 규칙 이름
|
||||
prefix VARCHAR(20) NOT NULL, -- 접두사 (RPT, INV 등)
|
||||
use_dept_code BOOLEAN DEFAULT FALSE, -- 부서 코드 사용 여부
|
||||
use_year BOOLEAN DEFAULT TRUE, -- 연도 사용 여부
|
||||
sequence_length INTEGER DEFAULT 4, -- 순차 번호 자릿수
|
||||
reset_period VARCHAR(20) DEFAULT 'YEARLY', -- 초기화 주기 (YEARLY, MONTHLY, NEVER)
|
||||
separator VARCHAR(5) DEFAULT '-', -- 구분자
|
||||
description TEXT, -- 설명
|
||||
is_active BOOLEAN DEFAULT TRUE, -- 활성화 여부
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
updated_by VARCHAR(50)
|
||||
);
|
||||
|
||||
-- 기본 데이터 삽입
|
||||
INSERT INTO report_number_rules (rule_name, prefix, description)
|
||||
VALUES ('리포트 문서 번호', 'RPT', '일반 리포트 문서 번호 규칙');
|
||||
```
|
||||
|
||||
### 3.2 문서 번호 시퀀스 테이블
|
||||
|
||||
```sql
|
||||
-- 문서 번호 시퀀스 관리 (연도/부서별 현재 번호)
|
||||
CREATE TABLE report_number_sequences (
|
||||
sequence_id SERIAL PRIMARY KEY,
|
||||
rule_id INTEGER NOT NULL REFERENCES report_number_rules(rule_id),
|
||||
dept_code VARCHAR(20), -- 부서 코드 (NULL 가능)
|
||||
year INTEGER NOT NULL, -- 연도
|
||||
current_number INTEGER DEFAULT 0, -- 현재 번호
|
||||
last_generated_at TIMESTAMP, -- 마지막 생성 시각
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (rule_id, dept_code, year) -- 규칙+부서+연도 조합 유니크
|
||||
);
|
||||
```
|
||||
|
||||
### 3.3 리포트 테이블 수정
|
||||
|
||||
```sql
|
||||
-- 기존 report_layout 테이블에 컬럼 추가
|
||||
ALTER TABLE report_layout
|
||||
ADD COLUMN document_number VARCHAR(100), -- 생성된 문서 번호
|
||||
ADD COLUMN number_rule_id INTEGER REFERENCES report_number_rules(rule_id), -- 사용된 규칙
|
||||
ADD COLUMN number_generated_at TIMESTAMP; -- 번호 생성 시각
|
||||
|
||||
-- 문서 번호 인덱스 (검색 성능)
|
||||
CREATE INDEX idx_report_layout_document_number ON report_layout(document_number);
|
||||
```
|
||||
|
||||
### 3.4 문서 번호 이력 테이블 (감사용)
|
||||
|
||||
```sql
|
||||
-- 문서 번호 생성 이력
|
||||
CREATE TABLE report_number_history (
|
||||
history_id SERIAL PRIMARY KEY,
|
||||
report_id INTEGER REFERENCES report_layout(id),
|
||||
document_number VARCHAR(100) NOT NULL,
|
||||
rule_id INTEGER REFERENCES report_number_rules(rule_id),
|
||||
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
generated_by VARCHAR(50),
|
||||
is_voided BOOLEAN DEFAULT FALSE, -- 번호 무효화 여부
|
||||
void_reason TEXT, -- 무효화 사유
|
||||
voided_at TIMESTAMP,
|
||||
voided_by VARCHAR(50)
|
||||
);
|
||||
|
||||
-- 문서 번호로 검색 인덱스
|
||||
CREATE INDEX idx_report_number_history_doc_number ON report_number_history(document_number);
|
||||
```
|
||||
|
||||
## 4. 백엔드 구현
|
||||
|
||||
### 4.1 서비스 레이어 (`reportNumberService.ts`)
|
||||
|
||||
```typescript
|
||||
export class ReportNumberService {
|
||||
// 문서 번호 생성
|
||||
static async generateNumber(
|
||||
ruleId: number,
|
||||
deptCode?: string
|
||||
): Promise<string>;
|
||||
|
||||
// 문서 번호 형식 검증
|
||||
static async validateNumber(documentNumber: string): Promise<boolean>;
|
||||
|
||||
// 문서 번호 중복 체크
|
||||
static async isDuplicate(documentNumber: string): Promise<boolean>;
|
||||
|
||||
// 문서 번호 무효화
|
||||
static async voidNumber(
|
||||
documentNumber: string,
|
||||
reason: string,
|
||||
userId: string
|
||||
): Promise<void>;
|
||||
|
||||
// 특정 규칙의 다음 번호 미리보기
|
||||
static async previewNextNumber(
|
||||
ruleId: number,
|
||||
deptCode?: string
|
||||
): Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 컨트롤러 (`reportNumberController.ts`)
|
||||
|
||||
```typescript
|
||||
// GET /api/report/number-rules - 규칙 목록
|
||||
// GET /api/report/number-rules/:id - 규칙 상세
|
||||
// POST /api/report/number-rules - 규칙 생성
|
||||
// PUT /api/report/number-rules/:id - 규칙 수정
|
||||
// DELETE /api/report/number-rules/:id - 규칙 삭제
|
||||
|
||||
// POST /api/report/:reportId/generate-number - 문서 번호 생성
|
||||
// POST /api/report/number/preview - 다음 번호 미리보기
|
||||
// POST /api/report/number/void - 문서 번호 무효화
|
||||
// GET /api/report/number/history/:documentNumber - 문서 번호 이력
|
||||
```
|
||||
|
||||
### 4.3 핵심 로직 (번호 생성)
|
||||
|
||||
```typescript
|
||||
async generateNumber(ruleId: number, deptCode?: string): Promise<string> {
|
||||
// 1. 트랜잭션 시작
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 2. 규칙 조회
|
||||
const rule = await this.getRule(ruleId);
|
||||
|
||||
// 3. 현재 연도/월
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
|
||||
// 4. 시퀀스 조회 또는 생성 (FOR UPDATE로 락)
|
||||
let sequence = await this.getSequence(ruleId, deptCode, year, true);
|
||||
|
||||
if (!sequence) {
|
||||
sequence = await this.createSequence(ruleId, deptCode, year);
|
||||
}
|
||||
|
||||
// 5. 다음 번호 계산
|
||||
const nextNumber = sequence.current_number + 1;
|
||||
|
||||
// 6. 문서 번호 생성
|
||||
const documentNumber = this.formatNumber(rule, deptCode, year, nextNumber);
|
||||
|
||||
// 7. 시퀀스 업데이트
|
||||
await this.updateSequence(sequence.sequence_id, nextNumber);
|
||||
|
||||
// 8. 커밋
|
||||
await client.query('COMMIT');
|
||||
|
||||
return documentNumber;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 번호 포맷팅
|
||||
private formatNumber(
|
||||
rule: NumberRule,
|
||||
deptCode: string | undefined,
|
||||
year: number,
|
||||
sequence: number
|
||||
): string {
|
||||
const parts = [rule.prefix];
|
||||
|
||||
if (rule.use_dept_code && deptCode) {
|
||||
parts.push(deptCode);
|
||||
}
|
||||
|
||||
if (rule.use_year) {
|
||||
parts.push(year.toString());
|
||||
}
|
||||
|
||||
// 0 패딩
|
||||
const paddedSequence = sequence.toString().padStart(rule.sequence_length, '0');
|
||||
parts.push(paddedSequence);
|
||||
|
||||
return parts.join(rule.separator);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 프론트엔드 구현
|
||||
|
||||
### 5.1 문서 번호 규칙 관리 화면
|
||||
|
||||
**경로**: `/admin/report/number-rules`
|
||||
|
||||
**기능**:
|
||||
|
||||
- 규칙 목록 조회
|
||||
- 규칙 생성/수정/삭제
|
||||
- 규칙 미리보기 (다음 번호 확인)
|
||||
- 규칙 활성화/비활성화
|
||||
|
||||
### 5.2 리포트 목록 화면 수정
|
||||
|
||||
**변경 사항**:
|
||||
|
||||
- 문서 번호 컬럼 추가
|
||||
- 문서 번호로 검색 기능
|
||||
|
||||
### 5.3 리포트 저장 시 번호 생성
|
||||
|
||||
**위치**: `ReportDesignerContext.tsx` - `saveLayout` 함수
|
||||
|
||||
```typescript
|
||||
const saveLayout = async () => {
|
||||
// 1. 새 리포트인 경우 문서 번호 자동 생성
|
||||
if (reportId === "new" && !documentNumber) {
|
||||
const response = await fetch(`/api/report/generate-number`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ruleId: 1 }), // 기본 규칙
|
||||
});
|
||||
const { documentNumber: newNumber } = await response.json();
|
||||
setDocumentNumber(newNumber);
|
||||
}
|
||||
|
||||
// 2. 리포트 저장 (문서 번호 포함)
|
||||
await saveReport({ ...reportData, documentNumber });
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 문서 번호 표시 UI
|
||||
|
||||
**위치**: 디자이너 헤더
|
||||
|
||||
```tsx
|
||||
<div className="document-number">
|
||||
<Label>문서 번호</Label>
|
||||
<Badge variant="outline">{documentNumber || "저장 시 자동 생성"}</Badge>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 6. 동시성 제어
|
||||
|
||||
### 6.1 문제점
|
||||
|
||||
여러 사용자가 동시에 문서 번호를 생성할 때 중복 발생 가능성
|
||||
|
||||
### 6.2 해결 방법
|
||||
|
||||
**PostgreSQL의 `FOR UPDATE` 사용**
|
||||
|
||||
```sql
|
||||
-- 시퀀스 조회 시 행 락 걸기
|
||||
SELECT * FROM report_number_sequences
|
||||
WHERE rule_id = $1 AND year = $2
|
||||
FOR UPDATE;
|
||||
```
|
||||
|
||||
**트랜잭션 격리 수준**
|
||||
|
||||
```typescript
|
||||
await client.query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
|
||||
```
|
||||
|
||||
## 7. 테스트 시나리오
|
||||
|
||||
### 7.1 기본 기능 테스트
|
||||
|
||||
- [ ] 규칙 생성 → 문서 번호 생성 → 포맷 확인
|
||||
- [ ] 연속 생성 시 순차 번호 증가 확인
|
||||
- [ ] 연도 변경 시 시퀀스 초기화 확인
|
||||
|
||||
### 7.2 동시성 테스트
|
||||
|
||||
- [ ] 10명이 동시에 문서 번호 생성 → 중복 없음 확인
|
||||
- [ ] 동일 규칙으로 100개 생성 → 순차 번호 연속성 확인
|
||||
|
||||
### 7.3 에러 처리
|
||||
|
||||
- [ ] 존재하지 않는 규칙 ID → 에러 메시지
|
||||
- [ ] 비활성화된 규칙 사용 → 경고 메시지
|
||||
- [ ] 시퀀스 최대값 초과 → 관리자 알림
|
||||
|
||||
## 8. 구현 순서
|
||||
|
||||
### Phase 1: 데이터베이스 (1단계)
|
||||
|
||||
1. 테이블 생성 SQL 작성
|
||||
2. 마이그레이션 실행
|
||||
3. 기본 데이터 삽입
|
||||
|
||||
### Phase 2: 백엔드 (2단계)
|
||||
|
||||
1. `reportNumberService.ts` 구현
|
||||
2. `reportNumberController.ts` 구현
|
||||
3. 라우트 추가
|
||||
4. 단위 테스트
|
||||
|
||||
### Phase 3: 프론트엔드 (3단계)
|
||||
|
||||
1. 문서 번호 규칙 관리 화면
|
||||
2. 리포트 목록 화면 수정
|
||||
3. 디자이너 문서 번호 표시
|
||||
4. 저장 시 자동 생성 연동
|
||||
|
||||
### Phase 4: 테스트 및 최적화 (4단계)
|
||||
|
||||
1. 통합 테스트
|
||||
2. 동시성 테스트
|
||||
3. 성능 최적화
|
||||
4. 사용자 가이드 작성
|
||||
|
||||
## 9. 향후 확장
|
||||
|
||||
### 9.1 고급 기능
|
||||
|
||||
- 문서 번호 예약 기능
|
||||
- 번호 건너뛰기 허용 설정
|
||||
- 커스텀 포맷 지원 (정규식 기반)
|
||||
- 연/월/일 단위 초기화 선택
|
||||
|
||||
### 9.2 통합
|
||||
|
||||
- 승인 완료 시점에 최종 번호 확정
|
||||
- 외부 시스템과 번호 동기화
|
||||
- 바코드/QR 코드 자동 생성
|
||||
|
||||
## 10. 보안 고려사항
|
||||
|
||||
- 문서 번호 생성 권한 제한
|
||||
- 번호 무효화 감사 로그
|
||||
- 시퀀스 직접 수정 방지
|
||||
- API 호출 횟수 제한 (Rate Limiting)
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
# 리포트 페이지 관리 시스템 설계
|
||||
|
||||
## 1. 개요
|
||||
|
||||
리포트 디자이너에 다중 페이지 관리 기능을 추가하여 여러 페이지에 걸친 복잡한 문서를 작성할 수 있도록 합니다.
|
||||
|
||||
## 2. 주요 기능
|
||||
|
||||
### 2.1 페이지 관리
|
||||
|
||||
- 페이지 추가/삭제
|
||||
- 페이지 복사
|
||||
- 페이지 순서 변경 (드래그 앤 드롭)
|
||||
- 페이지 이름 지정
|
||||
|
||||
### 2.2 페이지 네비게이션
|
||||
|
||||
- 좌측 페이지 썸네일 패널
|
||||
- 페이지 간 전환 (클릭)
|
||||
- 이전/다음 페이지 이동
|
||||
- 페이지 번호 표시
|
||||
|
||||
### 2.3 페이지별 설정
|
||||
|
||||
- 페이지 크기 (A4, A3, Letter, 사용자 정의)
|
||||
- 페이지 방향 (세로/가로)
|
||||
- 여백 설정
|
||||
- 배경색
|
||||
|
||||
### 2.4 컴포넌트 관리
|
||||
|
||||
- 컴포넌트는 특정 페이지에 속함
|
||||
- 페이지 간 컴포넌트 복사/이동
|
||||
- 현재 페이지의 컴포넌트만 표시
|
||||
|
||||
## 3. 데이터베이스 스키마
|
||||
|
||||
### 3.1 기존 구조 활용 (변경 없음)
|
||||
|
||||
**report_layout 테이블의 layout_config (JSONB) 활용**
|
||||
|
||||
기존:
|
||||
|
||||
```json
|
||||
{
|
||||
"width": 210,
|
||||
"height": 297,
|
||||
"orientation": "portrait",
|
||||
"components": [...]
|
||||
}
|
||||
```
|
||||
|
||||
변경 후:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"page_id": "page-uuid-1",
|
||||
"page_name": "표지",
|
||||
"page_order": 0,
|
||||
"width": 210,
|
||||
"height": 297,
|
||||
"orientation": "portrait",
|
||||
"margins": {
|
||||
"top": 20,
|
||||
"bottom": 20,
|
||||
"left": 20,
|
||||
"right": 20
|
||||
},
|
||||
"background_color": "#ffffff",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp-1",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 50,
|
||||
...
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"page_id": "page-uuid-2",
|
||||
"page_name": "본문",
|
||||
"page_order": 1,
|
||||
"width": 210,
|
||||
"height": 297,
|
||||
"orientation": "portrait",
|
||||
"margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 },
|
||||
"background_color": "#ffffff",
|
||||
"components": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 마이그레이션 전략
|
||||
|
||||
기존 단일 페이지 리포트 자동 변환:
|
||||
|
||||
```typescript
|
||||
// 기존 구조 감지 시
|
||||
if (layoutConfig.components && !layoutConfig.pages) {
|
||||
// 자동으로 pages 구조로 변환
|
||||
layoutConfig = {
|
||||
pages: [
|
||||
{
|
||||
page_id: uuidv4(),
|
||||
page_name: "페이지 1",
|
||||
page_order: 0,
|
||||
width: layoutConfig.width || 210,
|
||||
height: layoutConfig.height || 297,
|
||||
orientation: layoutConfig.orientation || "portrait",
|
||||
margins: { top: 20, bottom: 20, left: 20, right: 20 },
|
||||
background_color: "#ffffff",
|
||||
components: layoutConfig.components,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 프론트엔드 구조
|
||||
|
||||
### 4.1 타입 정의 (types/report.ts)
|
||||
|
||||
```typescript
|
||||
export interface ReportPage {
|
||||
page_id: string;
|
||||
report_id: string;
|
||||
page_order: number;
|
||||
page_name: string;
|
||||
|
||||
// 페이지 설정
|
||||
width: number;
|
||||
height: number;
|
||||
orientation: 'portrait' | 'landscape';
|
||||
|
||||
// 여백
|
||||
margin_top: number;
|
||||
margin_bottom: number;
|
||||
margin_left: number;
|
||||
margin_right: number;
|
||||
|
||||
// 배경
|
||||
background_color: string;
|
||||
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ComponentConfig {
|
||||
id: string;
|
||||
// page_id 불필요 (페이지의 components 배열에 포함됨)
|
||||
type: 'text' | 'label' | 'image' | 'table' | ...;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
// ... 기타 속성
|
||||
}
|
||||
|
||||
export interface ReportLayoutConfig {
|
||||
pages: ReportPage[];
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Context 구조 변경
|
||||
|
||||
```typescript
|
||||
interface ReportDesignerContextType {
|
||||
// 페이지 관리
|
||||
pages: ReportPage[];
|
||||
currentPageId: string | null;
|
||||
currentPage: ReportPage | null;
|
||||
|
||||
addPage: () => void;
|
||||
deletePage: (pageId: string) => void;
|
||||
duplicatePage: (pageId: string) => void;
|
||||
reorderPages: (sourceIndex: number, targetIndex: number) => void;
|
||||
selectPage: (pageId: string) => void;
|
||||
updatePage: (pageId: string, updates: Partial<ReportPage>) => void;
|
||||
|
||||
// 컴포넌트 (현재 페이지만)
|
||||
currentPageComponents: ComponentConfig[];
|
||||
|
||||
// ... 기존 기능들
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 UI 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ReportDesignerToolbar (저장, 미리보기, 페이지 추가 등) │
|
||||
├──────────┬────────────────────────────────────┬─────────────┤
|
||||
│ │ │ │
|
||||
│ PageList │ ReportDesignerCanvas │ Right │
|
||||
│ (좌측) │ (현재 페이지만 표시) │ Panel │
|
||||
│ │ │ (속성) │
|
||||
│ - Page 1 │ ┌──────────────────────────┐ │ │
|
||||
│ - Page 2 │ │ │ │ │
|
||||
│ * Page 3 │ │ [컴포넌트들] │ │ │
|
||||
│ (현재) │ │ │ │ │
|
||||
│ │ └──────────────────────────┘ │ │
|
||||
│ [+ 추가] │ │ │
|
||||
│ │ 이전 | 다음 (페이지 네비게이션) │ │
|
||||
└──────────┴────────────────────────────────────┴─────────────┘
|
||||
```
|
||||
|
||||
## 5. 컴포넌트 구조
|
||||
|
||||
### 5.1 새 컴포넌트
|
||||
|
||||
#### PageListPanel.tsx
|
||||
|
||||
```typescript
|
||||
- 좌측 페이지 목록 패널
|
||||
- 페이지 썸네일 표시
|
||||
- 드래그 앤 드롭으로 순서 변경
|
||||
- 페이지 추가/삭제/복사 버튼
|
||||
- 현재 페이지 하이라이트
|
||||
```
|
||||
|
||||
#### PageNavigator.tsx
|
||||
|
||||
```typescript
|
||||
- 캔버스 하단의 페이지 네비게이션
|
||||
- 이전/다음 버튼
|
||||
- 현재 페이지 번호 표시
|
||||
- 페이지 점프 (1/5 형식)
|
||||
```
|
||||
|
||||
#### PageSettingsPanel.tsx
|
||||
|
||||
```typescript
|
||||
- 우측 패널 내 페이지 설정 섹션
|
||||
- 페이지 크기, 방향
|
||||
- 여백 설정
|
||||
- 배경색
|
||||
```
|
||||
|
||||
### 5.2 수정할 컴포넌트
|
||||
|
||||
#### ReportDesignerContext.tsx
|
||||
|
||||
- pages 상태 추가
|
||||
- currentPageId 상태 추가
|
||||
- 페이지 관리 함수들 추가
|
||||
- components를 currentPageComponents로 필터링
|
||||
|
||||
#### ReportDesignerCanvas.tsx
|
||||
|
||||
- currentPageComponents만 렌더링
|
||||
- 캔버스 크기를 currentPage 기준으로 설정
|
||||
- 컴포넌트 추가 시 page_id 포함
|
||||
|
||||
#### ReportDesignerToolbar.tsx
|
||||
|
||||
- "페이지 추가" 버튼 추가
|
||||
- 저장 시 pages도 함께 저장
|
||||
|
||||
#### ReportPreviewModal.tsx
|
||||
|
||||
- 모든 페이지 순서대로 미리보기
|
||||
- 페이지 구분선 표시
|
||||
- PDF 저장 시 모든 페이지 포함
|
||||
|
||||
## 6. API 엔드포인트
|
||||
|
||||
### 6.1 페이지 관리
|
||||
|
||||
```typescript
|
||||
// 페이지 목록 조회
|
||||
GET /api/report/:reportId/pages
|
||||
Response: { pages: ReportPage[] }
|
||||
|
||||
// 페이지 생성
|
||||
POST /api/report/:reportId/pages
|
||||
Body: { page_name, width, height, orientation, margins }
|
||||
Response: { page: ReportPage }
|
||||
|
||||
// 페이지 수정
|
||||
PUT /api/report/pages/:pageId
|
||||
Body: Partial<ReportPage>
|
||||
Response: { page: ReportPage }
|
||||
|
||||
// 페이지 삭제
|
||||
DELETE /api/report/pages/:pageId
|
||||
Response: { success: boolean }
|
||||
|
||||
// 페이지 순서 변경
|
||||
PUT /api/report/:reportId/pages/reorder
|
||||
Body: { pageOrders: Array<{ page_id, page_order }> }
|
||||
Response: { success: boolean }
|
||||
|
||||
// 페이지 복사
|
||||
POST /api/report/pages/:pageId/duplicate
|
||||
Response: { page: ReportPage }
|
||||
```
|
||||
|
||||
### 6.2 레이아웃 (기존 수정)
|
||||
|
||||
```typescript
|
||||
// 레이아웃 저장 (페이지별)
|
||||
PUT /api/report/:reportId/layout
|
||||
Body: {
|
||||
pages: ReportPage[],
|
||||
components: ComponentConfig[] // page_id 포함
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 구현 단계
|
||||
|
||||
### Phase 1: DB 및 백엔드 (0.5일)
|
||||
|
||||
1. ✅ DB 스키마 생성
|
||||
2. ✅ API 엔드포인트 구현
|
||||
3. ✅ 기존 리포트 마이그레이션 (단일 페이지 생성)
|
||||
|
||||
### Phase 2: 타입 및 Context (0.5일)
|
||||
|
||||
1. ✅ 타입 정의 업데이트
|
||||
2. ✅ Context에 페이지 상태/함수 추가
|
||||
3. ✅ API 연동
|
||||
|
||||
### Phase 3: UI 컴포넌트 (1일)
|
||||
|
||||
1. ✅ PageListPanel 구현
|
||||
2. ✅ PageNavigator 구현
|
||||
3. ✅ PageSettingsPanel 구현
|
||||
|
||||
### Phase 4: 통합 및 수정 (1일)
|
||||
|
||||
1. ✅ Canvas에서 현재 페이지만 표시
|
||||
2. ✅ 컴포넌트 추가/수정 시 page_id 처리
|
||||
3. ✅ 미리보기에서 모든 페이지 표시
|
||||
4. ✅ PDF/WORD 저장에서 모든 페이지 처리
|
||||
|
||||
### Phase 5: 테스트 및 최적화 (0.5일)
|
||||
|
||||
1. ✅ 페이지 전환 성능 확인
|
||||
2. ✅ 썸네일 렌더링 최적화
|
||||
3. ✅ 버그 수정
|
||||
|
||||
**총 예상 기간: 3-4일**
|
||||
|
||||
## 8. 주의사항
|
||||
|
||||
### 8.1 성능 최적화
|
||||
|
||||
- 페이지 썸네일은 저해상도로 렌더링
|
||||
- 현재 페이지 컴포넌트만 DOM에 유지
|
||||
- 페이지 전환 시 애니메이션 최소화
|
||||
|
||||
### 8.2 호환성
|
||||
|
||||
- 기존 리포트는 자동으로 단일 페이지로 마이그레이션
|
||||
- 템플릿도 페이지 구조 포함
|
||||
|
||||
### 8.3 사용자 경험
|
||||
|
||||
- 페이지 삭제 시 확인 다이얼로그
|
||||
- 컴포넌트가 있는 페이지 삭제 시 경고
|
||||
- 페이지 순서 변경 시 즉시 반영
|
||||
|
||||
## 9. 추후 확장 기능
|
||||
|
||||
### 9.1 페이지 템플릿
|
||||
|
||||
- 자주 사용하는 페이지 레이아웃 저장
|
||||
- 페이지 추가 시 템플릿 선택
|
||||
|
||||
### 9.2 마스터 페이지
|
||||
|
||||
- 모든 페이지에 공통으로 적용되는 헤더/푸터
|
||||
- 페이지 번호 자동 삽입
|
||||
|
||||
### 9.3 페이지 연결
|
||||
|
||||
- 테이블 데이터가 여러 페이지에 자동 분할
|
||||
- 페이지 오버플로우 처리
|
||||
|
||||
## 10. 참고 자료
|
||||
|
||||
- 오즈리포트 메뉴얼
|
||||
- Crystal Reports 페이지 관리
|
||||
- Adobe InDesign 페이지 시스템
|
||||
Reference in New Issue
Block a user