Skip to content

Commit

Permalink
feat: 스터디원 엑셀 다운로드 API 추가 (#806)
Browse files Browse the repository at this point in the history
* feat: studyId로 수강이력 조회 메서드 추가

* feat: 수강생 엑셀 다운로드 api 추가

* feat: 엑셀 내용 입력 기능 추가

* refactor: IOException을 커스텀 예외로 변환하도록 수정

* refactor: 하드코딩된 index 수정
  • Loading branch information
Sangwook02 authored Oct 14, 2024
1 parent 382ac9a commit 53f6967
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +50,7 @@ public ResponseEntity<Void> updateMember(

@Operation(summary = "회원 정보 엑셀 다운로드", description = "회원 정보를 엑셀로 다운로드합니다.")
@GetMapping("/excel")
public ResponseEntity<byte[]> createWorkbook() throws IOException {
public ResponseEntity<byte[]> createWorkbook() {
byte[] response = adminMemberService.createExcel();
ContentDisposition contentDisposition =
ContentDisposition.builder("attachment").filename("members.xls").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,7 +65,7 @@ public void updateMember(Long memberId, MemberUpdateRequest request) {
request.nickname());
}

public byte[] createExcel() throws IOException {
public byte[] createExcel() {
return excelUtil.createMemberExcel();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand Down Expand Up @@ -66,4 +68,19 @@ public ResponseEntity<Void> deleteStudyAnnouncement(@PathVariable Long studyAnno
mentorStudyService.deleteStudyAnnouncement(studyAnnouncementId);
return ResponseEntity.ok().build();
}

@Operation(summary = "수강생 정보 엑셀 다운로드", description = "수강생 정보를 엑셀로 다운로드합니다.")
@GetMapping("/{studyId}/students/excel")
public ResponseEntity<byte[]> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -71,21 +73,10 @@ public Page<StudyStudentResponse> getStudyStudents(Long studyId, Pageable pageab
List<Long> studentIds = studyHistories.getContent().stream()
.map(studyHistory -> studyHistory.getStudent().getId())
.toList();
List<StudyAchievement> studyAchievements =
studyAchievementRepository.findByStudyIdAndMemberIds(studyId, studentIds);
List<Attendance> attendances = attendanceRepository.findByStudyIdAndMemberIds(studyId, studentIds);
List<AssignmentHistory> assignmentHistories =
assignmentHistoryRepository.findByStudyIdAndMemberIds(studyId, studentIds);

// StudyAchievement, Attendance, AssignmentHistory에 대해 Member의 id를 key로 하는 Map 생성
Map<Long, List<StudyAchievement>> studyAchievementMap = studyAchievements.stream()
.collect(groupingBy(
studyAchievement -> studyAchievement.getStudent().getId()));
Map<Long, List<Attendance>> attendanceMap = attendances.stream()
.collect(groupingBy(attendance -> attendance.getStudent().getId()));
Map<Long, List<AssignmentHistory>> assignmentHistoryMap = assignmentHistories.stream()
.collect(groupingBy(
assignmentHistory -> assignmentHistory.getMember().getId()));
Map<Long, List<StudyAchievement>> studyAchievementMap = getStudyAchievementMap(studyId, studentIds);
Map<Long, List<Attendance>> attendanceMap = getAttendanceMap(studyId, studentIds);
Map<Long, List<AssignmentHistory>> assignmentHistoryMap = getAssignmentHistoryMap(studyId, studentIds);

List<StudyStudentResponse> response = new ArrayList<>();
studyHistories.getContent().forEach(studyHistory -> {
Expand Down Expand Up @@ -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<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyId(studyId);
List<StudyHistory> studyHistories = studyHistoryRepository.findAllByStudyId(studyId);
List<Long> studentIds = studyHistories.stream()
.map(studyHistory -> studyHistory.getStudent().getId())
.toList();

Map<Long, List<StudyAchievement>> studyAchievementMap = getStudyAchievementMap(studyId, studentIds);
Map<Long, List<Attendance>> attendanceMap = getAttendanceMap(studyId, studentIds);
Map<Long, List<AssignmentHistory>> assignmentHistoryMap = getAssignmentHistoryMap(studyId, studentIds);

List<StudyStudentResponse> content = new ArrayList<>();
studyHistories.forEach(studyHistory -> {
List<StudyAchievement> currentStudyAchievements =
studyAchievementMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());
List<Attendance> currentAttendances =
attendanceMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());
List<AssignmentHistory> currentAssignmentHistories =
assignmentHistoryMap.getOrDefault(studyHistory.getStudent().getId(), new ArrayList<>());

List<StudyTodoResponse> 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<Long, List<StudyAchievement>> getStudyAchievementMap(Long studyId, List<Long> studentIds) {
List<StudyAchievement> studyAchievements =
studyAchievementRepository.findByStudyIdAndMemberIds(studyId, studentIds);
return studyAchievements.stream()
.collect(groupingBy(
studyAchievement -> studyAchievement.getStudent().getId()));
}

private Map<Long, List<Attendance>> getAttendanceMap(Long studyId, List<Long> studentIds) {
List<Attendance> attendances = attendanceRepository.findByStudyIdAndMemberIds(studyId, studentIds);
return attendances.stream()
.collect(groupingBy(attendance -> attendance.getStudent().getId()));
}

private Map<Long, List<AssignmentHistory>> getAssignmentHistoryMap(Long studyId, List<Long> studentIds) {
List<AssignmentHistory> assignmentHistories =
assignmentHistoryRepository.findByStudyIdAndMemberIds(studyId, studentIds);
return assignmentHistories.stream()
.collect(groupingBy(
assignmentHistory -> assignmentHistory.getMember().getId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public interface StudyHistoryRepository extends JpaRepository<StudyHistory, Long
Optional<StudyHistory> findByStudentAndStudyId(Member member, Long studyId);

Page<StudyHistory> findByStudyId(Long studyId, Pageable pageable);

List<StudyHistory> findAllByStudyId(Long studyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
109 changes: 92 additions & 17 deletions src/main/java/com/gdschongik/gdsc/global/util/ExcelUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<StudyStudentResponse> 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<StudyStudentResponse> 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);
Expand All @@ -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();
}
}

0 comments on commit 53f6967

Please sign in to comment.