- dev/guides/performance-report-excel-export.md 신규 작성 - 품질관리 README 미구현 항목 → 구현 완료로 업데이트 - INDEX.md에 새 문서 등록
9.3 KiB
9.3 KiB
실적신고 확정건 엑셀 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
// 같은 품질관리서에 개소가 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 패턴을 재사용:
// 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 컴포넌트 (다운로드 트리거)
// 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 확장이 설치되어 있어 문제 없음.
관련 문서
최종 업데이트: 2026-03-17