From 7cefc39b74d6581412f3ac8a8ad8ae8622e31dfb Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 14:05:06 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/리포트_관리_시스템_구현_진행상황.md | 288 ++++++++++++++++++ .../report/designer/TemplatePalette.tsx | 6 +- frontend/contexts/ReportDesignerContext.tsx | 284 +++++++++++++++++ 3 files changed, 576 insertions(+), 2 deletions(-) create mode 100644 docs/리포트_관리_시스템_구현_진행상황.md diff --git a/docs/리포트_관리_시스템_구현_진행상황.md b/docs/리포트_관리_시스템_구현_진행상황.md new file mode 100644 index 00000000..d93bb246 --- /dev/null +++ b/docs/리포트_관리_시스템_구현_진행상황.md @@ -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% 완료) diff --git a/frontend/components/report/designer/TemplatePalette.tsx b/frontend/components/report/designer/TemplatePalette.tsx index ffe8ea08..0986eeef 100644 --- a/frontend/components/report/designer/TemplatePalette.tsx +++ b/frontend/components/report/designer/TemplatePalette.tsx @@ -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 ( diff --git a/frontend/contexts/ReportDesignerContext.tsx b/frontend/contexts/ReportDesignerContext.tsx index 885aa654..50917d47 100644 --- a/frontend/contexts/ReportDesignerContext.tsx +++ b/frontend/contexts/ReportDesignerContext.tsx @@ -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; loadLayout: () => Promise; + // 템플릿 적용 + 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,