diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java index ce504ba80..ab4d9e703 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/api/AdminMemberController.java @@ -8,7 +8,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; @@ -51,7 +50,7 @@ public ResponseEntity updateMember( @Operation(summary = "회원 정보 엑셀 다운로드", description = "회원 정보를 엑셀로 다운로드합니다.") @GetMapping("/excel") - public ResponseEntity createWorkbook() throws IOException { + public ResponseEntity createWorkbook() { byte[] response = adminMemberService.createExcel(); ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename("members.xls").build(); diff --git a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java index 38a91e815..7a2312e50 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/member/application/AdminMemberService.java @@ -18,7 +18,6 @@ import com.gdschongik.gdsc.global.util.EnvironmentUtil; import com.gdschongik.gdsc.global.util.ExcelUtil; import com.gdschongik.gdsc.global.util.MemberUtil; -import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -66,7 +65,7 @@ public void updateMember(Long memberId, MemberUpdateRequest request) { request.nickname()); } - public byte[] createExcel() throws IOException { + public byte[] createExcel() { return excelUtil.createMemberExcel(); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java index 06eb3695d..fc837d2fa 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/api/MentorStudyController.java @@ -12,6 +12,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ContentDisposition; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -66,4 +68,19 @@ public ResponseEntity deleteStudyAnnouncement(@PathVariable Long studyAnno mentorStudyService.deleteStudyAnnouncement(studyAnnouncementId); return ResponseEntity.ok().build(); } + + @Operation(summary = "수강생 정보 엑셀 다운로드", description = "수강생 정보를 엑셀로 다운로드합니다.") + @GetMapping("/{studyId}/students/excel") + public ResponseEntity createStudyWorkbook(@PathVariable Long studyId) { + byte[] response = mentorStudyService.createStudyExcel(studyId); + ContentDisposition contentDisposition = + ContentDisposition.builder("attachment").filename("study.xls").build(); + return ResponseEntity.ok() + .headers(httpHeaders -> { + httpHeaders.setContentDisposition(contentDisposition); + httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); + httpHeaders.setContentLength(response.length); + }) + .body(response); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java index 0988720a0..effc3df08 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/application/MentorStudyService.java @@ -23,6 +23,7 @@ import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.util.ExcelUtil; import com.gdschongik.gdsc.global.util.MemberUtil; import java.time.LocalDate; import java.util.ArrayList; @@ -43,6 +44,7 @@ public class MentorStudyService { private final MemberUtil memberUtil; + private final ExcelUtil excelUtil; private final StudyValidator studyValidator; private final StudyDetailValidator studyDetailValidator; private final StudyRepository studyRepository; @@ -71,21 +73,10 @@ public Page getStudyStudents(Long studyId, Pageable pageab List studentIds = studyHistories.getContent().stream() .map(studyHistory -> studyHistory.getStudent().getId()) .toList(); - List studyAchievements = - studyAchievementRepository.findByStudyIdAndMemberIds(studyId, studentIds); - List attendances = attendanceRepository.findByStudyIdAndMemberIds(studyId, studentIds); - List assignmentHistories = - assignmentHistoryRepository.findByStudyIdAndMemberIds(studyId, studentIds); - // StudyAchievement, Attendance, AssignmentHistory에 대해 Member의 id를 key로 하는 Map 생성 - Map> studyAchievementMap = studyAchievements.stream() - .collect(groupingBy( - studyAchievement -> studyAchievement.getStudent().getId())); - Map> attendanceMap = attendances.stream() - .collect(groupingBy(attendance -> attendance.getStudent().getId())); - Map> assignmentHistoryMap = assignmentHistories.stream() - .collect(groupingBy( - assignmentHistory -> assignmentHistory.getMember().getId())); + Map> studyAchievementMap = getStudyAchievementMap(studyId, studentIds); + Map> attendanceMap = getAttendanceMap(studyId, studentIds); + Map> assignmentHistoryMap = getAssignmentHistoryMap(studyId, studentIds); List response = new ArrayList<>(); studyHistories.getContent().forEach(studyHistory -> { @@ -208,4 +199,65 @@ private void updateAllStudyDetailCurriculum( studyDetailRepository.saveAll(studyDetails); log.info("[MentorStudyService] 스터디 상세정보 커리큘럼 작성 완료: studyDetailId={}", studyDetails); } + + @Transactional(readOnly = true) + public byte[] createStudyExcel(Long studyId) { + Member currentMember = memberUtil.getCurrentMember(); + Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND)); + studyValidator.validateStudyMentor(currentMember, study); + + List studyDetails = studyDetailRepository.findAllByStudyId(studyId); + List studyHistories = studyHistoryRepository.findAllByStudyId(studyId); + List studentIds = studyHistories.stream() + .map(studyHistory -> studyHistory.getStudent().getId()) + .toList(); + + Map> studyAchievementMap = getStudyAchievementMap(studyId, studentIds); + Map> attendanceMap = getAttendanceMap(studyId, studentIds); + Map> assignmentHistoryMap = getAssignmentHistoryMap(studyId, studentIds); + + List content = new ArrayList<>(); + studyHistories.forEach(studyHistory -> { + List currentStudyAchievements = + studyAchievementMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>()); + List currentAttendances = + attendanceMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>()); + List currentAssignmentHistories = + assignmentHistoryMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>()); + + List studyTodos = new ArrayList<>(); + studyDetails.forEach(studyDetail -> { + studyTodos.add(StudyTodoResponse.createAttendanceType( + studyDetail, LocalDate.now(), isAttended(currentAttendances, studyDetail))); + studyTodos.add(StudyTodoResponse.createAssignmentType( + studyDetail, getSubmittedAssignment(currentAssignmentHistories, studyDetail))); + }); + + content.add(StudyStudentResponse.of(studyHistory, currentStudyAchievements, studyTodos)); + }); + + return excelUtil.createStudyExcel(study, content); + } + + private Map> getStudyAchievementMap(Long studyId, List studentIds) { + List studyAchievements = + studyAchievementRepository.findByStudyIdAndMemberIds(studyId, studentIds); + return studyAchievements.stream() + .collect(groupingBy( + studyAchievement -> studyAchievement.getStudent().getId())); + } + + private Map> getAttendanceMap(Long studyId, List studentIds) { + List attendances = attendanceRepository.findByStudyIdAndMemberIds(studyId, studentIds); + return attendances.stream() + .collect(groupingBy(attendance -> attendance.getStudent().getId())); + } + + private Map> getAssignmentHistoryMap(Long studyId, List studentIds) { + List assignmentHistories = + assignmentHistoryRepository.findByStudyIdAndMemberIds(studyId, studentIds); + return assignmentHistories.stream() + .collect(groupingBy( + assignmentHistory -> assignmentHistory.getMember().getId())); + } } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java index 6e2dbb2cf..9495736f2 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dao/StudyHistoryRepository.java @@ -20,4 +20,6 @@ public interface StudyHistoryRepository extends JpaRepository findByStudentAndStudyId(Member member, Long studyId); Page findByStudyId(Long studyId, Pageable pageable); + + List findAllByStudyId(Long studyId); } diff --git a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java index ce7d58577..518fd512b 100644 --- a/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java +++ b/src/main/java/com/gdschongik/gdsc/domain/study/dto/response/StudyTodoResponse.java @@ -64,6 +64,14 @@ public static StudyTodoResponse createAssignmentType(StudyDetail studyDetail, As AssignmentSubmissionStatusResponse.of(assignmentHistory, studyDetail)); } + public boolean isAttendance() { + return todoType == ATTENDANCE; + } + + public boolean isAssignment() { + return todoType == ASSIGNMENT; + } + @Getter @RequiredArgsConstructor public enum StudyTodoType { diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/WorkbookConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/WorkbookConstant.java index ddb4aee73..7580c77b0 100644 --- a/src/main/java/com/gdschongik/gdsc/global/common/constant/WorkbookConstant.java +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/WorkbookConstant.java @@ -1,11 +1,19 @@ package com.gdschongik.gdsc.global.common.constant; public class WorkbookConstant { + // Member public static final String ALL_MEMBER_SHEET_NAME = "전체 회원 목록"; public static final String REGULAR_MEMBER_SHEET_NAME = "정회원 목록"; public static final String[] MEMBER_SHEET_HEADER = { "가입 일시", "이름", "학번", "학과", "전화번호", "이메일", "디스코드 유저네임", "커뮤니티 닉네임" }; + // Study + public static final String[] STUDY_SHEET_HEADER = { + "이름", "학번", "디스코드 유저네임", "커뮤니티 닉네임", "깃허브 링크", "수료 여부", "1차 우수 스터디원 여부", "2차 우수 스터디원 여부", "출석률", "과제 수행률" + }; + public static final String WEEKLY_ASSIGNMENT = "%d주차 과제"; + public static final String WEEKLY_ATTENDANCE = "%d주차 출석"; + private WorkbookConstant() {} } diff --git a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java index e0f5ac309..4ab02f3f8 100644 --- a/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java +++ b/src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java @@ -171,6 +171,9 @@ public enum ErrorCode { GITHUB_FILE_READ_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 파일 읽기에 실패했습니다."), GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."), GITHUB_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 깃허브 유저입니다."), + + // Excel + EXCEL_WORKSHEET_WRITE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "엑셀 워크시트 작성에 실패했습니다."), ; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/gdschongik/gdsc/global/util/ExcelUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/ExcelUtil.java index cd9dcf5b1..5ca7974a1 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/ExcelUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/ExcelUtil.java @@ -6,10 +6,17 @@ import com.gdschongik.gdsc.domain.member.dao.MemberRepository; import com.gdschongik.gdsc.domain.member.domain.Department; import com.gdschongik.gdsc.domain.member.domain.MemberRole; +import com.gdschongik.gdsc.domain.study.domain.Study; +import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse; +import com.gdschongik.gdsc.domain.study.dto.response.StudyTodoResponse; +import com.gdschongik.gdsc.global.exception.CustomException; +import com.gdschongik.gdsc.global.exception.ErrorCode; import jakarta.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -25,34 +32,77 @@ public class ExcelUtil { private final MemberRepository memberRepository; - public byte[] createMemberExcel() throws IOException { + public byte[] createMemberExcel() { HSSFWorkbook workbook = new HSSFWorkbook(); - createSheet(workbook, ALL_MEMBER_SHEET_NAME, null); - createSheet(workbook, REGULAR_MEMBER_SHEET_NAME, REGULAR); + createMemberSheetByRole(workbook, ALL_MEMBER_SHEET_NAME, null); + createMemberSheetByRole(workbook, REGULAR_MEMBER_SHEET_NAME, REGULAR); return createByteArray(workbook); } - private void createSheet(Workbook workbook, String sheetName, @Nullable MemberRole role) { - Sheet sheet = setUpSheet(workbook, sheetName); + public byte[] createStudyExcel(Study study, List content) { + HSSFWorkbook workbook = new HSSFWorkbook(); + createStudySheet(workbook, study, content); + return createByteArray(workbook); + } + + private void createMemberSheetByRole(Workbook workbook, String sheetName, @Nullable MemberRole role) { + Sheet sheet = setUpMemberSheet(workbook, sheetName); memberRepository.findAllByRole(role).forEach(member -> { Row memberRow = sheet.createRow(sheet.getLastRowNum() + 1); - memberRow.createCell(0).setCellValue(member.getCreatedAt().toString()); - memberRow.createCell(1).setCellValue(member.getName()); - memberRow.createCell(2).setCellValue(member.getStudentId()); + int cellIndex = 0; + + memberRow.createCell(cellIndex++).setCellValue(member.getCreatedAt().toString()); + memberRow.createCell(cellIndex++).setCellValue(member.getName()); + memberRow.createCell(cellIndex++).setCellValue(member.getStudentId()); memberRow - .createCell(3) + .createCell(cellIndex++) .setCellValue(Optional.ofNullable(member.getDepartment()) .map(Department::getDepartmentName) .orElse("")); - memberRow.createCell(4).setCellValue(member.getPhone()); - memberRow.createCell(5).setCellValue(member.getEmail()); - memberRow.createCell(6).setCellValue(member.getDiscordUsername()); - memberRow.createCell(7).setCellValue(member.getNickname()); + memberRow.createCell(cellIndex++).setCellValue(member.getPhone()); + memberRow.createCell(cellIndex++).setCellValue(member.getEmail()); + memberRow.createCell(cellIndex++).setCellValue(member.getDiscordUsername()); + memberRow.createCell(cellIndex++).setCellValue(member.getNickname()); + }); + } + + private void createStudySheet(Workbook workbook, Study study, List content) { + Sheet sheet = setUpStudySheet(workbook, study.getTitle(), study.getTotalWeek()); + + content.forEach(student -> { + Row studentRow = sheet.createRow(sheet.getLastRowNum() + 1); + AtomicInteger cellIndex = new AtomicInteger(0); + + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.name()); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.studentId()); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.discordUsername()); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.nickname()); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.githubLink()); + // todo: 수료 여부 추가 + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue("X"); + studentRow + .createCell(cellIndex.getAndIncrement()) + .setCellValue(student.isFirstRoundOutstandingStudent() ? "O" : "X"); + studentRow + .createCell(cellIndex.getAndIncrement()) + .setCellValue(student.isSecondRoundOutstandingStudent() ? "O" : "X"); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.attendanceRate()); + studentRow.createCell(cellIndex.getAndIncrement()).setCellValue(student.assignmentRate()); + student.studyTodos().stream() + .filter(StudyTodoResponse::isAssignment) + .forEach(todo -> studentRow + .createCell(cellIndex.getAndIncrement()) + .setCellValue(todo.assignmentSubmissionStatus().getValue())); + student.studyTodos().stream() + .filter(StudyTodoResponse::isAttendance) + .forEach(todo -> studentRow + .createCell(cellIndex.getAndIncrement()) + .setCellValue(todo.attendanceStatus().getValue())); }); } - private Sheet setUpSheet(Workbook workbook, String sheetName) { + private Sheet setUpMemberSheet(Workbook workbook, String sheetName) { Sheet sheet = workbook.createSheet(sheetName); Row row = sheet.createRow(0); @@ -63,10 +113,35 @@ private Sheet setUpSheet(Workbook workbook, String sheetName) { return sheet; } - private byte[] createByteArray(Workbook workbook) throws IOException { + private Sheet setUpStudySheet(Workbook workbook, String sheetName, long totalWeek) { + Sheet sheet = workbook.createSheet(sheetName); + + Row row = sheet.createRow(0); + IntStream.range(0, STUDY_SHEET_HEADER.length).forEach(i -> { + Cell cell = row.createCell(i); + cell.setCellValue(STUDY_SHEET_HEADER[i]); + }); + + for (int i = 1; i <= totalWeek; i++) { + Cell cell = row.createCell(row.getLastCellNum()); + cell.setCellValue(String.format(WEEKLY_ASSIGNMENT, i)); + } + + for (int i = 1; i <= totalWeek; i++) { + Cell cell = row.createCell(row.getLastCellNum()); + cell.setCellValue(String.format(WEEKLY_ATTENDANCE, i)); + } + return sheet; + } + + private byte[] createByteArray(Workbook workbook) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - workbook.write(outputStream); - workbook.close(); + try { + workbook.write(outputStream); + workbook.close(); + } catch (IOException e) { + throw new CustomException(ErrorCode.EXCEL_WORKSHEET_WRITE_FAILED); + } return outputStream.toByteArray(); } }