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

Feature: 부원들 출석 정보 열람 기능 구현 #80

Merged
merged 9 commits into from
Aug 5, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.cotato.csquiz.api.attendance.controller;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.UpdateAttendanceRequest;
import org.cotato.csquiz.domain.attendance.service.AttendanceAdminService;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@Validated
@RequiredArgsConstructor
@RequestMapping("/v2/api/attendances")
public class AttendanceController {

private final AttendanceAdminService attendanceAdminService;

@Operation(summary = "출석 정보 변경 API")
@PatchMapping
public ResponseEntity<Void> updateAttendance(@RequestBody @Valid UpdateAttendanceRequest request) {
attendanceAdminService.updateAttendance(request);
return ResponseEntity.noContent().build();
}

@Operation(summary = "회원 출결사항 기간 단위 조회 API")
@GetMapping("/records")
public ResponseEntity<List<AttendanceRecordResponse>> findAttendanceRecords(
@RequestParam(name = "generationId") Long generationId,
@RequestParam(name = "month", required = false) @Min(value = 1, message = "달은 1 이상이어야 합니다.") @Max(value = 12, message = "달은 12 이하이어야 합니다") Integer month
) {
return ResponseEntity.ok().body(attendanceAdminService.findAttendanceRecords(generationId, month));
}

@Operation(summary = "회원 출결사항 출석 단위 조회 API")
@GetMapping("/{attendance-id}/records")
public ResponseEntity<List<AttendanceRecordResponse>> findAttendanceRecordsByAttendance(
@PathVariable("attendance-id") Long attendanceId) {
return ResponseEntity.ok().body(attendanceAdminService.findAttendanceRecordsByAttendance(attendanceId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.cotato.csquiz.api.attendance.dto;

import org.cotato.csquiz.domain.auth.entity.Member;
import org.cotato.csquiz.domain.auth.enums.MemberPosition;


public record AttendanceRecordResponse(
AttendanceMemberInfo memberInfo,
AttendanceStatistic statistic
) {
public static AttendanceRecordResponse of(Member member, AttendanceStatistic attendanceStatistic) {
return new AttendanceRecordResponse(
AttendanceMemberInfo.from(member),
attendanceStatistic
);
}

public record AttendanceMemberInfo(
Long memberId,
String memberName,
MemberPosition position
){
static AttendanceMemberInfo from(Member member) {
return new AttendanceMemberInfo(
member.getId(),
member.getName(),
member.getPosition()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.cotato.csquiz.api.attendance.dto;

import java.util.List;
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.AttendanceType;

public record AttendanceStatistic(
Integer onLine,
Integer offLine,
Integer late,
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());

int onlineCount = (int) presentRecords.stream()
.filter(record -> AttendanceType.ONLINE == record.getAttendanceType())
.count();
int offLineCount = (int) presentRecords.stream()
.filter(record -> AttendanceType.OFFLINE == record.getAttendanceType())
.count();

return new AttendanceStatistic(
onlineCount,
offLineCount,
countByStatus.getOrDefault(AttendanceStatus.LATE, List.of()).size(),
totalAttendance - attendanceRecords.size()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public record AddSessionRequest(
String placeName,
@NotNull
LocalDate sessionDate,

@Valid
@NotNull
AttendanceDeadLineDto attendanceDeadLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/v1/api/session/cs-on").hasAnyRole("EDUCATION", "ADMIN")
.requestMatchers(new AntPathRequestMatcher("/v1/api/session", "GET")).authenticated()
.requestMatchers("/v1/api/session/**").hasAnyRole("ADMIN")
.requestMatchers("/v2/api/attendance/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/v2/api/attendance/records").hasAnyRole("ADMIN")
.requestMatchers("/v2/api/attendance").hasAnyRole("ADMIN")
.requestMatchers(new AntPathRequestMatcher("/v1/api/socket/token", "POST"))
.hasAnyRole("MEMBER", "EDUCATION", "ADMIN")
.requestMatchers("/v1/api/socket/**").hasAnyRole("EDUCATION", "ADMIN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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.AttendanceType;

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

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

@Column(name = "location_accuracy")
private Double locationAccuracy;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cotato.csquiz.domain.attendance.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum AttendanceStatus {
PRESENT("출석"),
LATE("지각"),
;

private final String description;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.cotato.csquiz.domain.attendance.repository;

import java.util.List;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AttendanceRecordRepository extends JpaRepository<AttendanceRecord, Long> {
@Query("select a from AttendanceRecord a where a.attendance in :attendances")
List<AttendanceRecord> findAllByAttendanceIdsInQuery(@Param("attendances") List<Attendance> attendances);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package org.cotato.csquiz.domain.attendance.repository;

import java.util.List;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AttendanceRepository extends JpaRepository<Attendance, Long> {
@Query("select a from Attendance a where a.sessionId in :sessionIds")
List<Attendance> findAllBySessionIdsInQuery(@Param("sessionIds") List<Long> sessionIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import jakarta.persistence.EntityNotFoundException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.attendance.dto.AttendanceDeadLineDto;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.UpdateAttendanceRequest;
import org.cotato.csquiz.api.attendance.dto.AttendanceDeadLineDto;
import org.cotato.csquiz.common.error.ErrorCode;
import org.cotato.csquiz.common.error.exception.AppException;
import org.cotato.csquiz.domain.attendance.embedded.Location;
Expand All @@ -25,11 +28,11 @@
public class AttendanceAdminService {

private final AttendanceRepository attendanceRepository;
private final AttendanceRecordService attendanceRecordService;
private final SessionRepository sessionRepository;

@Transactional
public void addAttendance(Session session, Location location,
AttendanceDeadLineDto attendanceDeadLine) {
public void addAttendance(Session session, Location location, AttendanceDeadLineDto attendanceDeadLine) {

Attendance attendance = Attendance.builder()
.session(session)
Expand All @@ -53,12 +56,32 @@ public void updateAttendance(UpdateAttendanceRequest request) {
throw new AppException(ErrorCode.SESSION_DATE_NOT_FOUND);
}

LocalDate sessionDate = attendanceSession.getSessionDate();
AttendanceDeadLineDto deadLine = request.attendanceDeadLine();
LocalDateTime startLocalDateTime = LocalDateTime.of(sessionDate, deadLine.startTime());
LocalDateTime endLocalDateTime = LocalDateTime.of(sessionDate, deadLine.endTime());

attendance.updateDeadLine(startLocalDateTime, endLocalDateTime);
attendance.updateDeadLine(LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine()
.startTime()), LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine()
.endTime()));
attendance.updateLocation(request.location());
}

public List<AttendanceRecordResponse> findAttendanceRecords(Long generationId, Integer month) {
List<Session> sessions = sessionRepository.findAllByGenerationId(generationId);
if (month != null) {
sessions = sessions.stream()
.filter(session -> session.getSessionDate().getMonthValue() == month)
.toList();
}
List<Long> sessionIds = sessions.stream()
.map(Session::getId)
.toList();

List<Attendance> attendances = attendanceRepository.findAllBySessionIdsInQuery(sessionIds);

return attendanceRecordService.generateAttendanceResponses(attendances);
}

public List<AttendanceRecordResponse> findAttendanceRecordsByAttendance(Long attendanceId){
Attendance attendance = attendanceRepository.findById(attendanceId)
.orElseThrow(() -> new EntityNotFoundException("해당 출석이 존재하지 않습니다"));

return attendanceRecordService.generateAttendanceResponses(List.of(attendance));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.cotato.csquiz.domain.attendance.service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceStatistic;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository;
import org.cotato.csquiz.domain.auth.entity.Member;
import org.cotato.csquiz.domain.auth.service.MemberService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class AttendanceRecordService {

private final AttendanceRecordRepository attendanceRecordRepository;
private final MemberService memberService;

public List<AttendanceRecordResponse> generateAttendanceResponses(List<Attendance> attendances) {
List<AttendanceRecord> records = attendanceRecordRepository.findAllByAttendanceIdsInQuery(
attendances);

Map<Long, List<AttendanceRecord>> recordsByMemberId = records.stream()
.collect(Collectors.groupingBy(AttendanceRecord::getMemberId));

Map<Long, Member> memberMap = memberService.findActiveMember().stream()
.collect(Collectors.toMap(Member::getId, member -> member));

return recordsByMemberId.keySet().stream()
.filter(memberMap::containsKey)
.map(memberId -> AttendanceRecordResponse.of(memberMap.get(memberId),
AttendanceStatistic.of(recordsByMemberId.get(memberId), attendances.size())))
.toList();
}
Youthhing marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.cotato.csquiz.domain.auth.enums;

import org.cotato.csquiz.common.error.exception.AppException;
import org.cotato.csquiz.common.error.ErrorCode;
import java.util.Arrays;
import lombok.Getter;
import org.cotato.csquiz.common.error.ErrorCode;
import org.cotato.csquiz.common.error.exception.AppException;

@Getter
public enum MemberRole {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cotato.csquiz.domain.auth.service;

import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.admin.dto.MemberInfoResponse;
Expand All @@ -13,6 +14,7 @@
import org.cotato.csquiz.common.error.exception.AppException;
import org.cotato.csquiz.common.error.exception.ImageException;
import org.cotato.csquiz.domain.auth.entity.Member;
import org.cotato.csquiz.domain.auth.enums.MemberRoleGroup;
import org.cotato.csquiz.domain.auth.repository.MemberRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -125,4 +127,8 @@ public MemberInfo getMemberInfo(Long memberId) {
.orElseThrow(() -> new EntityNotFoundException("해당 멤버를 찾을 수 없습니다."));
return MemberInfo.of(findMember, findBackFourNumber(findMember));
}

public List<Member> findActiveMember() {
return memberRepository.findAllByRoleInQuery(MemberRoleGroup.ACTIVE_MEMBERS.getRoles());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
public interface SessionRepository extends JpaRepository<Session, Long> {
List<Session> findAllByGeneration(Generation generation);

List<Session> findAllByGenerationId(Long generationId);

List<Session> findAllByGenerationAndSessionContentsCsEducation(Generation generation, CSEducation csEducation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,30 @@
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cotato.csquiz.api.session.dto.AddSessionImageResponse;
import org.cotato.csquiz.api.session.dto.DeleteSessionImageRequest;
import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderInfoRequest;
import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderRequest;
import org.cotato.csquiz.api.session.dto.AddSessionImageRequest;
import org.cotato.csquiz.api.session.dto.AddSessionImageResponse;
import org.cotato.csquiz.api.session.dto.AddSessionRequest;
import org.cotato.csquiz.api.session.dto.AddSessionResponse;
import org.cotato.csquiz.api.session.dto.CsEducationOnSessionNumberResponse;
import org.cotato.csquiz.api.session.dto.DeleteSessionImageRequest;
import org.cotato.csquiz.api.session.dto.SessionListResponse;
import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderInfoRequest;
import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderRequest;
import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest;
import org.cotato.csquiz.api.session.dto.UpdateSessionRequest;
import org.cotato.csquiz.common.S3.S3Uploader;
import org.cotato.csquiz.common.entity.S3Info;
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.common.error.exception.ImageException;
import org.cotato.csquiz.domain.attendance.service.AttendanceAdminService;
import org.cotato.csquiz.domain.education.entity.Education;
import org.cotato.csquiz.domain.education.service.EducationService;
import org.cotato.csquiz.domain.generation.embedded.SessionContents;
import org.cotato.csquiz.domain.generation.entity.SessionImage;
import org.cotato.csquiz.domain.generation.enums.CSEducation;
import org.cotato.csquiz.domain.generation.entity.Generation;
import org.cotato.csquiz.domain.generation.entity.Session;
import org.cotato.csquiz.common.error.exception.ImageException;
import org.cotato.csquiz.common.S3.S3Uploader;
import org.cotato.csquiz.domain.generation.entity.SessionImage;
import org.cotato.csquiz.domain.generation.enums.CSEducation;
import org.cotato.csquiz.domain.generation.repository.GenerationRepository;
import org.cotato.csquiz.domain.generation.repository.SessionImageRepository;
import org.cotato.csquiz.domain.generation.repository.SessionRepository;
Expand Down
Loading