From 007e8a3ed3fe60076337ebf7b6afd6391903bd29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 16:55:19 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[quality]=20=EC=8B=A4=EC=A0=81=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=EC=97=91=EC=85=80=20Export=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dev/guides/performance-report-excel-export.md 신규 작성 - 품질관리 README 미구현 항목 → 구현 완료로 업데이트 - INDEX.md에 새 문서 등록 --- INDEX.md | 1 + dev/guides/performance-report-excel-export.md | 239 ++++++++++++++++++ features/quality-management/README.md | 5 +- 3 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 dev/guides/performance-report-excel-export.md diff --git a/INDEX.md b/INDEX.md index 75de0b4..df6d06a 100644 --- a/INDEX.md +++ b/INDEX.md @@ -220,6 +220,7 @@ DB 도메인별: | [claude-code-to-slack.md](dev/guides/claude-code-to-slack.md) | Claude Code → 슬랙 붙여넣기 가이드 | | [claude-code-btw-guide.md](dev/guides/claude-code-btw-guide.md) | Claude Code /btw 사이드 질문 기능 가이드 | | [tenant-email-integration-guide.md](dev/guides/tenant-email-integration-guide.md) | 테넌트 이메일 연동 (SMTP 프리셋, MNG 관리 화면, 연결 테스트) | +| [performance-report-excel-export.md](dev/guides/performance-report-excel-export.md) | 실적신고 확정건 엑셀 Export (건기원 양식, PhpSpreadsheet, 셀 병합) | --- diff --git a/dev/guides/performance-report-excel-export.md b/dev/guides/performance-report-excel-export.md new file mode 100644 index 0000000..8af9055 --- /dev/null +++ b/dev/guides/performance-report-excel-export.md @@ -0,0 +1,239 @@ +# 실적신고 확정건 엑셀 Export 구현 가이드 + +> **작성일**: 2026-03-17 +> **상태**: 운영 중 + +--- + +## 1. 개요 + +### 1.1 목적 + +품질관리 > 실적신고관리에서 **확정건 엑셀다운로드** 버튼 클릭 시, 건기원 제출 양식(품질인정자재등의 판매실적 대장)에 맞는 xlsx 파일을 생성하여 다운로드한다. + +### 1.2 왜 PhpSpreadsheet 직접 사용인가 + +기존 `ExportService`(Maatwebsite/Excel)는 단순 테이블 Export에 적합하지만, 건기원 양식은 다음 요구사항이 있어 PhpSpreadsheet를 직접 사용한다: + +- 다중 헤더 행 (제목, 부제목, 카테고리 헤더, 컬럼 헤더) +- 복잡한 셀 병합 (같은 품질관리서의 여러 개소 → 문서 수준 컬럼 병합) +- 카테고리별 배경색 (5개 카테고리, 각기 다른 색상) +- 회사 정보 섹션 (Tenant 모델 참조) +- 27개 컬럼 (A~AA) + +--- + +## 2. 아키텍처 + +### 2.1 구성 요소 + +``` +┌─────────────────────────────────────────────────────────┐ +│ React (PerformanceReportList.tsx) │ +│ └── handleExcelDownload() │ +│ └── exportConfirmedExcel() [Server Action] │ +│ └── fetch(API, Accept: xlsx) │ +└────────────────────┬────────────────────────────────────┘ + │ GET /api/v1/quality/performance-reports/export-excel + ▼ +┌─────────────────────────────────────────────────────────┐ +│ API (PerformanceReportController) │ +│ └── exportExcel() │ +│ └── PerformanceReportService.exportConfirmed() │ +│ └── PerformanceReportExcelService.generate() │ +│ ├── getConfirmedReports() │ +│ ├── setColumnWidths() │ +│ ├── writeTitle() │ +│ ├── writeCompanyInfo() │ +│ ├── writeCategoryHeaders() │ +│ ├── writeColumnHeaders() │ +│ └── writeDataRows() + merge logic │ +│ └── StreamedResponse (xlsx) │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 파일 구조 + +| 프로젝트 | 파일 | 역할 | +|---------|------|------| +| API | `app/Services/PerformanceReportExcelService.php` | 엑셀 생성 전담 서비스 | +| API | `app/Services/PerformanceReportService.php` | `exportConfirmed()` 메서드 | +| API | `app/Http/Controllers/Api/V1/PerformanceReportController.php` | `exportExcel()` 액션 | +| API | `routes/api/v1/quality.php` | GET 라우트 | +| React | `components/quality/PerformanceReportManagement/actions.ts` | `exportConfirmedExcel()` 서버 액션 | +| React | `components/quality/PerformanceReportManagement/PerformanceReportList.tsx` | `handleExcelDownload()` | + +--- + +## 3. 엑셀 양식 구조 + +### 3.1 시트 레이아웃 + +``` +Row 1 : [A1:AA1 병합] 제목 — "품질인정자재등의 판매실적 제출서식" (돋움 24pt bold) +Row 2 : 빈 행 +Row 3 : [A3:AA3 병합] 부제목 — "품질인정자재등의 판매실적 대장(2026년 1분기)" (돋움 18pt) +Row 4 : 빈 행 +Row 5~9 : 회사 정보 (Tenant 모델) +Row 10 : 빈 행 +Row 11 : 카테고리 헤더 (5개 카테고리, 각각 배경색) +Row 12 : 컬럼 헤더 (27개) +Row 13+ : 데이터 행 +``` + +### 3.2 카테고리 헤더 (Row 11) + +| 범위 | 카테고리 | 배경색 | +|------|---------|--------| +| A~L | 건축자재내역 | `#DAEEF3` (연한 파랑) | +| M~O | 건축공사장 | `#E2EFDA` (연한 초록) | +| P~S | 공사감리자 | `#FCE4D6` (연한 주황) | +| T~W | 공사시공자 | `#EDEDED` (연한 회색) | +| X~AA | 자재유통업자 | `#FFF2CC` (연한 노랑) | + +### 3.3 컬럼 헤더 (Row 12) — 27개 + +| 컬럼 | 헤더 | 데이터 소스 | 병합 | +|------|------|-----------|------| +| A | 일련번호 | 순번 (auto) | O | +| B | 품질관리서번호 | `quality_doc_number` | O | +| C | 작성일 | `received_date` | O | +| D | 인정품목 | **미확정** → '' | O | +| E | 규격(품명) | `orderItem.item_name` | O | +| F | 규격(종류) | `orderItem.specification` | O | +| G | 제품검사일 | `options.inspection.end_date` | O | +| H | 내화성능시간 | **미확정** → '' | O | +| I | 사용부위 | **미확정** → '' | O | +| J | 로트번호 | **미확정** → '' | X | +| K | 규격(치수) | `post_width × post_height` | X | +| L | 수량 | `orderItem.quantity` | X | +| M | 공사명칭 | `options.construction_site.name` | O | +| N | 소재지 | `options.construction_site.land_location` | O | +| O | 번지 | `options.construction_site.lot_number` | O | +| P | 사무소명 | `options.supervisor.office` | O | +| Q | 사무소주소 | `options.supervisor.address` | O | +| R | 성명 | `options.supervisor.name` | O | +| S | 연락처 | `options.supervisor.phone` | O | +| T | 업체명 | `options.contractor.company` | O | +| U | 업체주소 | `options.contractor.address` | O | +| V | 성명 | `options.contractor.name` | O | +| W | 연락처 | `options.contractor.phone` | O | +| X | 업체명 | `options.material_distributor.company` | O | +| Y | 업체주소 | `options.material_distributor.address` | O | +| Z | 대표자명 | `options.material_distributor.ceo` | O | +| AA | 연락처 | `options.material_distributor.phone` | O | + +> **병합 O**: 같은 품질관리서의 여러 개소 → 첫 행에만 기록, 나머지 행 병합 +> **병합 X**: 개소별로 다른 데이터 → 매 행마다 기록 + +### 3.4 셀 병합 로직 + +하나의 `PerformanceReport` → `QualityDocument` → 여러 `QualityDocumentLocation` + +```php +// 같은 품질관리서에 개소가 3개인 경우: +// Row 13: 일련번호=1, 품관번호, 작성일, ... (문서 데이터) + 개소1 데이터 +// Row 14: + 개소2 데이터 +// Row 15: + 개소3 데이터 +// → A13:A15, B13:B15, C13:C15, ... 병합 (J, K, L 제외) +``` + +--- + +## 4. 미확정 필드 (추후 확장 포인트) + +4개 필드가 현재 빈 문자열을 반환하며, 별도 메서드로 분리되어 있어 추후 데이터 매핑만 추가하면 된다: + +| 메서드 | 컬럼 | 설명 | +|--------|------|------| +| `getProductCategory($location)` | D | 인정품목 | +| `getFireResistanceTime($location)` | H | 내화성능시간 | +| `getUsagePart($location)` | I | 사용부위 | +| `getLotNumber($location)` | J | 로트번호 | + +> 이 4개 필드의 데이터 소스가 확정되면 해당 메서드 내부만 수정하면 된다. +> 참고: `project_excel_export_pending_fields.md` (메모리) + +--- + +## 5. API 엔드포인트 + +``` +GET /api/v1/quality/performance-reports/export-excel +``` + +| 파라미터 | 타입 | 필수 | 설명 | +|---------|------|------|------| +| `year` | int | O | 연도 (기본: 현재 연도) | +| `quarter` | int | O | 분기 (기본: 현재 분기) | + +**응답**: `StreamedResponse` (xlsx 파일) + +``` +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +Content-Disposition: attachment; filename*=UTF-8''{회사명}_품질인정자재등의_판매실적_대장_{year}년_{quarter}분기.xlsx +``` + +--- + +## 6. 프론트엔드 패턴 + +### 6.1 Server Action (Blob 다운로드) + +기존 급여관리 `exportPayrollExcel` 패턴을 재사용: + +```typescript +// actions.ts +export async function exportConfirmedExcel(params) { + const cookieStore = await cookies(); + const token = cookieStore.get('access_token')?.value; + const response = await fetch(url, { + headers: { + Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + Authorization: `Bearer ${token}`, + 'X-API-KEY': process.env.API_KEY, + }, + }); + const blob = await response.blob(); + return { success: true, data: blob, filename }; +} +``` + +### 6.2 컴포넌트 (다운로드 트리거) + +```typescript +// PerformanceReportList.tsx +const handleExcelDownload = useCallback(async () => { + const result = await exportConfirmedExcel({ year, quarter }); + if (result.success && result.data) { + const url = URL.createObjectURL(result.data); + const a = document.createElement('a'); + a.href = url; + a.download = result.filename; + a.click(); + URL.revokeObjectURL(url); + } +}, [year, quarter]); +``` + +--- + +## 7. 의존성 + +| 패키지 | 버전 | 프로젝트 | 용도 | +|--------|------|---------|------| +| `phpoffice/phpspreadsheet` | ^1.30 | API | xlsx 생성 | + +> `--ignore-platform-reqs`로 설치됨 (Docker 컨테이너에 `ext-gd` 미설치). xlsx 생성에는 gd 불필요. +> 개발 서버에는 gd 확장이 설치되어 있어 문제 없음. + +--- + +## 관련 문서 + +- [품질관리 시스템](../../features/quality-management/README.md) +- [API 개발 규칙](../standards/api-rules.md) + +--- + +**최종 업데이트**: 2026-03-17 diff --git a/features/quality-management/README.md b/features/quality-management/README.md index d2a5391..61dd66a 100644 --- a/features/quality-management/README.md +++ b/features/quality-management/README.md @@ -237,6 +237,7 @@ quality_documents (품질관리서) | GET | `/quality/performance-reports` | 목록 (연도/분기/상태 필터) | | GET | `/quality/performance-reports/stats` | 확정/미확정 통계 | | GET | `/quality/performance-reports/missing` | 누락체크 | +| GET | `/quality/performance-reports/export-excel` | 확정건 엑셀 다운로드 (건기원 양식) | | PATCH | `/quality/performance-reports/confirm` | 일괄 확정 | | PATCH | `/quality/performance-reports/unconfirm` | 확정 해제 | | PATCH | `/quality/performance-reports/memo` | 메모 일괄 업데이트 | @@ -325,7 +326,7 @@ react/src/components/quality/ | 기능 | 상태 | 설명 | |------|------|------| | 배포(distribute) API | 미구현 | 건기원 시스템 연동 자동 배포 | -| 확정건 엑셀 다운로드 | 미구현 | 확정된 실적 건기원 양식 엑셀 내보내기 | +| ~~확정건 엑셀 다운로드~~ | ✅ 구현 완료 | [구현 가이드](../../dev/guides/performance-report-excel-export.md) 참조 | | 품질인정심사 | 미구현 | 기준/매뉴얼 심사 + 로트 추적 심사 | --- @@ -359,4 +360,4 @@ react/src/components/quality/ --- -**최종 업데이트**: 2026-03-09 +**최종 업데이트**: 2026-03-17