Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 스터디원 엑셀 다운로드 API 추가 #806

Merged
merged 6 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
});
Comment on lines +220 to +237
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

스트림 API 활용으로 코드 간결화 가능

studyHistories를 순회하며 StudyStudentResponse를 생성하는 부분에서 스트림 API를 활용하면 코드를 더욱 간결하게 표현할 수 있습니다.

예시 수정안:

List<StudyStudentResponse> content = studyHistories.stream().map(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 = studyDetails.stream().flatMap(studyDetail -> Stream.of(
            StudyTodoResponse.createAttendanceType(
                    studyDetail, LocalDate.now(), isAttended(currentAttendances, studyDetail)),
            StudyTodoResponse.createAssignmentType(
                    studyDetail, getSubmittedAssignment(currentAssignmentHistories, studyDetail))
    )).collect(Collectors.toList());

    return StudyStudentResponse.of(studyHistory, currentStudyAchievements, studyTodos);
}).collect(Collectors.toList());


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()));
}
Comment on lines +242 to +262
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

유사한 메서드들의 중복 제거 고려

getStudyAchievementMap, getAttendanceMap, getAssignmentHistoryMap 메서드들이 유사한 로직을 가지고 있습니다. 제네릭을 활용하거나 공통 메서드로 추출하여 중복을 줄일 수 있습니다.

예시 수정안:

private <T, R> Map<Long, List<T>> getEntityMap(Long studyId, List<Long> studentIds,
                                               BiFunction<Long, List<Long>, List<T>> repositoryMethod,
                                               Function<T, R> groupingFunction) {
    List<T> entities = repositoryMethod.apply(studyId, studentIds);
    return entities.stream()
            .collect(groupingBy(entity -> groupingFunction.apply(entity).getId()));
}

// 활용 예시
private Map<Long, List<StudyAchievement>> getStudyAchievementMap(Long studyId, List<Long> studentIds) {
    return getEntityMap(studyId, studentIds,
            studyAchievementRepository::findByStudyIdAndMemberIds,
            studyAchievement -> studyAchievement.getStudent());
}

private Map<Long, List<Attendance>> getAttendanceMap(Long studyId, List<Long> studentIds) {
    return getEntityMap(studyId, studentIds,
            attendanceRepository::findByStudyIdAndMemberIds,
            attendance -> attendance.getStudent());
}

private Map<Long, List<AssignmentHistory>> getAssignmentHistoryMap(Long studyId, List<Long> studentIds) {
    return getEntityMap(studyId, studentIds,
            assignmentHistoryRepository::findByStudyIdAndMemberIds,
            assignmentHistory -> assignmentHistory.getMember());
}

}
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;
}
Comment on lines +116 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

헤더 셀 인덱스 관리 방식 개선

setUpStudySheet 메소드에서 헤더를 생성할 때 row.getLastCellNum()을 사용하고 있습니다. 하지만 새로운 행에서 getLastCellNum()은 예상치 못한 값을 반환할 수 있으므로 주의가 필요합니다.

헤더 셀 인덱스를 별도의 변수로 관리하여 정확한 위치에 헤더가 추가되도록 수정하는 것을 권장합니다.

int cellIndex = STUDY_SHEET_HEADER.length;

// 과제 헤더 추가
for (int i = 1; i <= totalWeek; i++) {
    Cell cell = row.createCell(cellIndex++);
    cell.setCellValue(String.format(WEEKLY_ASSIGNMENT, i));
}

// 출석 헤더 추가
for (int i = 1; i <= totalWeek; i++) {
    Cell cell = row.createCell(cellIndex++);
    cell.setCellValue(String.format(WEEKLY_ATTENDANCE, i));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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]);
});
int cellIndex = STUDY_SHEET_HEADER.length;
// 과제 헤더 추가
for (int i = 1; i <= totalWeek; i++) {
Cell cell = row.createCell(cellIndex++);
cell.setCellValue(String.format(WEEKLY_ASSIGNMENT, i));
}
// 출석 헤더 추가
for (int i = 1; i <= totalWeek; i++) {
Cell cell = row.createCell(cellIndex++);
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();
}
}