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: 대면 비대면 출석 기능 구현 #87

Merged
merged 18 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package org.cotato.csquiz.api.attendance.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
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.AttendResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse;
import org.cotato.csquiz.api.attendance.dto.AttendancesResponse;
import org.cotato.csquiz.api.attendance.dto.OfflineAttendanceRequest;
import org.cotato.csquiz.api.attendance.dto.OnlineAttendanceRequest;
import org.cotato.csquiz.api.attendance.dto.UpdateAttendanceRequest;
import org.cotato.csquiz.domain.attendance.service.AttendanceAdminService;
import org.cotato.csquiz.domain.attendance.service.AttendanceRecordService;
import org.cotato.csquiz.domain.attendance.service.AttendanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -33,6 +40,7 @@ public class AttendanceController {

private final AttendanceAdminService attendanceAdminService;
private final AttendanceService attendanceService;
private final AttendanceRecordService attendanceRecordService;

@Operation(summary = "출석 정보 변경 API")
@PatchMapping
Expand Down Expand Up @@ -62,4 +70,47 @@ public ResponseEntity<List<AttendanceRecordResponse>> findAttendanceRecordsByAtt
public ResponseEntity<AttendancesResponse> findAttendancesByGeneration(@RequestParam("generationId") Long generationId) {
return ResponseEntity.ok().body(attendanceService.findAttendancesByGenerationId(generationId));
}

@Operation(summary = "대면 출결 입력 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "성공"
),
@ApiResponse(
responseCode = "AT-301",
description = "이미 출석을 완료함"
),
@ApiResponse(
responseCode = "AT-401",
description = "출석 시간이 아님"
)
}
)
@PostMapping(value = "/records/offline")
public ResponseEntity<AttendResponse> submitOfflineAttendanceRecord(@RequestBody OfflineAttendanceRequest request,
@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok().body(attendanceRecordService.submitRecord(request, memberId));
}

@Operation(summary = "비대면 출결 입력 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "성공"
),
@ApiResponse(
responseCode = "AT-301",
description = "이미 출석을 완료함"
),
@ApiResponse(
responseCode = "AT-401",
description = "출석 시간이 아님"
)
})
@PostMapping(value = "/records/online")
public ResponseEntity<AttendResponse> submitOnlineAttendanceRecord(@RequestBody OnlineAttendanceRequest request,
@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok().body(attendanceRecordService.submitRecord(request, memberId));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cotato.csquiz.api.attendance.dto;

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

public record AttendResponse(
AttendanceStatus status,
String message
) {
public static AttendResponse from(AttendanceStatus status) {
return new AttendResponse(
status,
status.getMessage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.cotato.csquiz.api.attendance.dto;

import java.time.LocalDateTime;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

public interface AttendanceParams {

AttendanceType attendanceType();

Long attendanceId();

LocalDateTime requestTime();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.cotato.csquiz.api.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.cotato.csquiz.domain.attendance.embedded.Location;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

@Getter
@NoArgsConstructor
public class OfflineAttendanceRequest implements AttendanceParams {

@Schema(description = "출석 PK")
@NotNull
private Long attendanceId;

@Schema(description = "회원 요청 시간")
private LocalDateTime requestTime;

@Schema(description = "사용자 요청 위치")
@NotNull
private Location location;

@Override
public AttendanceType attendanceType() {
return AttendanceType.OFFLINE;
}

@Override
public Long attendanceId() {
return attendanceId;
}

@Override
public LocalDateTime requestTime() {
return requestTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.cotato.csquiz.api.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.Getter;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

@Getter
@NotNull
public class OnlineAttendanceRequest implements AttendanceParams {

@Schema(description = "출석 PK")
@NotNull
private Long attendanceId;

@Schema(description = "회원 요청 시간")
private LocalDateTime requestTime;

@Override
public AttendanceType attendanceType() {
return AttendanceType.ONLINE;
}

@Override
public Long attendanceId() {
return attendanceId;
}

@Override
public LocalDateTime requestTime() {
return requestTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public record UpdateAttendanceRequest(
@NotNull
Long attendanceId,
Location location,
AttendanceDeadLineDto attendanceDeadLine
AttendanceDeadLineDto attendTime
) {

public UpdateAttendanceRequest {
if (Objects.isNull(attendanceDeadLine)) {
attendanceDeadLine = new AttendanceDeadLineDto(DEFAULT_ATTENDANCE_DEADLINE.getTime(),
if (Objects.isNull(attendTime)) {
attendTime = new AttendanceDeadLineDto(DEFAULT_ATTENDANCE_DEADLINE.getTime(),
DEFAULT_LATE_DEADLINE.getTime());
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/cotato/csquiz/common/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public enum ErrorCode {

LAST_QUIZ_SCORER_NOT_EXIST(HttpStatus.CONFLICT, "W-201", "아직 마지막 문제 득점자가 없습니다"),
WINNER_EXIST(HttpStatus.CONFLICT, "W-301", "이미 우승자가 존재합니다"),

//출석 관련 AT
OFFLINE_ATTEND_FAIL(HttpStatus.BAD_REQUEST, "AT-101", "거리 부적합으로 인한 대면 출석 실패"),
ALREADY_ATTEND(HttpStatus.CONFLICT, "AT-301", "이미 해당 타입으로 출석한 기록이 있습니다."),
ATTENDANCE_CLOSED(HttpStatus.CONFLICT, "AT-401", "아직 출석 시간이 아닙니다."),

// 500 오류 -> 서버측에서 처리가 실패한 부분들
WEBSOCKET_SEND_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "소캣 메세지 전송 실패"),
IMAGE_PROCESSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-002", "이미지 처리에 실패했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ private Location(Double latitude, Double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}

public Double calculateAccuracy(Location location) {
return Math.pow(this.latitude - location.latitude, 2) + Math.pow(this.longitude - location.longitude, 2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
Expand All @@ -18,7 +19,7 @@
import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

@Table(name = "attendance_record")
@Table(name = "attendance_record", indexes = {@Index(name = "member_id_index", columnList = "member_id")})
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down Expand Up @@ -46,4 +47,35 @@ public class AttendanceRecord extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "attendance_id")
private Attendance attendance;

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

public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId,
AttendanceStatus attendanceStatus) {
return new AttendanceRecord(
AttendanceType.ONLINE,
attendanceStatus,
null,
gikhoon marked this conversation as resolved.
Show resolved Hide resolved
memberId,
attendance
);
}

public static AttendanceRecord offlineRecord(Attendance attendance, Long memberId, Double locationAccuracy,
AttendanceStatus attendanceStatus) {
return new AttendanceRecord(
AttendanceType.OFFLINE,
attendanceStatus,
locationAccuracy,
memberId,
attendance
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
@Getter
@AllArgsConstructor
public enum AttendanceStatus {
PRESENT("출석"),
LATE("지각"),
PRESENT("출석", "출석에 성공하셨습니다."),
LATE("지각", "기준 시간을 지나 지각 처리 되었습니다."),
ABSENT("결석", "지각 마감 시간을 지나 결석 처리 되었습니다.")
;

private final String description;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import java.util.List;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;
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);

boolean existsByAttendanceIdAndMemberIdAndAttendanceType(Long attendanceId, Long memberId, AttendanceType attendanceType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.cotato.csquiz.domain.attendance.service;

import org.cotato.csquiz.api.attendance.dto.AttendResponse;
import org.cotato.csquiz.api.attendance.dto.AttendanceParams;
import org.cotato.csquiz.domain.attendance.entity.Attendance;
import org.cotato.csquiz.domain.attendance.enums.AttendanceType;

public interface AttendClient {
AttendanceType attendanceType();

AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
@Slf4j
public class AttendanceAdminService {

private static final int DEFAULT_ATTEND_SECOND = 59;
private final AttendanceRepository attendanceRepository;
private final AttendanceRecordService attendanceRecordService;
private final SessionRepository sessionRepository;
Expand All @@ -35,8 +36,10 @@ public void addAttendance(Session session, Location location, AttendanceDeadLine
Attendance attendance = Attendance.builder()
.session(session)
.location(location)
.attendanceDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.attendanceDeadLine()))
.lateDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.lateDeadLine()))
.attendanceDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.attendanceDeadLine())
.plusSeconds(DEFAULT_ATTEND_SECOND))
.lateDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.lateDeadLine())
.plusSeconds(DEFAULT_ATTEND_SECOND))
.build();

attendanceRepository.save(attendance);
Expand All @@ -54,9 +57,11 @@ public void updateAttendance(UpdateAttendanceRequest request) {
throw new AppException(ErrorCode.SESSION_DATE_NOT_FOUND);
}

attendance.updateDeadLine(LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine()
.attendanceDeadLine()), LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine()
.lateDeadLine()));
attendance.updateDeadLine(
LocalDateTime.of(attendanceSession.getSessionDate(), request.attendTime().attendanceDeadLine())
.plusSeconds(DEFAULT_ATTEND_SECOND),
LocalDateTime.of(attendanceSession.getSessionDate(), request.attendTime()
.lateDeadLine()).plusSeconds(DEFAULT_ATTEND_SECOND));
attendance.updateLocation(request.location());
}

Expand Down
Loading
Loading