Skip to content

Commit

Permalink
Feature: 멤버의 기수별 출결 목록 반환 기능 구현 (#89)
Browse files Browse the repository at this point in the history
* refactor: 출결 결과 컬럼명 변경

- 출결 상태 -> 출결 결과로 변경

* refactor: 출결 열림 상태 반환 메서드명 변경

- openStatus로 명시적으로 변경

* feat: 출결 열림 상태에 '시작 전' 상태 추가

* feat: 출결 시작 전 상태 판단 로직 추가

* feat: 출석 목록에서 부원의 출결 기록을 조회하는 JPA 메서드 추가

* feat: 멤버의 기수별 출결 목록 반환

* feat: 출석 기록 반환 값에 출결 형식 추가

* test: 기준 시간이전이면 출석이 '이전'인지에 대한 테스트

* fix: 기존 브랜치 병합에 따른 컨플릭 수정

* refactor: 출결 상태가 고정된 경우 파라미터 대신 생성자에 값을 고정
  • Loading branch information
Youthhing authored Aug 12, 2024
1 parent b94bb6f commit f1a5ab8
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.cotato.csquiz.api.attendance.dto.AttendResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.AttendancesResponse;
import org.cotato.csquiz.api.attendance.dto.MemberAttendanceRecordsResponse;
import org.cotato.csquiz.api.attendance.dto.OfflineAttendanceRequest;
import org.cotato.csquiz.api.attendance.dto.OnlineAttendanceRequest;
import org.cotato.csquiz.api.attendance.dto.UpdateAttendanceRequest;
Expand Down Expand Up @@ -113,4 +114,11 @@ public ResponseEntity<AttendResponse> submitOnlineAttendanceRecord(@RequestBody
@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok().body(attendanceRecordService.submitRecord(request, memberId));
}

@Operation(summary = "부원의 기수별 출결 기록 반환 API")
@GetMapping("/records/members")
public ResponseEntity<MemberAttendanceRecordsResponse> findAllRecordsByGeneration(@RequestParam("generation-id") Long generationId ,
@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok().body(attendanceRecordService.findAllRecordsBy(generationId, memberId));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.cotato.csquiz.api.attendance.dto;

import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceResult;

public record AttendResponse(
AttendanceStatus status,
AttendanceResult status,
String message
) {
public static AttendResponse from(AttendanceStatus status) {
public static AttendResponse from(AttendanceResult status) {
return new AttendResponse(
status,
status.getMessage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.Map;
import java.util.stream.Collectors;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceResult;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

public record AttendanceStatistic(
Expand All @@ -14,9 +14,9 @@ public record AttendanceStatistic(
Integer absent
) {
public static AttendanceStatistic of(List<AttendanceRecord> attendanceRecords, Integer totalAttendance) {
Map<AttendanceStatus, List<AttendanceRecord>> countByStatus = attendanceRecords.stream()
.collect(Collectors.groupingBy(AttendanceRecord::getAttendanceStatus));
List<AttendanceRecord> presentRecords = countByStatus.getOrDefault(AttendanceStatus.PRESENT, List.of());
Map<AttendanceResult, List<AttendanceRecord>> countByStatus = attendanceRecords.stream()
.collect(Collectors.groupingBy(AttendanceRecord::getAttendanceResult));
List<AttendanceRecord> presentRecords = countByStatus.getOrDefault(AttendanceResult.PRESENT, List.of());

int onlineCount = (int) presentRecords.stream()
.filter(record -> AttendanceType.ONLINE == record.getAttendanceType())
Expand All @@ -28,7 +28,7 @@ public static AttendanceStatistic of(List<AttendanceRecord> attendanceRecords, I
return new AttendanceStatistic(
onlineCount,
offLineCount,
countByStatus.getOrDefault(AttendanceStatus.LATE, List.of()).size(),
countByStatus.getOrDefault(AttendanceResult.LATE, List.of()).size(),
totalAttendance - attendanceRecords.size()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.cotato.csquiz.api.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceResult;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;
import org.cotato.csquiz.domain.generation.entity.Session;

public record MemberAttendResponse(
@Schema(description = "멤버 PK")
Long memberId,
@Schema(description = "세션 타이틀", example = "3주차 세션")
String sessionTitle,
@Schema(description = "세션 날짜")
LocalDate sessionDate,
@Schema(description = "출결 진행 여부", examples = {
"CLOSED", "OPEN"
})
AttendanceOpenStatus isOpened,
@Schema(description = "출결 형식")
AttendanceType attendanceType,
@Schema(description = "마감된 출석에 대한 출결 결과", nullable = true)
AttendanceResult attendanceResult
) {
public static MemberAttendResponse closedAttendanceResponse(Session session, AttendanceRecord attendanceRecord) {
return new MemberAttendResponse(
attendanceRecord.getMemberId(),
session.getTitle(),
session.getSessionDate(),
AttendanceOpenStatus.CLOSED,
attendanceRecord.getAttendanceType(),
attendanceRecord.getAttendanceResult()
);
}

public static MemberAttendResponse openedAttendanceResponse(Session session, Long memberId) {
return new MemberAttendResponse(
memberId,
session.getTitle(),
session.getSessionDate(),
AttendanceOpenStatus.OPEN,
null,
null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.cotato.csquiz.api.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record MemberAttendanceRecordsResponse(
@Schema(description = "요청한 기수 PK")
Long generationId,
List<MemberAttendResponse> memberAttendResponses
) {
public static MemberAttendanceRecordsResponse of(Long generationId, List<MemberAttendResponse> memberAttendResponses) {
return new MemberAttendanceRecordsResponse(
generationId,
memberAttendResponses
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.cotato.csquiz.common.entity.BaseTimeEntity;
import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceResult;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

@Table(name = "attendance_record",
Expand All @@ -39,9 +39,9 @@ public class AttendanceRecord extends BaseTimeEntity {
@Enumerated(EnumType.STRING)
private AttendanceType attendanceType;

@Column(name = "attendance_status", nullable = false)
@Column(name = "attendance_result", nullable = false)
@Enumerated(EnumType.STRING)
private AttendanceStatus attendanceStatus;
private AttendanceResult attendanceResult;

@Column(name = "location_accuracy")
private Double locationAccuracy;
Expand All @@ -56,21 +56,21 @@ public class AttendanceRecord extends BaseTimeEntity {
@Column(name = "attend_time", nullable = false)
private LocalDateTime attendTime;

private AttendanceRecord(AttendanceType attendanceType, AttendanceStatus attendanceStatus, Double locationAccuracy,
private AttendanceRecord(AttendanceType attendanceType, AttendanceResult attendanceResult, Double locationAccuracy,
Long memberId, Attendance attendance, LocalDateTime attendTime) {
this.attendanceType = attendanceType;
this.attendanceStatus = attendanceStatus;
this.attendanceResult = attendanceResult;
this.locationAccuracy = locationAccuracy;
this.memberId = memberId;
this.attendance = attendance;
this.attendTime = attendTime;
}

public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId, AttendanceStatus attendanceStatus,
public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId, AttendanceResult attendanceResult,
LocalDateTime attendTime) {
return new AttendanceRecord(
AttendanceType.ONLINE,
attendanceStatus,
attendanceResult,
null,
memberId,
attendance,
Expand All @@ -79,10 +79,10 @@ public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId
}

public static AttendanceRecord offlineRecord(Attendance attendance, Long memberId, Double locationAccuracy,
AttendanceStatus attendanceStatus, LocalDateTime attendTime) {
AttendanceResult attendanceResult, LocalDateTime attendTime) {
return new AttendanceRecord(
AttendanceType.OFFLINE,
attendanceStatus,
attendanceResult,
locationAccuracy,
memberId,
attendance,
Expand All @@ -98,7 +98,7 @@ public void updateLocationAccuracy(Double accuracy) {
this.locationAccuracy = accuracy;
}

public void updateAttendanceStatus(AttendanceStatus attendanceStatus) {
this.attendanceStatus = attendanceStatus;
public void updateAttendanceStatus(AttendanceResult attendanceResult) {
this.attendanceResult = attendanceResult;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
@Getter
@AllArgsConstructor
public enum AttendanceOpenStatus {
CLOSED("출결 진행 중이 아닙니다."),
CLOSED("출결 입력 기간이 마감되었습니다."),
OPEN("현재 출석 진행 중"),
LATE("현재 출결 입력 시 지각"),
ABSENT("현재 출결 입력 시 결석")
ABSENT("현재 출결 입력 시 결석"),
BEFORE("아직 출석 시작 전입니다. 오지 않았습니다.")
;

private final String description;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@Getter
@AllArgsConstructor
public enum AttendanceStatus {
public enum AttendanceResult {
PRESENT("출석", "출석에 성공하셨습니다."),
LATE("지각", "기준 시간을 지나 지각 처리 되었습니다."),
ABSENT("결석", "지각 마감 시간을 지나 결석 처리 되었습니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.cotato.csquiz.domain.attendance.enums;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -16,4 +18,12 @@ public enum DeadLine {

private final LocalTime time;
private final String description;

public static LocalDateTime sessionStartTime(LocalDate date) {
return LocalDateTime.of(date, ATTENDANCE_START_TIME.getTime());
}

public static LocalDateTime sessionEndTime(LocalDate date) {
return LocalDateTime.of(date, ATTENDANCE_END_TIME.getTime());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ public interface AttendanceRecordRepository extends JpaRepository<AttendanceReco

Optional<AttendanceRecord> findByMemberIdAndAttendanceId(Long memberId, Long attendanceId);

@Query("select a from AttendanceRecord a where a.attendance.id in :attendanceIds and a.memberId = :memberId")
List<AttendanceRecord> findAllByAttendanceIdsInQueryAndMemberId(@Param("attendanceIds") List<Long> attendanceIds, @Param("memberId") Long memberId);

List<AttendanceRecord> findAllByAttendanceId(Long attendanceId);
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
package org.cotato.csquiz.domain.attendance.service;

import static org.cotato.csquiz.domain.attendance.util.AttendanceUtil.getAttendanceStatus;
import static org.cotato.csquiz.domain.attendance.util.AttendanceUtil.getAttendanceOpenStatus;

import jakarta.persistence.EntityNotFoundException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.attendance.dto.AttendResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceParams;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceStatistic;
import org.cotato.csquiz.api.attendance.dto.MemberAttendResponse;
import org.cotato.csquiz.api.attendance.dto.MemberAttendanceRecordsResponse;
import org.cotato.csquiz.common.error.ErrorCode;
import org.cotato.csquiz.common.error.exception.AppException;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceResult;
import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository;
import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository;
import org.cotato.csquiz.domain.attendance.util.AttendanceUtil;
import org.cotato.csquiz.domain.auth.entity.Member;
import org.cotato.csquiz.domain.auth.service.MemberService;
import org.cotato.csquiz.domain.generation.entity.Session;
import org.cotato.csquiz.domain.generation.repository.SessionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -36,6 +42,7 @@ public class AttendanceRecordService {
private final AttendanceRepository attendanceRepository;
private final MemberService memberService;
private final RequestAttendanceService requestAttendanceService;
private final SessionRepository sessionRepository;

public List<AttendanceRecordResponse> generateAttendanceResponses(List<Attendance> attendances) {
List<AttendanceRecord> records = attendanceRecordRepository.findAllByAttendanceIdsInQuery(
Expand All @@ -60,7 +67,7 @@ public AttendResponse submitRecord(AttendanceParams request, final Long memberId
.orElseThrow(() -> new EntityNotFoundException("해당 출석이 존재하지 않습니다."));

// 해당 출석이 열려있는지 확인, 닫혀있으면 제외
if (getAttendanceStatus(attendance, request.requestTime()) == AttendanceOpenStatus.CLOSED) {
if (getAttendanceOpenStatus(attendance, request.requestTime()) == AttendanceOpenStatus.CLOSED) {
throw new AppException(ErrorCode.ATTENDANCE_CLOSED);
}

Expand All @@ -73,13 +80,47 @@ public AttendResponse submitRecord(AttendanceParams request, final Long memberId
return requestAttendanceService.attend(request, memberId, attendance);
}

public MemberAttendanceRecordsResponse findAllRecordsBy(final Long generationId, final Long memberId) {
List<Session> sessions = sessionRepository.findAllByGenerationId(generationId);

Map<Long, Session> sessionMap = sessions.stream()
.collect(Collectors.toUnmodifiableMap(Session::getId, Function.identity()));

List<Long> sessionIds = sessions.stream()
.map(Session::getId)
.toList();
// 세션에 해당하는 모든 출결을 찾아
LocalDateTime currentTime = LocalDateTime.now();

Map<Boolean, List<Attendance>> isClosedAttendance = attendanceRepository.findAllBySessionIdsInQuery(sessionIds)
.stream()
.collect(Collectors.partitioningBy(attendance ->
getAttendanceOpenStatus(attendance, currentTime) == AttendanceOpenStatus.CLOSED));

List<Long> closedAttendanceIds = isClosedAttendance.get(true).stream()
.map(Attendance::getId)
.toList();

List<MemberAttendResponse> responses = attendanceRecordRepository.findAllByAttendanceIdsInQueryAndMemberId(closedAttendanceIds, memberId).stream()
.map(ar -> MemberAttendResponse.closedAttendanceResponse(
sessionMap.get(ar.getAttendance().getSessionId()), ar))
.collect(Collectors.toList());

responses.addAll(isClosedAttendance.get(false).stream()
.map(attendance -> MemberAttendResponse.openedAttendanceResponse(
sessionMap.get(attendance.getSessionId()), memberId))
.toList());

return MemberAttendanceRecordsResponse.of(generationId, responses);
}

@Transactional
public void updateAttendanceStatus(Attendance attendance) {
List<AttendanceRecord> attendanceRecords = attendanceRecordRepository.findAllByAttendanceId(attendance.getId());

for (AttendanceRecord attendanceRecord : attendanceRecords) {
AttendanceStatus attendanceStatus = AttendanceUtil.calculateAttendanceStatus(attendance, attendanceRecord.getAttendTime());
attendanceRecord.updateAttendanceStatus(attendanceStatus);
AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(attendance, attendanceRecord.getAttendTime());
attendanceRecord.updateAttendanceStatus(attendanceResult);
}

attendanceRecordRepository.saveAll(attendanceRecords);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public AttendancesResponse findAttendancesByGenerationId(final Long generationId
.attendanceId(at.getId())
.sessionTitle(sessionMap.get(at.getSessionId()).getTitle())
.sessionDate(at.getAttendanceDeadLine().toLocalDate())
.openStatus(AttendanceUtil.getAttendanceStatus(at, currentTime))
.openStatus(AttendanceUtil.getAttendanceOpenStatus(at, currentTime))
.build())
.toList();

Expand Down
Loading

0 comments on commit f1a5ab8

Please sign in to comment.