템플릿 구현
This commit is contained in:
parent
7a588b47f6
commit
7cefc39b74
|
|
@ -0,0 +1,288 @@
|
|||
# 리포트 관리 시스템 구현 진행 상황
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
동적 리포트 디자이너 시스템 구현
|
||||
|
||||
- 사용자가 드래그 앤 드롭으로 리포트 레이아웃 설계
|
||||
- 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] 브라우저 인쇄 기능
|
||||
- [ ] PDF 다운로드 (추후 구현)
|
||||
- [ ] WORD 다운로드 (추후 구현)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/ReportPreviewModal.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 진행 중인 작업 🚧
|
||||
|
||||
### 템플릿 적용 기능 (현재 작업)
|
||||
|
||||
- [ ] 템플릿 선택 시 컴포넌트 자동 배치
|
||||
- [ ] 템플릿별 기본 쿼리 생성
|
||||
- [ ] 발주서 템플릿 구현
|
||||
- [ ] 청구서 템플릿 구현
|
||||
- [ ] 기본 템플릿 구현
|
||||
|
||||
---
|
||||
|
||||
## 남은 작업 (우선순위순) 📋
|
||||
|
||||
### Phase 1: 핵심 기능 완성
|
||||
|
||||
1. **템플릿 적용 기능** ⬅️ 다음 작업
|
||||
|
||||
- 템플릿 팔레트 클릭 이벤트 처리
|
||||
- 템플릿별 레이아웃 정의
|
||||
- 컴포넌트 자동 배치 로직
|
||||
- 기본 쿼리 자동 생성
|
||||
|
||||
2. **스타일링 속성 추가**
|
||||
|
||||
- 폰트 크기, 색상, 굵기, 정렬
|
||||
- 배경색, 테두리 (색상, 두께, 스타일)
|
||||
- 패딩, 마진
|
||||
- 조건부 서식 (선택사항)
|
||||
|
||||
3. **리포트 복사/삭제 기능 완성**
|
||||
- 복사 기능 테스트 및 개선
|
||||
- 삭제 확인 다이얼로그
|
||||
- 삭제 API 연결
|
||||
|
||||
### Phase 2: 고급 기능
|
||||
|
||||
4. **사용자 정의 템플릿 저장**
|
||||
|
||||
- 현재 레이아웃을 템플릿으로 저장
|
||||
- 템플릿 이름/설명 입력
|
||||
- 템플릿 목록에 추가
|
||||
- 시스템 템플릿과 구분
|
||||
|
||||
5. **외부 DB 연동**
|
||||
|
||||
- 외부 DB 연결 정보 관리
|
||||
- 쿼리 실행 시 DB 선택
|
||||
- 연결 테스트 기능
|
||||
|
||||
6. **PDF/WORD 내보내기**
|
||||
- jsPDF 또는 pdfmake 라이브러리 사용
|
||||
- HTML to DOCX 변환
|
||||
- 다운로드 기능 구현
|
||||
|
||||
### Phase 3: 사용성 개선
|
||||
|
||||
7. **레이아웃 도구**
|
||||
|
||||
- 격자 스냅 (Grid Snap)
|
||||
- 정렬 가이드라인
|
||||
- 컴포넌트 그룹화
|
||||
- 실행 취소/다시 실행 (Undo/Redo)
|
||||
|
||||
8. **쿼리 관리 개선**
|
||||
|
||||
- 쿼리 미리보기 개선 (테이블 형태)
|
||||
- 쿼리 저장/불러오기
|
||||
- 쿼리 템플릿
|
||||
|
||||
9. **성능 최적화**
|
||||
- 쿼리 결과 캐싱
|
||||
- 대용량 데이터 페이징
|
||||
- 렌더링 최적화
|
||||
|
||||
### Phase 4: 추가 기능
|
||||
|
||||
10. **다양한 컴포넌트 추가**
|
||||
|
||||
- 이미지 컴포넌트
|
||||
- 차트 컴포넌트 (막대, 선, 원형)
|
||||
- 바코드/QR코드 (선택사항)
|
||||
|
||||
11. **권한 관리**
|
||||
- 리포트별 접근 권한
|
||||
- 수정 권한 분리
|
||||
- 템플릿 공유
|
||||
|
||||
---
|
||||
|
||||
## 기술 스택
|
||||
|
||||
### 백엔드
|
||||
|
||||
- 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. `TemplatePalette` 컴포넌트에 클릭 이벤트 추가
|
||||
2. Context에 `applyTemplate()` 함수 추가
|
||||
3. 템플릿별 레이아웃 정의 (발주서, 청구서, 기본)
|
||||
4. 컴포넌트 자동 배치 및 기본 쿼리 생성
|
||||
5. 템플릿 적용 확인 다이얼로그 (기존 레이아웃 덮어쓰기 경고)
|
||||
|
||||
### 예상 소요 시간
|
||||
|
||||
- 기본 구조: 30분
|
||||
- 템플릿 레이아웃 정의: 1시간
|
||||
- 테스트 및 개선: 30분
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2025-10-01
|
||||
**작성자**: AI Assistant
|
||||
**상태**: 진행 중 (60% 완료)
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileText } from "lucide-react";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
|
||||
const TEMPLATES = [
|
||||
{ id: "order", name: "발주서", icon: "📋" },
|
||||
|
|
@ -10,9 +11,10 @@ const TEMPLATES = [
|
|||
];
|
||||
|
||||
export function TemplatePalette() {
|
||||
const { applyTemplate } = useReportDesigner();
|
||||
|
||||
const handleApplyTemplate = (templateId: string) => {
|
||||
// TODO: 템플릿 적용 로직
|
||||
console.log("Apply template:", templateId);
|
||||
applyTemplate(templateId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,251 @@ export interface ReportQuery {
|
|||
parameters: string[];
|
||||
}
|
||||
|
||||
// 템플릿 레이아웃 정의
|
||||
interface TemplateLayout {
|
||||
components: ComponentConfig[];
|
||||
queries: ReportQuery[];
|
||||
}
|
||||
|
||||
function getTemplateLayout(templateId: string): TemplateLayout | null {
|
||||
switch (templateId) {
|
||||
case "order":
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
id: `comp-${Date.now()}-1`,
|
||||
type: "label",
|
||||
x: 50,
|
||||
y: 30,
|
||||
width: 200,
|
||||
height: 40,
|
||||
fontSize: 24,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 0,
|
||||
zIndex: 1,
|
||||
defaultValue: "발주서",
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-2`,
|
||||
type: "text",
|
||||
x: 50,
|
||||
y: 80,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-3`,
|
||||
type: "text",
|
||||
x: 220,
|
||||
y: 80,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-4`,
|
||||
type: "text",
|
||||
x: 390,
|
||||
y: 80,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-5`,
|
||||
type: "table",
|
||||
x: 50,
|
||||
y: 130,
|
||||
width: 500,
|
||||
height: 200,
|
||||
fontSize: 12,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
id: `query-${Date.now()}-1`,
|
||||
name: "발주 헤더",
|
||||
type: "MASTER",
|
||||
sqlQuery: "SELECT order_no, order_date, supplier_name FROM orders WHERE order_no = $1",
|
||||
parameters: ["$1"],
|
||||
},
|
||||
{
|
||||
id: `query-${Date.now()}-2`,
|
||||
name: "발주 품목",
|
||||
type: "DETAIL",
|
||||
sqlQuery: "SELECT item_name, quantity, unit_price FROM order_items WHERE order_no = $1",
|
||||
parameters: ["$1"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
case "invoice":
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
id: `comp-${Date.now()}-1`,
|
||||
type: "label",
|
||||
x: 50,
|
||||
y: 30,
|
||||
width: 200,
|
||||
height: 40,
|
||||
fontSize: 24,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 0,
|
||||
zIndex: 1,
|
||||
defaultValue: "청구서",
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-2`,
|
||||
type: "text",
|
||||
x: 50,
|
||||
y: 80,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-3`,
|
||||
type: "text",
|
||||
x: 220,
|
||||
y: 80,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-4`,
|
||||
type: "table",
|
||||
x: 50,
|
||||
y: 130,
|
||||
width: 500,
|
||||
height: 200,
|
||||
fontSize: 12,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-5`,
|
||||
type: "label",
|
||||
x: 400,
|
||||
y: 350,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 16,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffcc",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
defaultValue: "합계: 0원",
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
id: `query-${Date.now()}-1`,
|
||||
name: "청구 헤더",
|
||||
type: "MASTER",
|
||||
sqlQuery: "SELECT invoice_no, invoice_date, customer_name FROM invoices WHERE invoice_no = $1",
|
||||
parameters: ["$1"],
|
||||
},
|
||||
{
|
||||
id: `query-${Date.now()}-2`,
|
||||
name: "청구 항목",
|
||||
type: "DETAIL",
|
||||
sqlQuery: "SELECT description, quantity, unit_price, amount FROM invoice_items WHERE invoice_no = $1",
|
||||
parameters: ["$1"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
case "basic":
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
id: `comp-${Date.now()}-1`,
|
||||
type: "label",
|
||||
x: 50,
|
||||
y: 30,
|
||||
width: 300,
|
||||
height: 40,
|
||||
fontSize: 20,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 0,
|
||||
zIndex: 1,
|
||||
defaultValue: "리포트 제목",
|
||||
},
|
||||
{
|
||||
id: `comp-${Date.now()}-2`,
|
||||
type: "text",
|
||||
x: 50,
|
||||
y: 80,
|
||||
width: 500,
|
||||
height: 100,
|
||||
fontSize: 14,
|
||||
fontColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
defaultValue: "내용을 입력하세요",
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
id: `query-${Date.now()}-1`,
|
||||
name: "기본 쿼리",
|
||||
type: "MASTER",
|
||||
sqlQuery: "SELECT * FROM table_name WHERE id = $1",
|
||||
parameters: ["$1"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
queryId: string;
|
||||
fields: string[];
|
||||
|
|
@ -48,6 +293,9 @@ interface ReportDesignerContextType {
|
|||
saveLayout: () => Promise<void>;
|
||||
loadLayout: () => Promise<void>;
|
||||
|
||||
// 템플릿 적용
|
||||
applyTemplate: (templateId: string) => void;
|
||||
|
||||
// 캔버스 설정
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
|
|
@ -275,6 +523,41 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
}
|
||||
}, [reportId, canvasWidth, canvasHeight, pageOrientation, margins, components, queries, toast, loadLayout]);
|
||||
|
||||
// 템플릿 적용
|
||||
const applyTemplate = useCallback(
|
||||
(templateId: string) => {
|
||||
const templates = getTemplateLayout(templateId);
|
||||
|
||||
if (!templates) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "템플릿을 찾을 수 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 컴포넌트가 있으면 확인
|
||||
if (components.length > 0) {
|
||||
if (!confirm("현재 레이아웃을 덮어씁니다. 계속하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 컴포넌트 배치
|
||||
setComponents(templates.components);
|
||||
|
||||
// 쿼리 설정
|
||||
setQueries(templates.queries);
|
||||
|
||||
toast({
|
||||
title: "성공",
|
||||
description: "템플릿이 적용되었습니다.",
|
||||
});
|
||||
},
|
||||
[components.length, toast],
|
||||
);
|
||||
|
||||
const value: ReportDesignerContextType = {
|
||||
reportId,
|
||||
reportDetail,
|
||||
|
|
@ -295,6 +578,7 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
|||
updateLayout,
|
||||
saveLayout,
|
||||
loadLayout,
|
||||
applyTemplate,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
pageOrientation,
|
||||
|
|
|
|||
Loading…
Reference in New Issue