Skip to content

Commit 6382e80

Browse files
committed
[EA3-162] feature: 출석 조회 기능 구현
1 parent 82e2bd4 commit 6382e80

File tree

7 files changed

+100
-11
lines changed

7 files changed

+100
-11
lines changed

src/main/java/grep/neogul_coder/domain/attendance/controller/AttendanceController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package grep.neogul_coder.domain.attendance.controller;
22

3-
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceResponse;
3+
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceInfoResponse;
44
import grep.neogul_coder.domain.attendance.service.AttendanceService;
55
import grep.neogul_coder.global.auth.Principal;
66
import grep.neogul_coder.global.response.ApiResponse;
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.security.core.annotation.AuthenticationPrincipal;
99
import org.springframework.web.bind.annotation.*;
1010

11-
import java.util.List;
12-
1311
@RequestMapping("/api/studies/{studyId}/attendances")
1412
@RequiredArgsConstructor
1513
@RestController
@@ -18,8 +16,10 @@ public class AttendanceController implements AttendanceSpecification {
1816
private final AttendanceService attendanceService;
1917

2018
@GetMapping
21-
public ApiResponse<List<AttendanceResponse>> getAttendances() {
22-
return ApiResponse.success(List.of(new AttendanceResponse()));
19+
public ApiResponse<AttendanceInfoResponse> getAttendances(@PathVariable("studyId") Long studyId,
20+
@AuthenticationPrincipal Principal userDetails) {
21+
AttendanceInfoResponse attendances = attendanceService.getAttendances(studyId, userDetails.getUserId());
22+
return ApiResponse.success(attendances);
2323
}
2424

2525
@PostMapping

src/main/java/grep/neogul_coder/domain/attendance/controller/AttendanceSpecification.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package grep.neogul_coder.domain.attendance.controller;
22

3+
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceInfoResponse;
34
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceResponse;
45
import grep.neogul_coder.global.auth.Principal;
56
import grep.neogul_coder.global.response.ApiResponse;
@@ -12,7 +13,7 @@
1213
public interface AttendanceSpecification {
1314

1415
@Operation(summary = "출석 조회", description = "일주일 단위로 출석을 조회합니다.")
15-
ApiResponse<List<AttendanceResponse>> getAttendances();
16+
ApiResponse<AttendanceInfoResponse> getAttendances(Long studyId, Principal userDetails);
1617

1718
@Operation(summary = "출석 체크", description = "스터디에 출석을 합니다.")
1819
ApiResponse<Long> createAttendance(Long studyId, Principal userDetails);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package grep.neogul_coder.domain.attendance.controller.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
public class AttendanceInfoResponse {
11+
12+
@Schema(description = "출석일 목록")
13+
private List<AttendanceResponse> attendances;
14+
15+
@Schema(description = "출석률", example = "50")
16+
private int attendanceRate;
17+
18+
@Builder
19+
private AttendanceInfoResponse(List<AttendanceResponse> attendances, int attendanceRate) {
20+
this.attendances = attendances;
21+
this.attendanceRate = attendanceRate;
22+
}
23+
24+
public static AttendanceInfoResponse of(List<AttendanceResponse> responses, int attendanceRate) {
25+
return AttendanceInfoResponse.builder()
26+
.attendances(responses)
27+
.attendanceRate(attendanceRate)
28+
.build();
29+
}
30+
}

src/main/java/grep/neogul_coder/domain/attendance/controller/dto/response/AttendanceResponse.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package grep.neogul_coder.domain.attendance.controller.dto.response;
22

3+
import grep.neogul_coder.domain.attendance.Attendance;
34
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.Builder;
46
import lombok.Getter;
57

68
import java.time.LocalDate;
@@ -16,4 +18,19 @@ public class AttendanceResponse {
1618

1719
@Schema(description = "출석일", example = "2025-07-10")
1820
private LocalDate attendanceDate;
21+
22+
@Builder
23+
private AttendanceResponse(Long studyId, Long userId, LocalDate attendanceDate) {
24+
this.studyId = studyId;
25+
this.userId = userId;
26+
this.attendanceDate = attendanceDate;
27+
}
28+
29+
public static AttendanceResponse from(Attendance attendance) {
30+
return AttendanceResponse.builder()
31+
.studyId(attendance.getStudyId())
32+
.userId(attendance.getUserId())
33+
.attendanceDate(attendance.getAttendanceDate().toLocalDate())
34+
.build();
35+
}
1936
}

src/main/java/grep/neogul_coder/domain/attendance/repository/AttendanceRepository.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
import org.springframework.data.repository.query.Param;
77

88
import java.time.LocalDateTime;
9+
import java.util.List;
910

1011
public interface AttendanceRepository extends JpaRepository<Attendance, Long> {
1112
@Query("select count(a) > 0 from Attendance a where a.studyId = :studyId and a.userId = :userId and a.attendanceDate between :startOfDay and :endOfDay")
1213
boolean existsTodayAttendance(@Param("studyId") Long studyId,
13-
@Param("userId") Long userId,
14-
@Param("startOfDay") LocalDateTime startOfDay,
15-
@Param("endOfDay") LocalDateTime endOfDay);
14+
@Param("userId") Long userId,
15+
@Param("startOfDay") LocalDateTime startOfDay,
16+
@Param("endOfDay") LocalDateTime endOfDay);
17+
18+
List<Attendance> findByStudyIdAndUserId(Long studyId, Long userId);
1619
}

src/main/java/grep/neogul_coder/domain/attendance/service/AttendanceService.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package grep.neogul_coder.domain.attendance.service;
22

33
import grep.neogul_coder.domain.attendance.Attendance;
4+
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceInfoResponse;
5+
import grep.neogul_coder.domain.attendance.controller.dto.response.AttendanceResponse;
46
import grep.neogul_coder.domain.attendance.repository.AttendanceRepository;
57
import grep.neogul_coder.domain.study.Study;
8+
import grep.neogul_coder.domain.study.repository.StudyMemberRepository;
69
import grep.neogul_coder.domain.study.repository.StudyRepository;
710
import grep.neogul_coder.global.exception.business.BusinessException;
811
import grep.neogul_coder.global.exception.business.NotFoundException;
@@ -13,9 +16,11 @@
1316
import java.time.LocalDate;
1417
import java.time.LocalDateTime;
1518
import java.time.LocalTime;
19+
import java.time.temporal.ChronoUnit;
20+
import java.util.List;
1621

17-
import static grep.neogul_coder.domain.attendance.exception.code.AttendanceErrorCode.*;
18-
import static grep.neogul_coder.domain.study.exception.code.StudyErrorCode.*;
22+
import static grep.neogul_coder.domain.attendance.exception.code.AttendanceErrorCode.ATTENDANCE_ALREADY_CHECKED;
23+
import static grep.neogul_coder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
1924

2025
@Transactional(readOnly = true)
2126
@RequiredArgsConstructor
@@ -24,6 +29,21 @@ public class AttendanceService {
2429

2530
private final AttendanceRepository attendanceRepository;
2631
private final StudyRepository studyRepository;
32+
private final StudyMemberRepository studyMemberRepository;
33+
34+
public AttendanceInfoResponse getAttendances(Long studyId, Long userId) {
35+
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
36+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
37+
38+
List<Attendance> attendances = attendanceRepository.findByStudyIdAndUserId(studyId, userId);
39+
List<AttendanceResponse> responses = attendances.stream()
40+
.map(AttendanceResponse::from)
41+
.toList();
42+
43+
int attendanceRate = getAttendanceRate(studyId, userId, study, responses);
44+
45+
return AttendanceInfoResponse.of(responses, attendanceRate);
46+
}
2747

2848
@Transactional
2949
public Long createAttendance(Long studyId, Long userId) {
@@ -37,9 +57,23 @@ public Long createAttendance(Long studyId, Long userId) {
3757
return attendance.getId();
3858
}
3959

60+
private int getAttendanceRate(Long studyId, Long userId, Study study, List<AttendanceResponse> responses) {
61+
LocalDate start = study.getStartDate().toLocalDate();
62+
LocalDate participated = studyMemberRepository.findCreatedDateByStudyIdAndUserId(studyId, userId).toLocalDate();
63+
LocalDate attendanceStart = start.isAfter(participated) ? start : participated;
64+
LocalDate end = study.getEndDate().toLocalDate();
65+
66+
int totalDays = (int) ChronoUnit.DAYS.between(attendanceStart, end) + 1;
67+
int attendDays = responses.size();
68+
int attendanceRate = totalDays == 0 ? 0 : Math.round(((float) attendDays / totalDays) * 100);
69+
70+
return attendanceRate;
71+
}
72+
4073
private void validateNotAlreadyChecked(Long studyId, Long userId) {
4174
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
4275
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
76+
4377
if (attendanceRepository.existsTodayAttendance(studyId, userId, startOfDay, endOfDay)) {
4478
throw new BusinessException(ATTENDANCE_ALREADY_CHECKED);
4579
}

src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.data.jpa.repository.Query;
88
import org.springframework.data.repository.query.Param;
99

10+
import java.time.LocalDateTime;
1011
import java.util.List;
1112
import java.util.Optional;
1213

@@ -31,4 +32,7 @@ public interface StudyMemberRepository extends JpaRepository<StudyMember, Long>
3132

3233
@Query("select m from StudyMember m where m.study.id = :studyId and m.role = 'MEMBER' and m.activated = true")
3334
List<StudyMember> findAvailableNewLeaders(@Param("studyId") Long studyId);
35+
36+
@Query("select m.createdDate from StudyMember m where m.study.id = :studyId and m.userId = :userId and m.activated = true")
37+
LocalDateTime findCreatedDateByStudyIdAndUserId(@Param("studyId") Long studyId, @Param("userId") Long userId);
3438
}

0 commit comments

Comments
 (0)