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

[COT-17]Feature: 활동중인 부원 출결 정보 엑셀로 추출 #179

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from

Conversation

yunhacandy
Copy link
Member

✅ 작업 내용

  • 세션 id를 넣으면 세션에 맞는 출결 정보를 엑셀로 추출
  • 출결 정보를 입력하지 않은 경우에 대해서는 일괄 결석 처리

주석은 마지막에 지우겠습니다

@yunhacandy yunhacandy requested review from Youthhing and gikhoon and removed request for Youthhing October 13, 2024 13:37
Copy link
Member

@Youthhing Youthhing left a comment

Choose a reason for hiding this comment

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

리뷰가 조금 많은데 의견 남기거나 반영 부탁드립니다!

개인적으로 결과를 미리 세팅해놓고 이를 분리한 메서드의 파라미터로 넘기는 구조보단, 여러 곳에서 호출이 되지 않는다면

public <반환형> get ~~ , create ~~ (){
//..
}

이런 구조가 조금 메서드 이름과 어울릴 것 같습니다

@@ -95,4 +118,165 @@ public List<AttendanceRecordResponse> findAttendanceRecordsByAttendance(Long att

return attendanceRecordService.generateAttendanceResponses(List.of(attendance));
}

public ResponseEntity<byte[]> exportAttendanceRecordsToExcelBySessions(List<Long> sessionIds) {
Copy link
Member

Choose a reason for hiding this comment

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

Service 계층에도 ResponseEntity를 반환하는 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

엑셀 파일 다운로드 로직이 service 계층에서 처리되기 위해 코드를 작성했었습니다.
그러나 코드 리뷰를 바탕으로 고민한 결과 ResponseEntity는 service에서 반환하는 것이 적절하지 않다고 결론을 내리게 되어서 수정했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

네네, 계층간 DTO 의존성을 고려하면 서비스 계층에 웹 계층 Dto를 필요 이상으로 끌고 올 필요는 없는 것 같네요~


// 파일 다운로드 설정
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=attendance_summary.xlsx");
Copy link
Member

Choose a reason for hiding this comment

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

파일명은 항상 고정되는건가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

피그마에 있는 디자인에서 사용자가 원하는대로 파일명을 정하는 화면이 없어서 일단 고정되게 했었습니다.
확장성을 생각해서 사용자가 원하는 파일명을 입력할 수 있게 하는게 좋을거 같아서 같이 수정하겠습니다 (다른 파트에게 이 부분에 대해 요청해야 할거 같습니다)

Copy link
Member

Choose a reason for hiding this comment

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

화면을 추가하기보단 서버에서 존재하는 데이터를 기반으로 나름의 기준을 가지고 이름을 정하면 좋을 것 같아요.

기수와 포함된 출석 날짜 또는 세션을 기준으로 잡으면 좋을 것 같아요
가령, 출결기록_9기_1,2,3주차 출석.xlsx와 같은 형식으로요


// 세션별 출석 데이터를 저장할 구조체
Map<String, Map<String, String>> memberStatisticsMap = new HashMap<>();
LinkedHashMap<String, String> sessionColumnNames = new LinkedHashMap<>();
Copy link
Member

Choose a reason for hiding this comment

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

이 부분만 LinkedHashMap으로 구현한 이유가 궁금합니다.

Comment on lines 133 to 138
@Operation(summary = "세션별 출석 기록 엑셀 다운로드 API")
@GetMapping("/excel")
public ResponseEntity<byte[]> downloadAttendanceRecordsAsExcelBySessions(
@RequestParam(name = "sessionIds") List<Long> sessionIds) {
return attendanceAdminService.exportAttendanceRecordsToExcelBySessions(sessionIds);
}
Copy link
Member

Choose a reason for hiding this comment

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

추가로, '출결'에 대한 '기록'들을 엑셀로 가져오는 것이니 클라이언트에서 넘어오는 값을 출결id로 정하고 API를 구상하는게 더 합리적인 것 같습니다!

Copy link
Member

Choose a reason for hiding this comment

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

오,, 근데 RequestParam으로 List가 넘어 오는군요 .. ?

@@ -81,6 +81,7 @@ public enum ErrorCode {
//출석 관련 AT
OFFLINE_ATTEND_FAIL(HttpStatus.BAD_REQUEST, "AT-101", "거리 부적합으로 인한 대면 출석 실패"),
INVALID_ATTEND_TIME(HttpStatus.BAD_REQUEST, "AT-102", "시간 입력 범위가 잘못되었습니다."),
FILE_GENERATION_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "AT-201", "엑셀 파일 생성에 실패했습니다."),
Copy link
Member

Choose a reason for hiding this comment

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

Internal Server Error는 아래 S 태그 관련 에러로 옮겨주세요~

Comment on lines 168 to 178
private void initializeMemberAttendance(Map<String, Map<String, String>> memberStatisticsMap, String columnName) {
List<String> allMemberNames = memberService.findActiveMember().stream()
.map(Member::getName)
.toList();

for (String memberName : allMemberNames) {
memberStatisticsMap
.computeIfAbsent(memberName, k -> new HashMap<>())
.put(columnName, "결석");
}
}
Copy link
Member

Choose a reason for hiding this comment

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

'결석'으로 초기화하는 이유가 있을까요?

Copy link
Member

Choose a reason for hiding this comment

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

추가로 Map의 Key를 Member객체 또는 멤버를 구분할 수 있는 id로 한게 아닌 이름으로 짠 이유가 있을까요?

map의 키는 중복이 안되는데, 이 경우에 동명이인 이슈가 고려되지 않은 것 같습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

코드를 짜는 과정에서 '결석'으로 전체 초기화를 하지 않았을때 출석 기록이 없어서 행의 일부가 땡겨지는 경우가 있었습니다.
이에 전체 초기화를 하게 되었는데 현재 방식 대신 출석 기록이 없는 경우에만 ‘결석’ 상태로 처리하는 방식이 더 효율적이라고 계산되어서 수정했습니다.

두번째 리뷰에 대해서는 동명이인 이슈를 고려하지 않아서 수정했습니다.

Comment on lines 216 to 226
int colNum = 1;

for (String columnName : sessionColumnNames.keySet()) {
headerRow.createCell(colNum++).setCellValue(columnName);
}

headerRow.createCell(colNum++).setCellValue("출석");
headerRow.createCell(colNum++).setCellValue("대면");
headerRow.createCell(colNum++).setCellValue("비대면");
headerRow.createCell(colNum++).setCellValue("지각");
headerRow.createCell(colNum++).setCellValue("결석");
Copy link
Member

Choose a reason for hiding this comment

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

"출석", "대면"같은 값은 매직넘버 사용하지 말고 상수화 또는 이미 정해져있는 ENUM 필드 활용 권장드립니다~

Copy link
Member Author

@yunhacandy yunhacandy Oct 14, 2024

Choose a reason for hiding this comment

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

@Youthhing 데이터가 아닌 header row 인데도 상수를 사용하면 안되나요?

Copy link
Member

Choose a reason for hiding this comment

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

지금 사용하고 있는게 매직넘버입니다! "출석" 과 같은 문자열이 파라미터에 그대로 들어가는 경우
매직넘버 사용을 지양하는 이유는 String이나 int와 같은 값은 컴파일 단계에서 걸리지 않는데,
해당 Value를 변경해야할 때 사용 위치를 다 찾아서 변경하기가 힘들고 컴파일 단계에서 알 수 없다는 단점이 있기 때문에 매직 넘버 대신 상수를 사용하는 것을 권장한다는 이야기 입니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

매직넘버에 대한 이해가 부족했었습니다.
리뷰를 바탕으로 수정했습니다.
부원 이름에 대해서도 우선은 MemberRoleGroup.ACTIVE_MEMBERS.getDescription()을 사용했습니다.

Comment on lines 180 to 190
// 실제 출석 기록을 업데이트하는 메소드
private void updateAttendanceRecords(Long sessionId, Map<String, Map<String, String>> memberStatisticsMap, String columnName) {
List<Attendance> attendances = attendanceRepository.findAllBySessionIdsInQuery(List.of(sessionId));
List<AttendanceRecordResponse> attendanceRecords = attendanceRecordService.generateAttendanceResponses(attendances);

for (AttendanceRecordResponse record : attendanceRecords) {
String memberName = record.memberInfo().name();
String attendanceStatus = determineAttendanceStatus(record.statistic());
memberStatisticsMap.get(memberName).put(columnName, attendanceStatus);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

우선, 위에 initialize~메서드에서 activeMembers를 호출하는 쿼리를 날리는데 generateAttendanceResponses()를 호출해 내부적으로 Member 목록을 2번 쿼리하는것이 어색합니다.

Copy link
Member

Choose a reason for hiding this comment

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

그리고 약간? 희박한 경우지만 아래와 같은 경우에 동시성 이슈가 발생해 map.get()을 했을 때 NPE 발생할 수 있습니다.

  1. Map을 초기화하는 당시에 활동 부원
  2. generateAttendanceResponse에서 호출된 활동 중인 부원

두 시점에서의 활동 부원이 다를 가능성이 존재합니다.

Comment on lines 150 to 165
private void collectAttendanceRecords(List<Long> sessionIds, Map<String, Map<String, String>> memberStatisticsMap,
LinkedHashMap<String, String> sessionColumnNames) {
for (Long sessionId : sessionIds) {
Session session = sessionRepository.findById(sessionId)
.orElseThrow(() -> new EntityNotFoundException("세션을 찾을 수 없습니다: " + sessionId));

// 세션 이름을 생성하고 열 이름에 추가
String sessionDate = session.getSessionDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String columnName = session.getNumber() + "주차 세션 (" + sessionDate + ")";
sessionColumnNames.put(columnName, sessionDate);

// 회원들의 출석 상태를 '결석'으로 초기화하고, 출석 기록을 업데이트
initializeMemberAttendance(memberStatisticsMap, columnName);
updateAttendanceRecords(sessionId, memberStatisticsMap, columnName);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

약간 개인적인 생각인데 분리한 메서드안의 작업이 for문만 있다면, for문 내부에 대한 메서드 분리가 조금 더 의미 있어 보입니다!

Copy link
Member

Choose a reason for hiding this comment

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

추가로, 이렇게 세션을 호출하면 선택된 모든 세션에 대해서 N개의 쿼리가 나가게 되지 않나요?

Copy link
Member

@gikhoon gikhoon left a comment

Choose a reason for hiding this comment

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

1차 리뷰 완료했습니다!

LinkedHashMap<String, String> sessionColumnNames = new LinkedHashMap<>();

// 모든 세션에 대한 출석 정보 수집
collectAttendanceRecords(sessionIds, memberStatisticsMap, sessionColumnNames);
Copy link
Member

Choose a reason for hiding this comment

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

한 메소드에 너무 많은 내용이 들어 있는 것 같습니다.

  • sessionId를 기반으로 session 목록 가져오기
  • session 목록를 하나씩 돌면서 column 명 셋팅 하는 메소드
  • 결석으로 초기화 메소드
  • 출결 결과를 기반으로 수정하는 메소드

로 나눠서 작성하면 더 단일 책임 원칙에 맞는 코드가 될 것 같습니다


// 회원의 출석 상태를 초기화하는 메소드
private void initializeMemberAttendance(Map<String, Map<String, String>> memberStatisticsMap, String columnName) {
List<String> allMemberNames = memberService.findActiveMember().stream()
Copy link
Member

Choose a reason for hiding this comment

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

한 엑셀에서 활동 중인 멤버는 모든 세션에서 동일하지 않을까요?

해당 쿼리는 각 세션마다 나갈 필요 없이 최초에 한번만 하면 된다고 생각합니다.


// 실제 출석 기록을 업데이트하는 메소드
private void updateAttendanceRecords(Long sessionId, Map<String, Map<String, String>> memberStatisticsMap, String columnName) {
List<Attendance> attendances = attendanceRepository.findAllBySessionIdsInQuery(List.of(sessionId));
Copy link
Member

Choose a reason for hiding this comment

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

sessionId가 하나인데 InQuery로 한 이유가 있을까요?

headerRow.createCell(colNum++).setCellValue("대면");
headerRow.createCell(colNum++).setCellValue("비대면");
headerRow.createCell(colNum++).setCellValue("지각");
headerRow.createCell(colNum++).setCellValue("결석");
Copy link
Member

Choose a reason for hiding this comment

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

마지막 줄은 colNum 값을 1 증가 할 필요 없을 것 같습니다.

row.createCell(colNum++).setCellValue(totalOffline);
row.createCell(colNum++).setCellValue(totalOnline);
row.createCell(colNum++).setCellValue(totalLate);
row.createCell(colNum++).setCellValue(totalAbsent);
Copy link
Member

Choose a reason for hiding this comment

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

마지막 줄은 colNum 값을 1 증가 할 필요 없을 것 같습니다.

@yunhacandy
Copy link
Member Author

시간이 없어서 1차로 리팩토링 했고 내일 더 하겠습니다..!!
리뷰는 지금 말고 내일 이후로 해주시면 감사하겠습니다:)

Comment on lines 143 to 150
public String getEncodedFileName(String fileName) {
try {
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
return encodedFileName + ".xlsx";
} catch (Exception e) {
throw new AppException(ErrorCode.FILE_NAME_ENCODING_FAIL);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

파일과 관련된 내용은 별도의 유틸 클래스에 작성하는게 나을 것 같습니다.
이와 관련해서 엑셀 파일 제작과 관련된 내용을 별도로 분리하는걸 고려해보는건 어떨까요?

추가로 AbstaractXlsxView와 관련된 개념을 한번 공부해보면 좋을 것 같습니다

Copy link
Member

Choose a reason for hiding this comment

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

그런데 Encode하는 과정에서 Throw할 수 있는 에러가 뭐죠?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants