Skip to content

Commit b6cdf20

Browse files
Merge pull request #163 from prgrms-web-devcourse-final-project/develop
chore: develop → main 브랜치 머지
2 parents 1af4a24 + 1acc0c3 commit b6cdf20

File tree

86 files changed

+2705
-645
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+2705
-645
lines changed

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ dependencies {
8686
// 배포 관련 의존성
8787
// runtimeOnly 'org.postgresql:postgresql'
8888
implementation 'org.springframework.boot:spring-boot-devtools'
89-
9089
implementation 'com.google.cloud:spring-cloud-gcp-starter-secretmanager:4.9.1'
9190
implementation 'com.google.cloud:google-cloud-storage:2.38.0'
91+
92+
// WebSocket + STOMP 통신용
93+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
9294
}
9395

9496
tasks.named('test') {

src/main/java/grep/neogul_coder/domain/attendance/Attendance.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import grep.neogul_coder.global.entity.BaseEntity;
44
import jakarta.persistence.*;
5+
import lombok.Builder;
6+
import lombok.Getter;
57

6-
import java.time.LocalDate;
8+
import java.time.LocalDateTime;
79

10+
@Getter
811
@Entity
912
public class Attendance extends BaseEntity {
1013

1114
@Id
1215
@GeneratedValue(strategy = GenerationType.IDENTITY)
13-
private Long attendanceId;
16+
private Long id;
1417

1518
@Column(nullable = false)
1619
private Long studyId;
@@ -19,5 +22,22 @@ public class Attendance extends BaseEntity {
1922
private Long userId;
2023

2124
@Column(nullable = false)
22-
private LocalDate attendanceDate;
25+
private LocalDateTime attendanceDate;
26+
27+
protected Attendance() {}
28+
29+
@Builder
30+
private Attendance(Long studyId, Long userId, LocalDateTime attendanceDate) {
31+
this.studyId = studyId;
32+
this.userId = userId;
33+
this.attendanceDate = attendanceDate;
34+
}
35+
36+
public static Attendance create(Long studyId, Long userId) {
37+
return Attendance.builder()
38+
.studyId(studyId)
39+
.userId(userId)
40+
.attendanceDate(LocalDateTime.now())
41+
.build();
42+
}
2343
}
Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
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;
4+
import grep.neogul_coder.domain.attendance.service.AttendanceService;
5+
import grep.neogul_coder.global.auth.Principal;
46
import grep.neogul_coder.global.response.ApiResponse;
5-
import org.springframework.web.bind.annotation.GetMapping;
6-
import org.springframework.web.bind.annotation.PostMapping;
7-
import org.springframework.web.bind.annotation.RequestMapping;
8-
import org.springframework.web.bind.annotation.RestController;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
9+
import org.springframework.web.bind.annotation.*;
910

10-
import java.util.List;
11-
12-
@RequestMapping("/api/attendances")
11+
@RequestMapping("/api/studies/{studyId}/attendances")
12+
@RequiredArgsConstructor
1313
@RestController
1414
public class AttendanceController implements AttendanceSpecification {
1515

16+
private final AttendanceService attendanceService;
17+
1618
@GetMapping
17-
public ApiResponse<List<AttendanceResponse>> getAttendances() {
18-
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);
1923
}
2024

2125
@PostMapping
22-
public ApiResponse<Void> createAttendance() {
23-
return ApiResponse.noContent();
26+
public ApiResponse<Long> createAttendance(@PathVariable("studyId") Long studyId,
27+
@AuthenticationPrincipal Principal userDetails) {
28+
Long userId = userDetails.getUserId();
29+
Long id = attendanceService.createAttendance(studyId, userId);
30+
return ApiResponse.success(id);
2431
}
2532
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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;
5+
import grep.neogul_coder.global.auth.Principal;
46
import grep.neogul_coder.global.response.ApiResponse;
57
import io.swagger.v3.oas.annotations.Operation;
68
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -11,8 +13,8 @@
1113
public interface AttendanceSpecification {
1214

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

1618
@Operation(summary = "출석 체크", description = "스터디에 출석을 합니다.")
17-
ApiResponse<Void> createAttendance();
19+
ApiResponse<Long> createAttendance(Long studyId, Principal userDetails);
1820
}
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
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package grep.neogul_coder.domain.attendance.exception.code;
2+
3+
import grep.neogul_coder.global.response.code.ErrorCode;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
public enum AttendanceErrorCode implements ErrorCode {
9+
10+
ATTENDANCE_ALREADY_CHECKED("A001", HttpStatus.BAD_REQUEST, "출석은 하루에 한 번만 가능합니다.");
11+
12+
private final String code;
13+
private final HttpStatus status;
14+
private final String message;
15+
16+
AttendanceErrorCode(String code, HttpStatus status, String message) {
17+
this.code = code;
18+
this.status = status;
19+
this.message = message;
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package grep.neogul_coder.domain.attendance.repository;
2+
3+
import grep.neogul_coder.domain.attendance.Attendance;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.List;
10+
11+
public interface AttendanceRepository extends JpaRepository<Attendance, Long> {
12+
@Query("select count(a) > 0 from Attendance a where a.studyId = :studyId and a.userId = :userId and a.attendanceDate between :startOfDay and :endOfDay")
13+
boolean existsTodayAttendance(@Param("studyId") Long studyId,
14+
@Param("userId") Long userId,
15+
@Param("startOfDay") LocalDateTime startOfDay,
16+
@Param("endOfDay") LocalDateTime endOfDay);
17+
18+
List<Attendance> findByStudyIdAndUserId(Long studyId, Long userId);
19+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package grep.neogul_coder.domain.attendance.service;
2+
3+
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;
6+
import grep.neogul_coder.domain.attendance.repository.AttendanceRepository;
7+
import grep.neogul_coder.domain.study.Study;
8+
import grep.neogul_coder.domain.study.repository.StudyMemberRepository;
9+
import grep.neogul_coder.domain.study.repository.StudyRepository;
10+
import grep.neogul_coder.global.exception.business.BusinessException;
11+
import grep.neogul_coder.global.exception.business.NotFoundException;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.time.LocalDate;
17+
import java.time.LocalDateTime;
18+
import java.time.LocalTime;
19+
import java.time.temporal.ChronoUnit;
20+
import java.util.List;
21+
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;
24+
25+
@Transactional(readOnly = true)
26+
@RequiredArgsConstructor
27+
@Service
28+
public class AttendanceService {
29+
30+
private final AttendanceRepository attendanceRepository;
31+
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+
}
47+
48+
@Transactional
49+
public Long createAttendance(Long studyId, Long userId) {
50+
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
51+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
52+
53+
validateNotAlreadyChecked(studyId, userId);
54+
55+
Attendance attendance = Attendance.create(studyId, userId);
56+
attendanceRepository.save(attendance);
57+
return attendance.getId();
58+
}
59+
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+
73+
private void validateNotAlreadyChecked(Long studyId, Long userId) {
74+
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
75+
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
76+
77+
if (attendanceRepository.existsTodayAttendance(studyId, userId, startOfDay, endOfDay)) {
78+
throw new BusinessException(ATTENDANCE_ALREADY_CHECKED);
79+
}
80+
}
81+
}

src/main/java/grep/neogul_coder/domain/comment/Comment.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)