Skip to content

Commit

Permalink
Merge pull request #74 from CSID-DGU/backend/feature/excel
Browse files Browse the repository at this point in the history
BE: [feat] 시험 별 레포트 생성 및 다운로드
  • Loading branch information
JongbeomLee623 authored Dec 1, 2024
2 parents 536635e + 2d3da8e commit 2c6c7b0
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 28 deletions.

This file was deleted.

4 changes: 0 additions & 4 deletions .idea/shelf/11_20_24__5_16PM__________.xml

This file was deleted.

6 changes: 6 additions & 0 deletions src/backend/Eyesee/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ dependencies {
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

// AWS S3
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.530'

// Apache POI (엑셀 다운로드)
implementation 'org.apache.poi:poi-ooxml:5.2.3'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public enum BaseResponseCode {
INVALID_STATUS("GL012", HttpStatus.BAD_REQUEST, "유효하지 않은 상태 값입니다."),
INVALID_INPUT("GL013", HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."),

NOT_FOUND_DATA("GL014", HttpStatus.NOT_FOUND, "요청한 데이터를 찾을 수 없습니다."),

// User Errors
ALREADY_EXIST_USER("U0001", HttpStatus.CONFLICT, "이미 존재하는 사용자입니다"),
WRONG_PASSWORD("U0002", HttpStatus.BAD_REQUEST, "비밀번호가 틀렸습니다."),
Expand All @@ -56,6 +58,7 @@ public enum BaseResponseCode {
// Cheating Errors
NOT_FOUND_CHEATING_TYPE("C0001", HttpStatus.NOT_FOUND, "부정행위 타입을 찾을 수 없습니다."),


// 기타 추가 오류 코드 ...

;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import com.fortune.eyesee.common.response.BaseResponseCode;
import com.fortune.eyesee.dto.*;
import com.fortune.eyesee.enums.ExamStatus;
import com.fortune.eyesee.service.ExamReportService;
import com.fortune.eyesee.service.ExamService;
import com.fortune.eyesee.service.ExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import jakarta.servlet.http.HttpSession;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -25,6 +29,12 @@ public class ExamController {
@Autowired
private ExamService examService;

@Autowired
private ExamReportService examReportService;

@Autowired
private ExcelService excelService;

// // "before" 상태의 Exam 리스트 조회
// @GetMapping("/before")
// public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getBeforeExams(HttpSession session) {
Expand Down Expand Up @@ -119,4 +129,40 @@ public ResponseEntity<BaseResponse<UserDetailResponseDTO>> getUserDetailByExamId
UserDetailResponseDTO response = examService.getUserDetailByExamIdAndUserId(examId, userId);
return ResponseEntity.ok(new BaseResponse<>(response, "학생 상세 정보 조회 성공"));
}

// 사후 레포트 생성
@GetMapping("/{sessionId}/report")
public ResponseEntity<ExamReportResponseDTO> getExamReport(@PathVariable Integer sessionId) {
// SecurityContextHolder에서 adminId 가져오기
Integer adminId = (Integer) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// 레포트 생성
ExamReportResponseDTO report = examReportService.generateExamReport(adminId, sessionId);
return ResponseEntity.ok(report);
}


// 사후 레포트 엑셀 다운로드
@GetMapping("/{sessionId}/report/download")
public ResponseEntity<InputStreamResource> downloadExamReport(@PathVariable Integer sessionId) throws IOException {

// SecurityContextHolder에서 adminId 가져오기
Integer adminId = (Integer) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// 레포트 생성
ExamReportResponseDTO report = examReportService.generateExamReport(adminId, sessionId);

// 엑셀 파일 생성
ByteArrayInputStream excelFile = excelService.generateExcelFile(report);

HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=exam_report.xlsx");

return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(excelFile));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.fortune.eyesee.dto;

import lombok.Data;

import java.util.Map;

@Data
public class ExamReportResponseDTO {
private String examName; // 시험 이름
private Integer totalCheatingCount; // 총 탐지된 부정행위 건수
private Integer cheatingStudentsCount; // 부정행위 탐지된 학생 수
private Double averageCheatingCount; // 평균 부정행위 탐지 건수
private String maxCheatingStudent; // 최다 부정행위 탐지 학생 학번 및 이름
private Double cheatingRate; // 부정행위 탐지율
private Map<String, Integer> cheatingTypeStatistics; // 부정행위 유형별 통계
private String peakCheatingTimeRange; // 부정행위 발생 시간대
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@

import com.fortune.eyesee.entity.DetectedCheating;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface DetectedCheatingRepository extends JpaRepository<DetectedCheating, Integer> {
Optional<DetectedCheating> findByUserIdAndCheatingTypeId(Integer userId, Integer cheatingTypeId);

// 시험 ID로 부정행위 데이터 조회
@Query("SELECT dc FROM DetectedCheating dc WHERE dc.sessionId = :sessionId")
List<DetectedCheating> findBySessionId(@Param("sessionId") Integer sessionId);

// 시험에 참여한 학생 수 조회
@Query("SELECT COUNT(DISTINCT dc.userId) FROM DetectedCheating dc WHERE dc.sessionId = :sessionId")
Integer countDistinctUsersBySessionId(@Param("sessionId") Integer sessionId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface ExamRepository extends JpaRepository<Exam, Integer> {
Expand All @@ -23,4 +24,8 @@ public interface ExamRepository extends JpaRepository<Exam, Integer> {

// adminId 없이 상태로만 Exam 조회
List<Exam> findByExamStatus(ExamStatus examStatus);

// 특정 sessionId와 adminId를 기준으로 Exam 데이터 조회
Optional<Exam> findBySession_SessionIdAndAdmin_AdminId(Integer sessionId, Integer adminId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.fortune.eyesee.repository.CheatingStatisticsRepository;
import org.springframework.stereotype.Service;

import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -60,7 +61,7 @@ public List<DetectedCheating> saveCheating(CheatingResponseDTO cheatingResponseD
detectedCheating.setUserId(userId);
detectedCheating.setSessionId(finalSessionId); // 세션 ID 추가
detectedCheating.setCheatingTypeId(cheatingTypeId);
detectedCheating.setDetectedTime(cheatingResponseDTO.getTimestamp().toLocalTime());
detectedCheating.setDetectedTime(LocalTime.from(cheatingResponseDTO.getTimestamp()));
detectedCheatings.add(detectedCheatingRepository.save(detectedCheating));

// CheatingStatistics 갱신
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.fortune.eyesee.service;

import com.fortune.eyesee.common.exception.BaseException;
import com.fortune.eyesee.common.response.BaseResponseCode;
import com.fortune.eyesee.dto.ExamReportResponseDTO;
import com.fortune.eyesee.entity.*;
import com.fortune.eyesee.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class ExamReportService {

@Autowired
private DetectedCheatingRepository detectedCheatingRepository;

@Autowired
private UserRepository userRepository;

@Autowired
private AdminRepository adminRepository;

@Autowired
private ExamRepository examRepository;

@Autowired
private CheatingTypeRepository cheatingTypeRepository;

public ExamReportResponseDTO generateExamReport(Integer adminId, Integer sessionId) {

// Admin 인증 확인
Admin admin = adminRepository.findById(adminId)
.orElseThrow(() -> new BaseException(BaseResponseCode.UNAUTHORIZED));

// Admin이 생성한 시험인지 확인
Exam exam = examRepository.findBySession_SessionIdAndAdmin_AdminId(sessionId, admin.getAdminId())
.orElseThrow(() -> new BaseException(BaseResponseCode.UNAUTHORIZED));

// 시험의 전체 학생 수 가져오기
Integer totalStudents = exam.getExamStudentNumber();

// 1. 부정행위 데이터 조회
List<DetectedCheating> detectedCheatings = detectedCheatingRepository.findBySessionId(sessionId);

// 2. 총 부정행위 횟수
int totalCheatingCount = detectedCheatings.size();

// 3. 부정행위 탐지된 학생 수
int cheatingStudentsCount = detectedCheatingRepository.countDistinctUsersBySessionId(sessionId);

// 4. 평균 부정행위 횟수
double averageCheatingCount = totalCheatingCount / (double) cheatingStudentsCount;

// 5. 최다 부정행위 탐지 학생
Map<Integer, Long> userCheatingCounts = detectedCheatings.stream()
.collect(Collectors.groupingBy(DetectedCheating::getUserId, Collectors.counting()));
Map.Entry<Integer, Long> maxCheatingEntry = userCheatingCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElse(null);

String maxCheatingStudent = "N/A";
if (maxCheatingEntry != null) {
Integer maxUserId = maxCheatingEntry.getKey();
User maxUser = userRepository.findById(maxUserId).orElse(null);
if (maxUser != null) {
maxCheatingStudent = "학번: " + maxUser.getUserNum() + ", 횟수: " + maxCheatingEntry.getValue();
}
}

// 6. 부정행위 탐지율
double cheatingRate = (cheatingStudentsCount / (double) totalStudents) * 100;

// 7. 부정행위 유형별 통계
Map<String, Integer> cheatingTypeStatistics = detectedCheatings.stream()
.collect(Collectors.groupingBy(dc -> {
CheatingType cheatingType = cheatingTypeRepository.findByCheatingTypeId(dc.getCheatingTypeId())
.orElseThrow(() -> new BaseException(BaseResponseCode.NOT_FOUND_DATA));
return cheatingType.getKoreanTypeName();
}, Collectors.summingInt(e -> 1)));

// 8. 부정행위 발생 시간대 분석
Map<LocalTime, Long> timeFrequency = detectedCheatings.stream()
.collect(Collectors.groupingBy(dc -> dc.getDetectedTime().withMinute(0).withSecond(0).withNano(0),
Collectors.counting()));
LocalTime peakTime = timeFrequency.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
String peakCheatingTimeRange = peakTime != null
? peakTime + " ~ " + peakTime.plusMinutes(30)
: "N/A";


ExamReportResponseDTO report = new ExamReportResponseDTO();

report.setExamName(exam.getExamName());
report.setTotalCheatingCount(totalCheatingCount);
report.setCheatingStudentsCount(cheatingStudentsCount);
report.setAverageCheatingCount(averageCheatingCount);
report.setMaxCheatingStudent(maxCheatingStudent);
report.setCheatingRate(cheatingRate);
report.setCheatingTypeStatistics(cheatingTypeStatistics);
report.setPeakCheatingTimeRange(peakCheatingTimeRange);

return report;
}
}
Loading

0 comments on commit 2c6c7b0

Please sign in to comment.