# 06. 리포트 모듈 ## PH1-1차 리포트 (2종) ### 1. 프로젝트별 시수 분석 (No.82) ``` GET /api/reports/project-hours ?projectId=1 &from=2025-04-01 &to=2025-05-31 &groupBy=month // month, week, discipline &format=json // json, excel ``` ```java @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class ReportService { private final TimesheetEntryRepository entryRepository; public ProjectHoursReport getProjectHoursReport(ProjectHoursFilter filter) { // QueryDSL 동적 쿼리 QTimesheetEntry e = QTimesheetEntry.timesheetEntry; QTimesheet ts = QTimesheet.timesheet; QUser u = QUser.user; var query = queryFactory .select(Projections.constructor(ProjectHoursRow.class, e.epcProject.projectCode, e.epcProject.name, u.discipline, e.entryDate, e.hours.sum() )) .from(e) .join(e.timesheet, ts) .join(ts.user, u) .where( ts.status.eq(TimesheetStatus.APPROVED), epcProjectIdEq(filter.getProjectId()), entryDateBetween(filter.getFrom(), filter.getTo()) ) .groupBy(e.epcProject.projectCode, e.epcProject.name, u.discipline, e.entryDate); List rows = query.fetch(); return ProjectHoursReport.builder() .filter(filter) .rows(rows) .totalHours(rows.stream() .map(ProjectHoursRow::getHours) .reduce(BigDecimal.ZERO, BigDecimal::add)) .generatedAt(LocalDateTime.now()) .build(); } } ``` ### 2. WBS Level별 시수 분석 (No.83) ``` GET /api/reports/wbs-hours ?projectId=1 &wbsLevel=3 // 1~5 &from=2025-04-01 &to=2025-05-31 &format=json ``` ```java public WbsHoursReport getWbsHoursReport(WbsHoursFilter filter) { var rows = queryFactory .select(Projections.constructor(WbsHoursRow.class, cw.wbsCode, cw.name, cw.level, cw.discipline, e.hours.sum(), ts.user.countDistinct() )) .from(e) .join(e.timesheet, ts) .join(e.canonicalWbs, cw) .where( ts.status.eq(TimesheetStatus.APPROVED), cw.projectId.eq(filter.getProjectId()), cw.level.eq(filter.getWbsLevel()), entryDateBetween(filter.getFrom(), filter.getTo()) ) .groupBy(cw.wbsCode, cw.name, cw.level, cw.discipline) .orderBy(cw.wbsCode.asc()) .fetch(); return WbsHoursReport.builder() .filter(filter) .rows(rows) .build(); } ``` ## PH1-2차 리포트 (2종) ### 3. Phase별 시수 비율 (No.85) ``` GET /api/reports/phase-ratio?projectId=1&from=...&to=... ``` ### 4. Non-Project 시수 비율 (No.86) ``` GET /api/reports/np-ratio?department=Engineering&from=...&to=... ``` ## Excel Export ```java @Service public class ReportExcelExporter { public byte[] exportProjectHours(ProjectHoursReport report) { try (Workbook wb = new XSSFWorkbook()) { Sheet sheet = wb.createSheet("프로젝트별 시수"); // 헤더 CellStyle headerStyle = createHeaderStyle(wb); Row header = sheet.createRow(0); String[] headers = {"프로젝트코드", "프로젝트명", "Discipline", "날짜", "시수(h)"}; for (int i = 0; i < headers.length; i++) { Cell cell = header.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 데이터 int rowIdx = 1; for (ProjectHoursRow row : report.getRows()) { Row r = sheet.createRow(rowIdx++); r.createCell(0).setCellValue(row.getProjectCode()); r.createCell(1).setCellValue(row.getProjectName()); r.createCell(2).setCellValue(row.getDiscipline()); r.createCell(3).setCellValue(row.getDate().toString()); r.createCell(4).setCellValue(row.getHours().doubleValue()); } // 합계 행 Row totalRow = sheet.createRow(rowIdx); totalRow.createCell(3).setCellValue("합계"); totalRow.createCell(4).setCellValue(report.getTotalHours().doubleValue()); // Auto-size for (int i = 0; i < headers.length; i++) sheet.autoSizeColumn(i); ByteArrayOutputStream out = new ByteArrayOutputStream(); wb.write(out); return out.toByteArray(); } } } ``` ## REST API (리포트) ``` # PH1-1차 GET /api/reports/project-hours 프로젝트별 시수 분석 GET /api/reports/project-hours/export Excel 다운로드 GET /api/reports/wbs-hours WBS Level별 시수 분석 GET /api/reports/wbs-hours/export Excel 다운로드 # PH1-2차 GET /api/reports/phase-ratio Phase별 시수 비율 GET /api/reports/np-ratio Non-Project 시수 비율 GET /api/reports/wbs-version-history WBS 버전 이력 조회 # 공통 파라미터 # projectId - 프로젝트 ID (필수/선택) # from, to - 기간 (ISO 8601) # groupBy - 집계 기준 (month, week, discipline) # format - 응답 형식 (json, excel) ```