파일
wbx-spring/plans/wtmgr/06-reporting-module.md
accura0117 9707a6eeb1 feat: FE 화면 구현 완료 + 샘플 데이터 + 결재라인 연동
- WBS/TEAL 화면 실제 구현 (TreeTable, FileUpload, 버전관리)
- 시수이력/결재이력 화면 구현 (DataTable, Filter, Timeline)
- 비밀번호변경 화면 추가
- 로그인 snake_case 응답 매핑 수정
- Vite 프록시 8081 포트 수정
- auth guard에서 fetchMe 자동 호출
- V108 샘플 데이터 (10명 사용자, 4주 시수 215건, 결재 9건)
- 배너 추가 (WBX Spring)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:17:32 +09:00

5.4 KiB

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
@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<ProjectHoursRow> 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
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

@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)