From 06e7fcb903495a98ea1876b24470b46c41f9cca9 Mon Sep 17 00:00:00 2001 From: GiHun Nam <52378919+gikhoon@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:42:01 +0900 Subject: [PATCH 1/7] =?UTF-8?q?Fix:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=A0=80=EC=9E=A5=EC=9D=84=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=97=90=20=EC=88=9C=EC=84=9C=EB=A5=BC=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 중복된 순서 저장을 방지하기 위해 요청에 순서를 명시하게 변경 * refactor: 세션 사진 에러코드명 변경 --- .../csquiz/api/session/dto/AddSessionImageRequest.java | 4 +++- .../java/org/cotato/csquiz/common/error/ErrorCode.java | 8 ++++---- .../generation/repository/SessionImageRepository.java | 3 +-- .../domain/generation/service/SessionImageService.java | 7 ++++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java index 1d21b8d1..1d8990d8 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java @@ -8,6 +8,8 @@ public record AddSessionImageRequest( @NotNull Long sessionId, @NotNull - MultipartFile image + MultipartFile image, + @NotNull + Integer order ) { } diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 11fc980e..839a257f 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -52,13 +52,13 @@ public enum ErrorCode { EDUCATION_STATUS_NOT_BEFORE(HttpStatus.BAD_REQUEST, "E-402", "이미 시작한 적이 있는 교육입니다."), MEMBER_CANT_ACCESS(HttpStatus.BAD_REQUEST, "E-403", "해당 멤버의 ROLE로 접근할 수 없습니다"), - //세션 사진 - SESSION_IMAGE_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "P-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), - SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "P-102", "입력한 순서는 유효하지 않습니다."), - FILE_EXTENSION_FAULT(HttpStatus.BAD_REQUEST, "F-001", "해당 파일 확장자 명이 존재하지 않습니다."), FILE_IS_EMPTY(HttpStatus.BAD_REQUEST, "F-002", "파일이 비어있습니다"), + //세션 사진 + SESSION_IMAGE_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "IM-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), + SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "IM-102", "입력한 순서는 유효하지 않습니다."), + INVALID_ANSWER(HttpStatus.BAD_REQUEST, "Q-101", "객관식 문제는 숫자 형식의 값만 정답으로 추가할 수 있습니다."), CONTENT_IS_NOT_ANSWER(HttpStatus.BAD_REQUEST, "Q-201", "추가되지 않은 정답을 추가할 수 없습니다."), QUIZ_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "Q-301", "퀴즈 번호는 중복될 수 없습니다."), diff --git a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java index c6e87a17..9f448ffe 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java @@ -1,7 +1,6 @@ package org.cotato.csquiz.domain.generation.repository; import java.util.List; -import java.util.Optional; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.domain.generation.entity.SessionImage; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,5 +10,5 @@ public interface SessionImageRepository extends JpaRepository findAllBySessionIn(List sessions); - Optional findFirstBySessionOrderByOrderDesc(Session session); + boolean existsBySessionAndOrder(Session session, Integer order); } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java index 2318caeb..1ad1ea04 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionImageService.java @@ -67,13 +67,14 @@ public AddSessionImageResponse additionalSessionImage(AddSessionImageRequest req S3Info imageInfo = s3Uploader.uploadFiles(request.image(), SESSION_BUCKET_DIRECTORY); - Integer imageOrder = sessionImageRepository.findFirstBySessionOrderByOrderDesc(session) - .map(sessionImage -> sessionImage.getOrder() + 1).orElse(0); + if (sessionImageRepository.existsBySessionAndOrder(session, request.order())) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); + } SessionImage sessionImage = SessionImage.builder() .session(session) .s3Info(imageInfo) - .order(imageOrder) + .order(request.order()) .build(); return AddSessionImageResponse.from(sessionImageRepository.save(sessionImage)); From 382fb4220c12ac855d197a6d29dad67ad2eabc74 Mon Sep 17 00:00:00 2001 From: GiHun Nam <52378919+gikhoon@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:04:37 +0900 Subject: [PATCH 2/7] =?UTF-8?q?Feature:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=20webp=20=EB=B3=80=ED=99=98=20(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: webp 변환 라이브러리 추가 * feat: 프로젝트 이미지 등록 시 webp 변환 기능 추가 * refactor: 파일 변환 메소드 위치 이동 - S3Uploader -> FileUtil --- build.gradle | 4 +++ .../cotato/csquiz/common/error/ErrorCode.java | 1 + .../cotato/csquiz/common/s3/S3Uploader.java | 33 +++++------------ .../cotato/csquiz/common/util/FileUtil.java | 36 +++++++++++++++++++ .../service/ProjectImageService.java | 11 ++++-- 5 files changed, 58 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index 7cd5ffdc..1cab0c52 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,10 @@ dependencies { //S3 관련 의존성 부여 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + //webp 변환 라이브러리 + implementation "com.sksamuel.scrimage:scrimage-core:4.0.32" + implementation "com.sksamuel.scrimage:scrimage-webp:4.0.32" + implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.32.0' implementation 'io.jsonwebtoken:jjwt:0.9.1' diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 839a257f..7d3a27b6 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -93,6 +93,7 @@ public enum ErrorCode { SCORER_LOCK_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-006", "득점자 락 획득 과정에서 에러 발생"), IMAGE_CONVERT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-007", "로컬 이미지 변환에 실패했습니다"), SSE_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-008", "서버 이벤트 전송간 오류 발생"), + WEBP_CONVERT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-009", "webp 변환에 실패했습니다") ; private final HttpStatus httpStatus; diff --git a/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java b/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java index 117f94f7..75db8840 100644 --- a/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java +++ b/src/main/java/org/cotato/csquiz/common/s3/S3Uploader.java @@ -1,6 +1,6 @@ package org.cotato.csquiz.common.s3; -import static org.cotato.csquiz.common.util.FileUtil.extractFileExtension; +import static org.cotato.csquiz.common.util.FileUtil.convert; import static org.cotato.csquiz.common.util.FileUtil.isImageFileExtension; import com.amazonaws.services.s3.AmazonS3Client; @@ -8,9 +8,7 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import org.cotato.csquiz.common.entity.S3Info; -import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.common.error.exception.ImageException; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -18,8 +16,6 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; @Slf4j @RequiredArgsConstructor @@ -36,13 +32,17 @@ public S3Info uploadFiles(MultipartFile multipartFile, String folderName) throws log.info("{} 사진 업로드", multipartFile.getOriginalFilename()); File localUploadFile = convert(multipartFile); - String fileName = folderName + "/" + localUploadFile.getName(); - String uploadUrl = putS3(localUploadFile, fileName); - localUploadFile.delete(); + return uploadFiles(localUploadFile, folderName); + } + + public S3Info uploadFiles(File file, String folderName) { + String fileName = folderName + "/" + file.getName(); + String uploadUrl = putS3(file, fileName); + file.delete(); return S3Info.builder() .folderName(folderName) - .fileName(localUploadFile.getName()) + .fileName(file.getName()) .url(uploadUrl) .build(); } @@ -74,19 +74,4 @@ private boolean isImageFile(File file) { return isImageFileExtension(extension); } - - private File convert(MultipartFile file) throws ImageException { - String fileExtension = extractFileExtension(file); - File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); - - try { - FileOutputStream fos = new FileOutputStream(convertFile); - fos.write(file.getBytes()); - fos.close(); - - return convertFile; - } catch (IOException e) { - throw new ImageException(ErrorCode.IMAGE_CONVERT_FAIL); - } - } } diff --git a/src/main/java/org/cotato/csquiz/common/util/FileUtil.java b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java index efed8113..40267f1f 100644 --- a/src/main/java/org/cotato/csquiz/common/util/FileUtil.java +++ b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java @@ -1,6 +1,12 @@ package org.cotato.csquiz.common.util; +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.webp.WebpWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; +import java.util.UUID; import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.common.error.exception.ImageException; import org.springframework.web.multipart.MultipartFile; @@ -21,4 +27,34 @@ public static String extractFileExtension(MultipartFile file) throws ImageExcept public static boolean isImageFileExtension(String fileExtension) { return IMAGE_FILE_EXTENSIONS.contains(fileExtension.toLowerCase()); } + + public static File convert(MultipartFile file) throws ImageException { + String fileExtension = extractFileExtension(file); + File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); + + try { + FileOutputStream fos = new FileOutputStream(convertFile); + fos.write(file.getBytes()); + fos.close(); + + return convertFile; + } catch (IOException e) { + throw new ImageException(ErrorCode.IMAGE_CONVERT_FAIL); + } + } + + public static File convertToWebp(File originalFile) throws ImageException { + try { + String fileNameWithoutExtension = originalFile.getName().replaceFirst("[.][^.]+$", ""); + + File webpFile = ImmutableImage.loader() + .fromFile(originalFile) + .output(WebpWriter.DEFAULT, new File(fileNameWithoutExtension + ".webp")); + + originalFile.delete(); + return webpFile; + } catch (IOException e) { + throw new ImageException(ErrorCode.WEBP_CONVERT_FAIL); + } + } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java index 2921671c..622f6683 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java @@ -1,5 +1,8 @@ package org.cotato.csquiz.domain.generation.service; +import static org.cotato.csquiz.common.util.FileUtil.*; + +import java.io.File; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -21,14 +24,16 @@ public class ProjectImageService { private final ProjectImageRepository projectImageRepository; @Transactional - public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNameImage) + public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNailImage) throws ImageException { List newImages = new ArrayList<>(); - S3Info logoImageInfo = s3Uploader.uploadFiles(logoImage, PROJECT_IMAGE); + File webpLogoImage = convertToWebp(convert(logoImage)); + S3Info logoImageInfo = s3Uploader.uploadFiles(webpLogoImage, PROJECT_IMAGE); newImages.add(ProjectImage.logoImage(logoImageInfo, projectId)); - S3Info thumbNailInfo = s3Uploader.uploadFiles(thumbNameImage, PROJECT_IMAGE); + File webpThumbNailImage = convertToWebp(convert(thumbNailImage)); + S3Info thumbNailInfo = s3Uploader.uploadFiles(webpThumbNailImage, PROJECT_IMAGE); newImages.add(ProjectImage.thumbNailImage(thumbNailInfo, projectId)); projectImageRepository.saveAll(newImages); From 96e52f9d0886f6f691a04952590f1b57f170bef6 Mon Sep 17 00:00:00 2001 From: Youth <109585620+Youthhing@users.noreply.github.com> Date: Fri, 13 Sep 2024 02:16:03 +0900 Subject: [PATCH 3/7] =?UTF-8?q?Feature:=20=EC=84=B8=EC=85=98=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=B6=9C=EA=B2=B0=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 대면 출결 기준 값 환경 변수화 * refactor: 출석 결과 변경 메서드명 수정 * style: 불필요한 엔드포인트 삭제 * refactor: 사용하지 않는 메서드 제거 * feat: 세션 날짜 컬럼을 세션 시작 시간으로 변경 * refactor: 출결 상태 결정 메서드 파라미터 수정 - 세션 시작 시간을 기준으로 구분 * refactor: 출결 가능 상태 반환 메서드 수정 - 세션 시작 시간을 기준으로 변경 * refactor: 출결 시작 알림 등록 스케줄러 메서드 변경 * refactor: 출석 정보 등록 메서드 수정 - 불필요한 dto 삭제 - 비즈니스 로직 상 59초 더하는 로직 제거 * refactor: 출석 정보 수정 메서드 변경 * refactor: 세션 날짜 변경 메서드 변경 * refactor: 세션 시작 시간 변경에 따른 변수명 변경 * chore: 주석 수정 * test: 유틸 메서드 변경에 따른 테스트 코드 수정 * refactor: 출결 시간 검증 로직 수정 * chore: 사용하지 않는 deadline 값 제거 * test: 기존 시간 전에 출석이 닫혀있는 테스트 수정 --- .github/workflows/deploy.yml | 1 + .github/workflows/release.yml | 1 + .../attendance/dto/MemberAttendResponse.java | 9 ++-- .../session/controller/SessionController.java | 10 +--- .../api/session/dto/AddSessionRequest.java | 9 ++-- .../api/session/dto/SessionListResponse.java | 6 +-- .../dto/UpdateSessionNumberRequest.java | 11 ---- .../api/session/dto/UpdateSessionRequest.java | 4 +- .../cotato/csquiz/common/error/ErrorCode.java | 2 +- .../common/schedule/SchedulerService.java | 5 +- .../cotato/csquiz/common/sse/SseSender.java | 13 +++-- .../domain/attendance/embedded/Location.java | 4 ++ .../attendance/entity/AttendanceRecord.java | 2 +- .../domain/attendance/enums/DeadLine.java | 12 ----- .../attendance/service/AttendClient.java | 3 +- .../service/AttendanceAdminService.java | 32 +++++------ .../service/AttendanceRecordService.java | 30 +++++++---- .../attendance/service/AttendanceService.java | 2 +- .../service/OfflineAttendClient.java | 14 +++-- .../service/OnlineAttendClient.java | 5 +- .../service/RequestAttendanceService.java | 5 +- .../attendance/util/AttendanceUtil.java | 40 +++++++------- .../domain/generation/entity/Session.java | 18 +++---- .../generation/service/SessionService.java | 53 +++++++------------ src/main/resources/application.yml | 3 ++ .../attendance/util/AttendanceUtilTest.java | 38 +++++-------- 26 files changed, 143 insertions(+), 189 deletions(-) delete mode 100644 src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionNumberRequest.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bc73cd96..d3e7304a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,6 +50,7 @@ jobs: aes.secret.salt: ${{ secrets.AES_SECRET_SALT }} spring.mail.username: ${{ secrets.SENDER_EMAIL }} spring.mail.password: ${{ secrets.SENDER_PASSWORD }} + location.distance: ${{ secrets.STANDARD_DISTANCE }} # [2] 실행 권한 부여 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31d71939..9c3706f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,7 @@ jobs: aes.secret.salt: ${{ secrets.AES_SECRET_SALT }} spring.mail.username: ${{ secrets.SENDER_EMAIL }} spring.mail.password: ${{ secrets.SENDER_PASSWORD }} + location.distance: ${{ secrets.STANDARD_DISTANCE }} # [2] 실행 권한 부여 diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java index 9f8b30eb..c1fb67cc 100644 --- a/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java @@ -1,7 +1,6 @@ package org.cotato.csquiz.api.attendance.dto; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDate; import java.time.LocalDateTime; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord; @@ -21,7 +20,7 @@ public record MemberAttendResponse( @Schema(description = "세션 타이틀", example = "3주차 세션") String sessionTitle, @Schema(description = "세션 날짜") - LocalDate sessionDate, + LocalDateTime sessionDateTime, @Schema(description = "출결 진행 여부", examples = { "CLOSED", "OPEN" }) @@ -37,7 +36,7 @@ public static MemberAttendResponse closedAttendanceResponse(Session session, Att attendanceRecord.getAttendance().getId(), attendanceRecord.getMemberId(), session.getTitle(), - session.getSessionDate(), + session.getSessionDateTime(), AttendanceOpenStatus.CLOSED, attendanceRecord.getAttendanceType(), attendanceRecord.getAttendanceResult() @@ -50,8 +49,8 @@ public static MemberAttendResponse openedAttendanceResponse(Attendance attendanc attendance.getId(), memberId, session.getTitle(), - session.getSessionDate(), - AttendanceUtil.getAttendanceOpenStatus(attendance, LocalDateTime.now()), + session.getSessionDateTime(), + AttendanceUtil.getAttendanceOpenStatus(session.getSessionDateTime(), attendance, LocalDateTime.now()), null, null ); diff --git a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java index 561df15f..0db3819a 100644 --- a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java +++ b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java @@ -13,7 +13,6 @@ 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.UpdateSessionNumberRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.domain.generation.service.SessionImageService; @@ -42,7 +41,7 @@ public class SessionController { private final SessionImageService sessionImageService; @Operation(summary = "세션 목록 반환 API") - @GetMapping("") + @GetMapping public ResponseEntity> findSessionsByGenerationId(@RequestParam Long generationId) { return ResponseEntity.status(HttpStatus.OK).body(sessionService.findSessionsByGenerationId(generationId)); } @@ -69,13 +68,6 @@ public ResponseEntity updateSession(@RequestBody @Valid UpdateSessionReque return ResponseEntity.noContent().build(); } - @Operation(summary = "세션 숫자 변경 API") - @PatchMapping("/number") - public ResponseEntity updateSessionNumber(@RequestBody @Valid UpdateSessionNumberRequest request) { - sessionService.updateSessionNumber(request); - return ResponseEntity.noContent().build(); - } - @Operation(summary = "세션 사진 순서 변경 API") @PatchMapping("/image/order") public ResponseEntity updateSessionImageOrder(@RequestBody UpdateSessionImageOrderRequest request) { diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java index 3b119d59..7dee8445 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import org.cotato.csquiz.domain.generation.enums.CSEducation; @@ -23,14 +23,15 @@ public record AddSessionRequest( Double latitude, Double longitude, String placeName, + @Schema(description = "세션 날짜 및 시작 시간") @NotNull - LocalDate sessionDate, + LocalDateTime sessionDateTime, - @Schema(example = "19:05:00") + @Schema(example = "19:10:00", description = "출석 마감 시간, 해당 시간 이후 지각 처리") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss") LocalTime attendanceDeadLine, - @Schema(example = "19:20:00") + @Schema(example = "19:20:00", description = "지각 마감 시간, 해당 시간 이후 결석 처리") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss") LocalTime lateDeadLine, diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java index 775b2bb0..1624fb61 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java @@ -1,6 +1,6 @@ package org.cotato.csquiz.api.session.dto; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import org.cotato.csquiz.domain.generation.embedded.SessionContents; import org.cotato.csquiz.domain.generation.entity.Session; @@ -14,7 +14,7 @@ public record SessionListResponse( String description, Long generationId, String placeName, - LocalDate sessionDate, + LocalDateTime sessionDateTime, SessionContents sessionContents ) { public static SessionListResponse of(Session session, List sessionImages) { @@ -28,7 +28,7 @@ public static SessionListResponse of(Session session, List session session.getDescription(), session.getGeneration().getId(), session.getPlaceName(), - session.getSessionDate(), + session.getSessionDateTime(), session.getSessionContents() ); } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionNumberRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionNumberRequest.java deleted file mode 100644 index e1c5c53a..00000000 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionNumberRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.cotato.csquiz.api.session.dto; - -import jakarta.validation.constraints.NotNull; - -public record UpdateSessionNumberRequest( - @NotNull - Long sessionId, - @NotNull - Integer sessionNum -) { -} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java index c1608383..ac1db4c3 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java @@ -3,7 +3,7 @@ import static org.cotato.csquiz.domain.attendance.enums.DeadLine.DEFAULT_ATTENDANCE_DEADLINE; import static org.cotato.csquiz.domain.attendance.enums.DeadLine.DEFAULT_LATE_DEADLINE; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Objects; import org.cotato.csquiz.api.attendance.dto.AttendanceDeadLineDto; import org.cotato.csquiz.domain.attendance.embedded.Location; @@ -19,7 +19,7 @@ public record UpdateSessionRequest( String title, String description, @NotNull - LocalDate sessionDate, + LocalDateTime sessionDateTime, String placeName, Location location, AttendanceDeadLineDto attendTime, diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 7d3a27b6..bb28d755 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -82,7 +82,7 @@ public enum ErrorCode { OFFLINE_ATTEND_FAIL(HttpStatus.BAD_REQUEST, "AT-101", "거리 부적합으로 인한 대면 출석 실패"), INVALID_ATTEND_TIME(HttpStatus.BAD_REQUEST, "AT-102", "시간 입력 범위가 잘못되었습니다."), ALREADY_ATTEND(HttpStatus.CONFLICT, "AT-301", "이미 해당 타입으로 출석한 기록이 있습니다."), - ATTENDANCE_CLOSED(HttpStatus.BAD_REQUEST, "AT-401", "아직 출석 시간이 아닙니다."), + ATTENDANCE_NOT_OPEN(HttpStatus.BAD_REQUEST, "AT-401", "출석 시간이 아닙니다."), // 500 오류 -> 서버측에서 처리가 실패한 부분들 WEBSOCKET_SEND_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "소캣 메세지 전송 실패"), diff --git a/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java b/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java index e16d978e..84ecf0c5 100644 --- a/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java +++ b/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java @@ -55,10 +55,7 @@ public void closeAllCsQuiz() { log.info("[ CS 퀴즈 모두 닫기 Scheduler 완료 ]"); } - // sessionDate 18시 50분에 출결을 구독 중인 부원들에게 출결 입력 시작 알림을 전송하는 스케줄러 - public void scheduleSessionNotification(LocalDate sessionDate) { - LocalDateTime notificationTime = LocalDateTime.of(sessionDate, DeadLine.ATTENDANCE_START_TIME.getTime()); - + public void scheduleSessionNotification(LocalDateTime notificationTime) { ZonedDateTime zonedDateTime = notificationTime.atZone(ZoneId.of("Asia/Seoul")); taskScheduler.schedule(() -> sseSender.sendNotification(notificationTime), zonedDateTime.toInstant()); diff --git a/src/main/java/org/cotato/csquiz/common/sse/SseSender.java b/src/main/java/org/cotato/csquiz/common/sse/SseSender.java index b98fe1a3..4de18225 100644 --- a/src/main/java/org/cotato/csquiz/common/sse/SseSender.java +++ b/src/main/java/org/cotato/csquiz/common/sse/SseSender.java @@ -13,6 +13,8 @@ import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository; import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; +import org.cotato.csquiz.domain.generation.entity.Session; +import org.cotato.csquiz.domain.generation.repository.SessionRepository; import org.springframework.stereotype.Component; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.DataWithMediaType; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -23,7 +25,7 @@ public class SseSender { private static final String ATTENDANCE_STATUS = "AttendanceStatus"; private final SseAttendanceRepository sseAttendanceRepository; - + private final SessionRepository sessionRepository; private final AttendanceRepository attendanceRepository; public void sendInitialAttendanceStatus(SseEmitter sseEmitter) { @@ -39,16 +41,21 @@ public void sendInitialAttendanceStatus(SseEmitter sseEmitter) { return; } + Attendance attendance = maybeAttendance.get(); + Session session = sessionRepository.findById(attendance.getSessionId()) + .orElseThrow(() -> new EntityNotFoundException("해당 출석에 대한 세션이 존재하지 않습니다.")); + send(sseEmitter, SseEmitter.event() .name(ATTENDANCE_STATUS) .data(AttendanceStatusInfo.builder() .attendanceId(maybeAttendance.get().getId()) - .openStatus(AttendanceUtil.getAttendanceOpenStatus(maybeAttendance.get(), LocalDateTime.now())) + .openStatus(AttendanceUtil.getAttendanceOpenStatus(session.getSessionDateTime(), attendance, + LocalDateTime.now())) .build()) .build()); } - // sessionDate 6시 50분에 출결을 구독 중인 부원들에게 출결 입력 시작 알림을 전송한다. + // sessionDateTime 7시에 출결을 구독 중인 부원들에게 출결 입력 시작 알림을 전송한다. public void sendNotification(LocalDateTime notificationDate) { Attendance attendance = attendanceRepository.findByAttendanceDeadLineDate(notificationDate) .orElseThrow(() -> new EntityNotFoundException("해당 날짜에 진행하는 출석이 없습니다.")); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java b/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java index d693b503..7807bd7c 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java @@ -20,6 +20,10 @@ private Location(Double latitude, Double longitude) { this.longitude = longitude; } + public static Location location(Double latitude, Double longitude){ + return new Location(latitude, longitude); + } + public Double calculateAccuracy(Location location) { return Math.pow(this.latitude - location.latitude, 2) + Math.pow(this.longitude - location.longitude, 2); } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java index a6abff08..45050d39 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java @@ -98,7 +98,7 @@ public void updateLocationAccuracy(Double accuracy) { this.locationAccuracy = accuracy; } - public void updateAttendanceStatus(AttendanceResult attendanceResult) { + public void updateAttendanceResult(AttendanceResult attendanceResult) { this.attendanceResult = attendanceResult; } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/enums/DeadLine.java b/src/main/java/org/cotato/csquiz/domain/attendance/enums/DeadLine.java index e1a5b28d..f2bc1955 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/enums/DeadLine.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/enums/DeadLine.java @@ -1,7 +1,5 @@ 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; @@ -10,20 +8,10 @@ @AllArgsConstructor public enum DeadLine { - ATTENDANCE_START_TIME(LocalTime.of(18, 50, 0), "고정 출석 시작 시간"), DEFAULT_ATTENDANCE_DEADLINE(LocalTime.of(19, 5, 0), "기본 출석 마감 시간"), DEFAULT_LATE_DEADLINE(LocalTime.of(19,20,0),"기본 지각 마감 시간"), - ATTENDANCE_END_TIME(LocalTime.of(20, 0,0), "고정 세션 종료 시간") ; 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()); - } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java index d304dea9..02a4fc79 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.domain.attendance.service; +import java.time.LocalDateTime; import org.cotato.csquiz.api.attendance.dto.AttendResponse; import org.cotato.csquiz.api.attendance.dto.AttendanceParams; import org.cotato.csquiz.domain.attendance.entity.Attendance; @@ -8,5 +9,5 @@ public interface AttendClient { AttendanceType attendanceType(); - AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance); + AttendResponse request(AttendanceParams params, LocalDateTime sessionStartTime, Long memberId, Attendance attendance); } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java index 324fd891..b720fe7d 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java @@ -3,6 +3,7 @@ import jakarta.persistence.EntityNotFoundException; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,23 +27,19 @@ @Slf4j public class AttendanceAdminService { - private static final int DEFAULT_ATTEND_SECOND = 59; private final AttendanceRepository attendanceRepository; private final AttendanceRecordService attendanceRecordService; private final SessionRepository sessionRepository; @Transactional - public void addAttendance(Session session, Location location, AttendanceDeadLineDto attendanceDeadLine) { - AttendanceUtil.validateAttendanceTime(attendanceDeadLine.attendanceDeadLine(), - attendanceDeadLine.lateDeadLine()); + public void addAttendance(Session session, Location location, LocalTime attendanceDeadline, LocalTime lateDeadline) { + AttendanceUtil.validateAttendanceTime(session.getSessionDateTime(), attendanceDeadline, lateDeadline); Attendance attendance = Attendance.builder() .session(session) .location(location) - .attendanceDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.attendanceDeadLine()) - .plusSeconds(DEFAULT_ATTEND_SECOND)) - .lateDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.lateDeadLine()) - .plusSeconds(DEFAULT_ATTEND_SECOND)) + .attendanceDeadLine(LocalDateTime.of(session.getSessionDateTime().toLocalDate(), attendanceDeadline)) + .lateDeadLine(LocalDateTime.of(session.getSessionDateTime().toLocalDate(), lateDeadline)) .build(); attendanceRepository.save(attendance); @@ -55,35 +52,32 @@ public void updateAttendanceByAttendanceId(UpdateAttendanceRequest request) { Session attendanceSession = sessionRepository.findById(attendance.getSessionId()) .orElseThrow(() -> new EntityNotFoundException("출석과 연결된 세션을 찾을 수 없습니다")); - updateAttendance(attendanceSession, attendance, request - .attendTime(), request.location()); + updateAttendance(attendanceSession, attendance, request.attendTime(), request.location()); } @Transactional public void updateAttendance(Session attendanceSession, Attendance attendance, AttendanceDeadLineDto attendanceDeadLine, Location location) { - AttendanceUtil.validateAttendanceTime(attendanceDeadLine.attendanceDeadLine(), + AttendanceUtil.validateAttendanceTime(attendanceSession.getSessionDateTime(), attendanceDeadLine.attendanceDeadLine(), attendanceDeadLine.lateDeadLine()); - if (attendanceSession.getSessionDate() == null) { + // 세션 날짜가 존재하지 않는 경우 예외 발생 + if (attendanceSession.getSessionDateTime() == null) { throw new AppException(ErrorCode.SESSION_DATE_NOT_FOUND); } - attendance.updateDeadLine( - LocalDateTime.of(attendanceSession.getSessionDate(), attendanceDeadLine.attendanceDeadLine()) - .plusSeconds(DEFAULT_ATTEND_SECOND), - LocalDateTime.of(attendanceSession.getSessionDate(), attendanceDeadLine.lateDeadLine()) - .plusSeconds(DEFAULT_ATTEND_SECOND)); + attendance.updateDeadLine(LocalDateTime.of(attendanceSession.getSessionDateTime().toLocalDate(), attendanceDeadLine.attendanceDeadLine()), + LocalDateTime.of(attendanceSession.getSessionDateTime().toLocalDate(), attendanceDeadLine.lateDeadLine())); attendance.updateLocation(location); - attendanceRecordService.updateAttendanceStatus(attendance); + attendanceRecordService.updateAttendanceStatus(attendanceSession.getSessionDateTime(), attendance); } public List findAttendanceRecords(Long generationId, Integer month) { List sessions = sessionRepository.findAllByGenerationId(generationId); if (month != null) { sessions = sessions.stream() - .filter(session -> session.getSessionDate().getMonthValue() == month) + .filter(session -> session.getSessionDateTime().getMonthValue() == month) .toList(); } List sessionIds = sessions.stream() diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java index f18a0fe9..4eb1e12c 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java @@ -66,9 +66,13 @@ public AttendResponse submitRecord(AttendanceParams request, final Long memberId Attendance attendance = attendanceRepository.findById(request.attendanceId()) .orElseThrow(() -> new EntityNotFoundException("해당 출석이 존재하지 않습니다.")); - // 해당 출석이 열려있는지 확인, 닫혀있으면 제외 - if (getAttendanceOpenStatus(attendance, request.requestTime()) == AttendanceOpenStatus.CLOSED) { - throw new AppException(ErrorCode.ATTENDANCE_CLOSED); + Session session = sessionRepository.findById(attendance.getSessionId()) + .orElseThrow(() -> new EntityNotFoundException("해당 출석에 대한 세션이 존재하지 않습니다.")); + + // 해당 출석에 출결 입력이 가능한지 확인하는 과정 + if (getAttendanceOpenStatus(session.getSessionDateTime(), attendance, request.requestTime()) + == AttendanceOpenStatus.CLOSED) { + throw new AppException(ErrorCode.ATTENDANCE_NOT_OPEN); } // 기존 출결 데이터가 존재하는지 확인 @@ -77,7 +81,7 @@ public AttendResponse submitRecord(AttendanceParams request, final Long memberId throw new AppException(ErrorCode.ALREADY_ATTEND); } - return requestAttendanceService.attend(request, memberId, attendance); + return requestAttendanceService.attend(request, session.getSessionDateTime(), memberId, attendance); } public MemberAttendanceRecordsResponse findAllRecordsBy(final Long generationId, final Long memberId) { @@ -95,14 +99,17 @@ public MemberAttendanceRecordsResponse findAllRecordsBy(final Long generationId, Map> isClosedAttendance = attendanceRepository.findAllBySessionIdsInQuery(sessionIds) .stream() .collect(Collectors.partitioningBy(attendance -> - getAttendanceOpenStatus(attendance, currentTime) == AttendanceOpenStatus.CLOSED)); + getAttendanceOpenStatus(sessionMap.get(attendance.getSessionId()).getSessionDateTime(), + attendance, currentTime) == AttendanceOpenStatus.CLOSED)); List closedAttendanceIds = isClosedAttendance.get(true).stream() .map(Attendance::getId) .toList(); - List responses = attendanceRecordRepository.findAllByAttendanceIdsInQueryAndMemberId(closedAttendanceIds, memberId).stream() - .map(ar -> MemberAttendResponse.closedAttendanceResponse(sessionMap.get(ar.getAttendance().getSessionId()), ar)) + List 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() @@ -112,14 +119,15 @@ public MemberAttendanceRecordsResponse findAllRecordsBy(final Long generationId, return MemberAttendanceRecordsResponse.of(generationId, responses); } - + @Transactional - public void updateAttendanceStatus(Attendance attendance) { + public void updateAttendanceStatus(LocalDateTime sessionStartTime, Attendance attendance) { List attendanceRecords = attendanceRecordRepository.findAllByAttendanceId(attendance.getId()); for (AttendanceRecord attendanceRecord : attendanceRecords) { - AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(attendance, attendanceRecord.getAttendTime()); - attendanceRecord.updateAttendanceStatus(attendanceResult); + AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(sessionStartTime, attendance, + attendanceRecord.getAttendTime()); + attendanceRecord.updateAttendanceResult(attendanceResult); } attendanceRecordRepository.saveAll(attendanceRecords); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java index a22d790e..1f370dc1 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java @@ -50,7 +50,7 @@ public AttendancesResponse findAttendancesByGenerationId(final Long generationId .sessionId(at.getSessionId()) .sessionTitle(sessionMap.get(at.getSessionId()).getTitle()) .sessionDate(at.getAttendanceDeadLine().toLocalDate()) - .openStatus(AttendanceUtil.getAttendanceOpenStatus(at, currentTime)) + .openStatus(AttendanceUtil.getAttendanceOpenStatus(sessionMap.get(at.getSessionId()).getSessionDateTime(), at, currentTime)) .build()) .toList(); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java index 04ddcd24..3e833cd1 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.domain.attendance.service; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cotato.csquiz.api.attendance.dto.AttendResponse; @@ -13,6 +14,7 @@ import org.cotato.csquiz.domain.attendance.enums.AttendanceType; import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository; import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Slf4j @@ -20,7 +22,8 @@ @RequiredArgsConstructor public class OfflineAttendClient implements AttendClient { - private static final Double ACCURACY_STANDARD = 0.1; + @Value("${location.distance}") + private Double standardDistance; private final AttendanceRecordRepository attendanceRecordRepository; @Override @@ -29,10 +32,10 @@ public AttendanceType attendanceType() { } @Override - public AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance) { + public AttendResponse request(AttendanceParams params, LocalDateTime sessionStartTime, Long memberId, Attendance attendance) { OfflineAttendanceRequest request = (OfflineAttendanceRequest) params; - AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(attendance, params.requestTime()); + AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(sessionStartTime, attendance, params.requestTime()); log.info("[출결 위치 로그: 위도 {}, 경도 {}]", request.getLocation().getLatitude(), request.getLocation().getLongitude()); Double accuracy = attendance.getLocation().calculateAccuracy(request.getLocation()); @@ -40,7 +43,8 @@ public AttendResponse request(AttendanceParams params, Long memberId, Attendance AttendanceRecord attendanceRecord = attendanceRecordRepository.findByMemberIdAndAttendanceId(memberId, request.getAttendanceId()) - .orElseGet(() -> AttendanceRecord.offlineRecord(attendance, memberId, accuracy, attendanceResult, request.getRequestTime())); + .orElseGet(() -> AttendanceRecord.offlineRecord(attendance, memberId, accuracy, attendanceResult, + request.getRequestTime())); attendanceRecord.updateAttendanceType(request.attendanceType()); attendanceRecord.updateLocationAccuracy(accuracy); @@ -52,7 +56,7 @@ public AttendResponse request(AttendanceParams params, Long memberId, Attendance private void validateAccuracy(Double accuracy) { log.info("[위치 정확도] : {}", accuracy); - if (accuracy >= ACCURACY_STANDARD) { + if (accuracy >= standardDistance) { throw new AppException(ErrorCode.OFFLINE_ATTEND_FAIL); } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java index 28dbd912..f1c08e0f 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.domain.attendance.service; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.cotato.csquiz.api.attendance.dto.AttendResponse; import org.cotato.csquiz.api.attendance.dto.AttendanceParams; @@ -23,8 +24,8 @@ public AttendanceType attendanceType() { } @Override - public AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance) { - AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(attendance, params.requestTime()); + public AttendResponse request(AttendanceParams params, LocalDateTime sessionStartTime, Long memberId, Attendance attendance) { + AttendanceResult attendanceResult = AttendanceUtil.calculateAttendanceStatus(sessionStartTime, attendance, params.requestTime()); attendanceRecordRepository.save(AttendanceRecord.onLineRecord(attendance, memberId, attendanceResult, params.requestTime())); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java index 1639becf..e347e5af 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.domain.attendance.service; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -23,8 +24,8 @@ public RequestAttendanceService(List clients) { ); } - public AttendResponse attend(AttendanceParams params, Long memberId, Attendance attendance) { + public AttendResponse attend(AttendanceParams params, LocalDateTime sessionStartTime, Long memberId, Attendance attendance) { AttendClient attendClient = clients.get(params.attendanceType()); - return attendClient.request(params, memberId, attendance); + return attendClient.request(params, sessionStartTime, memberId, attendance); } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java b/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java index 3a61ff08..2da22a79 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java @@ -7,56 +7,52 @@ import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; import org.cotato.csquiz.domain.attendance.enums.AttendanceResult; -import org.cotato.csquiz.domain.attendance.enums.DeadLine; public class AttendanceUtil { - // 출석 시간에 따른 지각 여부 구분하기 - public static AttendanceResult calculateAttendanceStatus(Attendance attendance, LocalDateTime attendTime){ - if (attendTime.isBefore(attendance.getAttendanceDeadLine())) { + public static AttendanceResult calculateAttendanceStatus(LocalDateTime sessionDateTime, Attendance attendance, + LocalDateTime attendTime) { + // 입력한 날짜와 세션 날짜가 다르거나, 시작 전이라면 + if (!attendTime.toLocalDate().equals(sessionDateTime.toLocalDate()) || attendTime.isBefore(sessionDateTime)) { + throw new AppException(ErrorCode.ATTENDANCE_NOT_OPEN); + } + + if (attendTime.isAfter(sessionDateTime) && attendTime.isBefore(attendance.getAttendanceDeadLine())) { return AttendanceResult.PRESENT; - } if (attendTime.isBefore(attendance.getLateDeadLine())) { + } + if (attendTime.isBefore(attendance.getLateDeadLine())) { return AttendanceResult.LATE; } return AttendanceResult.ABSENT; } - // 현재 시간을 기준으로 출석 open 상태를 반환한다. - public static AttendanceOpenStatus getAttendanceOpenStatus(Attendance attendance, LocalDateTime currentDateTime) { - if (currentDateTime.isBefore(DeadLine.sessionStartTime(attendance.getAttendanceDeadLine().toLocalDate()))) { + // 현재 시간을 기준으로 출석이 열려있는지를 반환한다. + public static AttendanceOpenStatus getAttendanceOpenStatus(LocalDateTime sessionStartTime, Attendance attendance, LocalDateTime currentDateTime) { + if (currentDateTime.isBefore(sessionStartTime)) { return AttendanceOpenStatus.BEFORE; } - if (currentDateTime.isAfter(DeadLine.sessionEndTime(attendance.getLateDeadLine().toLocalDate()))) { + if (currentDateTime.toLocalDate().isAfter(sessionStartTime.toLocalDate())) { return AttendanceOpenStatus.CLOSED; } - LocalTime currentTime = currentDateTime.toLocalTime(); - - if (currentTime.isAfter(DeadLine.ATTENDANCE_START_TIME.getTime()) - && currentTime.isBefore(attendance.getAttendanceDeadLine().toLocalTime())) { + if (currentDateTime.isAfter(sessionStartTime) && currentDateTime.isBefore(attendance.getAttendanceDeadLine())) { return AttendanceOpenStatus.OPEN; } - if (currentTime.isAfter(attendance.getAttendanceDeadLine().toLocalTime()) - && currentTime.isBefore(attendance.getLateDeadLine().toLocalTime())) { + if (currentDateTime.isAfter(attendance.getAttendanceDeadLine()) && currentDateTime.isBefore(attendance.getLateDeadLine())) { return AttendanceOpenStatus.LATE; } - return AttendanceOpenStatus.ABSENT; } - public static void validateAttendanceTime(LocalTime attendDeadLine, LocalTime lateDeadLine) { - if (!DeadLine.ATTENDANCE_START_TIME.getTime().isBefore(attendDeadLine)) { + public static void validateAttendanceTime(LocalDateTime sessionStartTime, LocalTime attendDeadLine, LocalTime lateDeadLine) { + if (!sessionStartTime.toLocalTime().isBefore(attendDeadLine)) { throw new AppException(ErrorCode.INVALID_ATTEND_TIME); } if (!attendDeadLine.isBefore(lateDeadLine)) { throw new AppException(ErrorCode.INVALID_ATTEND_TIME); } - - if (!lateDeadLine.isBefore(DeadLine.ATTENDANCE_END_TIME.getTime())) { - throw new AppException(ErrorCode.INVALID_ATTEND_TIME); - } } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java index b6525dda..cd5b786a 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java @@ -11,7 +11,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDate; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -56,28 +56,24 @@ public class Session extends BaseTimeEntity { }) private SessionContents sessionContents; - @Column(name = "session_date") - private LocalDate sessionDate; + @Column(name = "session_start_time") + private LocalDateTime sessionDateTime; @Column(name = "session_place_name") private String placeName; @Builder - public Session(Integer number, String title, String description, String placeName, LocalDate sessionDate, + public Session(Integer number, String title, String description, String placeName, LocalDateTime sessionDateTime, Generation generation, SessionContents sessionContents) { this.number = number; this.title = title; this.description = description; this.placeName = placeName; - this.sessionDate = sessionDate; + this.sessionDateTime = sessionDateTime; this.generation = generation; this.sessionContents = sessionContents; } - public void changeSessionNumber(Integer sessionNumber) { - this.number = sessionNumber; - } - public void updateDescription(String description) { this.description = description; } @@ -90,8 +86,8 @@ public void updateSessionTitle(String title) { this.title = title; } - public void updateSessionDate(LocalDate sessionDate) { - this.sessionDate = sessionDate; + public void updateSessionDateTime(LocalDateTime sessionDateTime) { + this.sessionDateTime = sessionDateTime; } public void updateSessionPlace(String placeName) { diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java index be101392..76642b6c 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java @@ -1,19 +1,17 @@ package org.cotato.csquiz.domain.generation.service; import jakarta.persistence.EntityNotFoundException; -import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; 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.AttendanceDeadLineDto; 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.SessionListResponse; -import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.common.error.exception.ImageException; import org.cotato.csquiz.common.schedule.SchedulerService; @@ -65,7 +63,7 @@ public AddSessionResponse addSession(AddSessionRequest request) throws ImageExce .generation(findGeneration) .title(request.title()) .placeName(request.placeName()) - .sessionDate(request.sessionDate()) + .sessionDateTime(request.sessionDateTime()) .sessionContents(SessionContents.builder() .csEducation(request.csEducation()) .devTalk(request.devTalk()) @@ -80,18 +78,9 @@ public AddSessionResponse addSession(AddSessionRequest request) throws ImageExce sessionImageService.addSessionImages(request.images(), savedSession); } - Location location = Location.builder() - .latitude(request.latitude()) - .longitude(request.longitude()) - .build(); - - AttendanceDeadLineDto attendanceDeadLine = AttendanceDeadLineDto.builder() - .attendanceDeadLine(request.attendanceDeadLine()) - .lateDeadLine(request.lateDeadLine()) - .build(); - - attendanceAdminService.addAttendance(session, location, attendanceDeadLine); - schedulerService.scheduleSessionNotification(savedSession.getSessionDate()); + attendanceAdminService.addAttendance(session, Location.location(request.latitude(), request.longitude()), + request.attendanceDeadLine(), request.lateDeadLine()); + schedulerService.scheduleSessionNotification(savedSession.getSessionDateTime()); return AddSessionResponse.from(savedSession); } @@ -102,12 +91,6 @@ private int calculateLastSessionNumber(Generation generation) { .orElse(-1); } - @Transactional - public void updateSessionNumber(UpdateSessionNumberRequest request) { - Session session = findSessionById(request.sessionId()); - session.changeSessionNumber(session.getNumber()); - } - @Transactional public void updateSession(UpdateSessionRequest request) { Session session = findSessionById(request.sessionId()); @@ -123,31 +106,31 @@ public void updateSession(UpdateSessionRequest request) { .networking(request.networking()) .build()); - updateSessionDate(session, request.sessionDate(), request.attendTime()); + updateSessionDateTime(session, request.sessionDateTime(), request.attendTime().attendanceDeadLine(), request.attendTime().lateDeadLine()); sessionRepository.save(session); } - public void updateSessionDate(Session session, LocalDate newDate, AttendanceDeadLineDto newDeadline) { - Attendance findAttendance = attendanceRepository.findBySessionId(session.getId()) + @Transactional + public void updateSessionDateTime(Session session, LocalDateTime newDateTime, LocalTime attendanceDeadline, LocalTime lateDeadline) { + Attendance attendance = attendanceRepository.findBySessionId(session.getId()) .orElseGet(() -> Attendance.builder() .session(session) .build()); - // 날짜가 바뀌지 않았고, 출결 시간이 모두 동일한 경우 - if (newDate.equals(session.getSessionDate()) && - findAttendance.getAttendanceDeadLine().toLocalTime().equals(newDeadline.attendanceDeadLine()) && - findAttendance.getLateDeadLine().toLocalTime().equals(newDeadline.lateDeadLine())) { + if (newDateTime.equals(session.getSessionDateTime()) && + attendance.getAttendanceDeadLine().toLocalTime().equals(attendanceDeadline) && + attendance.getLateDeadLine().toLocalTime().equals(lateDeadline)) { return; } - session.updateSessionDate(newDate); + session.updateSessionDateTime(newDateTime); - LocalDateTime newAttendanceDeadline = LocalDateTime.of(newDate, newDeadline.attendanceDeadLine()); - LocalDateTime newLateDeadline = LocalDateTime.of(newDate, newDeadline.lateDeadLine()); - findAttendance.updateDeadLine(newAttendanceDeadline, newLateDeadline); + LocalDateTime newAttendanceDeadline = LocalDateTime.of(newDateTime.toLocalDate(), attendanceDeadline); + LocalDateTime newLateDeadline = LocalDateTime.of(newDateTime.toLocalDate(), lateDeadline); + attendance.updateDeadLine(newAttendanceDeadline, newLateDeadline); - attendanceRepository.save(findAttendance); - attendanceRecordService.updateAttendanceStatus(findAttendance); + attendanceRepository.save(attendance); + attendanceRecordService.updateAttendanceStatus(newDateTime, attendance); } public List findSessionsByGenerationId(Long generationId) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b609b164..4a3ddbb2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -66,3 +66,6 @@ aes: secret: key: ${AES_SECRET_KEY} salt: ${AES_SECRET_SALT} + +location: + distance: ${STANDARD_DISTANCE} \ No newline at end of file diff --git a/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java b/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java index 4caf6dcf..69f94399 100644 --- a/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java +++ b/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java @@ -1,7 +1,7 @@ package org.cotato.csquiz.domain.attendance.util; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.LocalDate; import java.time.LocalDateTime; @@ -11,7 +11,6 @@ import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; -import org.cotato.csquiz.domain.attendance.enums.DeadLine; import org.cotato.csquiz.domain.generation.entity.Session; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,7 +28,7 @@ class AttendanceUtilTest { .build(); //when - AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceOpenStatus(attendance, + AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceOpenStatus(LocalDateTime.now(), attendance, LocalDateTime.now().plusDays(1)); //then @@ -40,18 +39,19 @@ class AttendanceUtilTest { void 기준시간_전이면_출석이_닫혀있다() { //given LocalDateTime attendanceDeadLine = LocalDateTime.of(2024, Month.AUGUST, 9, 19, 10, 0); - + Session session = Session.builder() + .sessionDateTime(attendanceDeadLine.minusMinutes(10)) + .build(); Attendance attendance = Attendance.builder() .attendanceDeadLine(attendanceDeadLine) .lateDeadLine(attendanceDeadLine.plusMinutes(10)) - .session(Session.builder() - .build()) + .session(session) .build(); - LocalDateTime beforeTime = LocalDateTime.of(LocalDate.of(2024, Month.AUGUST, 9),DeadLine.ATTENDANCE_START_TIME.getTime().minusMinutes(10)); + LocalDateTime beforeTime = session.getSessionDateTime().minusMinutes(10); //when - AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceOpenStatus(attendance, beforeTime); + AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceOpenStatus(LocalDateTime.of(2024, Month.AUGUST, 9, 19, 0, 0), attendance, beforeTime); //then assertEquals(attendanceStatus, AttendanceOpenStatus.BEFORE); @@ -60,11 +60,12 @@ class AttendanceUtilTest { @Test void 지각마감이_세션시작보다_빠를_수_없다() { //given - LocalTime attendDeadline = LocalTime.of(18, 40, 0); + LocalDateTime sessionStartTime = LocalDateTime.of(LocalDate.now(), LocalTime.of(15, 0)); + LocalTime attendDeadline = LocalTime.of(19, 40, 0); LocalTime lateDeadline = LocalTime.of(19, 20, 0); //when, then - assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) + assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(sessionStartTime, attendDeadline, lateDeadline)) .isInstanceOf(AppException.class) .extracting("errorCode") .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); @@ -74,25 +75,12 @@ class AttendanceUtilTest { @Test void 지각마감보다_출석마감이_빠르다() { //given + LocalDateTime sessionStartTime = LocalDateTime.of(LocalDate.now(), LocalTime.of(15, 0)); LocalTime attendDeadline = LocalTime.of(19, 40, 0); LocalTime lateDeadline = LocalTime.of(19, 20, 0); //when, then - assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) - .isInstanceOf(AppException.class) - .extracting("errorCode") - .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); - } - - @DisplayName(value = "지각 마감이 세션 종료보다 늦으면 예외를 발생한다.") - @Test - void 지각마감시간_검증_기능() { - //given - LocalTime attendDeadline = LocalTime.of(19, 40, 0); - LocalTime lateDeadline = LocalTime.of(20, 20, 0); - - //when, then - assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) + assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(sessionStartTime, attendDeadline, lateDeadline)) .isInstanceOf(AppException.class) .extracting("errorCode") .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); From 1b651849305fa2c121863ece12bb512bc0e1cedc Mon Sep 17 00:00:00 2001 From: Youth <109585620+Youthhing@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:19:26 +0900 Subject: [PATCH 4/7] =?UTF-8?q?Feature:=20=EC=97=B4=EB=A0=A4=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=B6=9C=EA=B2=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=B6=9C=EA=B2=B0=20=EA=B8=B0=EB=A1=9D=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 출석과 출결 기록의 연관관계 제거 * feat: 부원 출결 기록 반환 시 출결 기록의 존재 여부를 기준으로 변경 * feat: 서울 지역을 기반으로 하는 시간 유틸 클래스 구현 * feat: 출결 미입력 활동 부원 결석 처리 기능 구현 * feat: 출결 미입력 활동 부원 결석 처리 스케줄러 구현 * feat: 세션 추가 시 결석 기록 업데이트 스케줄링 --- .../attendance/dto/MemberAttendResponse.java | 23 ++++---- .../common/schedule/SchedulerService.java | 38 +++++++++++-- .../cotato/csquiz/common/util/TimeUtil.java | 14 +++++ .../attendance/entity/AttendanceRecord.java | 20 +++++-- .../attendance/enums/AttendanceType.java | 3 +- .../AttendanceRecordRepository.java | 7 +-- .../service/AttendanceRecordService.java | 56 +++++++++++++------ .../generation/service/SessionService.java | 7 ++- 8 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/cotato/csquiz/common/util/TimeUtil.java diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java index c1fb67cc..0c6675b0 100644 --- a/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/MemberAttendResponse.java @@ -30,29 +30,30 @@ public record MemberAttendResponse( @Schema(description = "마감된 출석에 대한 출결 결과", nullable = true) AttendanceResult attendanceResult ) { - public static MemberAttendResponse closedAttendanceResponse(Session session, AttendanceRecord attendanceRecord) { + public static MemberAttendResponse unrecordedAttendance(Session session, Attendance attendance, Long memberId) { return new MemberAttendResponse( session.getId(), - attendanceRecord.getAttendance().getId(), - attendanceRecord.getMemberId(), + attendance.getId(), + memberId, session.getTitle(), session.getSessionDateTime(), - AttendanceOpenStatus.CLOSED, - attendanceRecord.getAttendanceType(), - attendanceRecord.getAttendanceResult() + AttendanceUtil.getAttendanceOpenStatus(session.getSessionDateTime(), attendance, LocalDateTime.now()), + null, + null ); } - public static MemberAttendResponse openedAttendanceResponse(Attendance attendance, Session session, Long memberId) { + public static MemberAttendResponse recordedAttendance(Session session, Attendance attendance, + AttendanceRecord attendanceRecord) { return new MemberAttendResponse( session.getId(), - attendance.getId(), - memberId, + attendanceRecord.getAttendanceId(), + attendanceRecord.getMemberId(), session.getTitle(), session.getSessionDateTime(), AttendanceUtil.getAttendanceOpenStatus(session.getSessionDateTime(), attendance, LocalDateTime.now()), - null, - null + attendanceRecord.getAttendanceType(), + attendanceRecord.getAttendanceResult() ); } } diff --git a/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java b/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java index 84ecf0c5..a3dd5ebb 100644 --- a/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java +++ b/src/main/java/org/cotato/csquiz/common/schedule/SchedulerService.java @@ -1,15 +1,16 @@ package org.cotato.csquiz.common.schedule; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cotato.csquiz.common.sse.SseSender; -import org.cotato.csquiz.domain.attendance.enums.DeadLine; +import org.cotato.csquiz.common.util.TimeUtil; +import org.cotato.csquiz.domain.attendance.service.AttendanceRecordService; import org.cotato.csquiz.domain.auth.entity.RefusedMember; import org.cotato.csquiz.domain.auth.enums.MemberRole; import org.cotato.csquiz.domain.auth.repository.MemberRepository; @@ -29,8 +30,10 @@ public class SchedulerService { private final RefusedMemberRepository refusedMemberRepository; private final MemberRepository memberRepository; private final EducationService educationService; + private final AttendanceRecordService attendanceRecordService; private final SseSender sseSender; private final TaskScheduler taskScheduler; + private final Map> scheduledTasks = new ConcurrentHashMap<>(); @Transactional @Scheduled(cron = "0 0 0 * * *") @@ -56,8 +59,33 @@ public void closeAllCsQuiz() { } public void scheduleSessionNotification(LocalDateTime notificationTime) { - ZonedDateTime zonedDateTime = notificationTime.atZone(ZoneId.of("Asia/Seoul")); + ZonedDateTime zonedDateTime = TimeUtil.getSeoulZoneTime(notificationTime); taskScheduler.schedule(() -> sseSender.sendNotification(notificationTime), zonedDateTime.toInstant()); } + + public void scheduleAbsentRecords(LocalDateTime sessionDateTime, Long sessionId) { + // 이미 해당 세션에 스케줄된 작업이 있으면 취소 + ScheduledFuture existingTask = scheduledTasks.get(sessionId); + if (existingTask != null && !existingTask.isDone()) { + existingTask.cancel(false); + } + + LocalDateTime nextDateTime = sessionDateTime.plusDays(1); + ZonedDateTime zonedDateTime = TimeUtil.getSeoulZoneTime(nextDateTime); + + // 새로운 작업 스케줄링 + ScheduledFuture newTask = taskScheduler.schedule( + () -> { + try { + attendanceRecordService.updateUnrecordedAttendanceRecord(sessionId); + } finally { + scheduledTasks.remove(sessionId); + } + }, + zonedDateTime.toInstant() + ); + + scheduledTasks.put(sessionId, newTask); + } } diff --git a/src/main/java/org/cotato/csquiz/common/util/TimeUtil.java b/src/main/java/org/cotato/csquiz/common/util/TimeUtil.java new file mode 100644 index 00000000..ddb1fe0d --- /dev/null +++ b/src/main/java/org/cotato/csquiz/common/util/TimeUtil.java @@ -0,0 +1,14 @@ +package org.cotato.csquiz.common.util; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class TimeUtil { + + private static final String SEOUL_ZONE = "Asia/Seoul"; + + public static ZonedDateTime getSeoulZoneTime(LocalDateTime localDateTime){ + return localDateTime.atZone(ZoneId.of(SEOUL_ZONE)); + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java index 45050d39..11a24bb8 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java @@ -49,11 +49,10 @@ public class AttendanceRecord extends BaseTimeEntity { @Column(name = "member_id", nullable = false) private Long memberId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "attendance_id") - private Attendance attendance; + @Column(name = "attendance_id", nullable = false) + private Long attendanceId; - @Column(name = "attend_time", nullable = false) + @Column(name = "attend_time") private LocalDateTime attendTime; private AttendanceRecord(AttendanceType attendanceType, AttendanceResult attendanceResult, Double locationAccuracy, @@ -62,7 +61,7 @@ private AttendanceRecord(AttendanceType attendanceType, AttendanceResult attenda this.attendanceResult = attendanceResult; this.locationAccuracy = locationAccuracy; this.memberId = memberId; - this.attendance = attendance; + this.attendanceId = attendance.getId(); this.attendTime = attendTime; } @@ -90,6 +89,17 @@ public static AttendanceRecord offlineRecord(Attendance attendance, Long memberI ); } + public static AttendanceRecord absentRecord(Attendance attendance, Long memberId) { + return new AttendanceRecord( + AttendanceType.ABSENT, + AttendanceResult.ABSENT, + null, + memberId, + attendance, + null + ); + } + public void updateAttendanceType(AttendanceType attendanceType) { this.attendanceType = attendanceType; } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceType.java b/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceType.java index 84884772..64a7bb7e 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceType.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceType.java @@ -8,7 +8,8 @@ public enum AttendanceType { OFFLINE("대면 출석"), - ONLINE("비대면 출석") + ONLINE("비대면 출석"), + ABSENT("출결 미 입력") ; private final String description; diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java b/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java index dec5b31f..62b6920b 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Optional; -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; @@ -10,14 +9,14 @@ import org.springframework.data.repository.query.Param; public interface AttendanceRecordRepository extends JpaRepository { - @Query("select a from AttendanceRecord a where a.attendance in :attendances") - List findAllByAttendanceIdsInQuery(@Param("attendances") List attendances); + @Query("select a from AttendanceRecord a where a.attendanceId in :attendanceIds") + List findAllByAttendanceIdsInQuery(@Param("attendanceIds") List attendanceIds); boolean existsByAttendanceIdAndMemberIdAndAttendanceType(Long attendanceId, Long memberId, AttendanceType attendanceType); Optional findByMemberIdAndAttendanceId(Long memberId, Long attendanceId); - @Query("select a from AttendanceRecord a where a.attendance.id in :attendanceIds and a.memberId = :memberId") + @Query("select a from AttendanceRecord a where a.attendanceId in :attendanceIds and a.memberId = :memberId") List findAllByAttendanceIdsInQueryAndMemberId(@Param("attendanceIds") List attendanceIds, @Param("memberId") Long memberId); List findAllByAttendanceId(Long attendanceId); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java index 4eb1e12c..85e0f5ef 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -45,8 +46,11 @@ public class AttendanceRecordService { private final SessionRepository sessionRepository; public List generateAttendanceResponses(List attendances) { - List records = attendanceRecordRepository.findAllByAttendanceIdsInQuery( - attendances); + List attendanceIds = attendances.stream() + .map(Attendance::getId) + .toList(); + + List records = attendanceRecordRepository.findAllByAttendanceIdsInQuery(attendanceIds); Map> recordsByMemberId = records.stream() .collect(Collectors.groupingBy(AttendanceRecord::getMemberId)); @@ -93,28 +97,27 @@ public MemberAttendanceRecordsResponse findAllRecordsBy(final Long generationId, List sessionIds = sessions.stream() .map(Session::getId) .toList(); - // 세션에 해당하는 모든 출결을 찾아 - LocalDateTime currentTime = LocalDateTime.now(); - Map> isClosedAttendance = attendanceRepository.findAllBySessionIdsInQuery(sessionIds) - .stream() - .collect(Collectors.partitioningBy(attendance -> - getAttendanceOpenStatus(sessionMap.get(attendance.getSessionId()).getSessionDateTime(), - attendance, currentTime) == AttendanceOpenStatus.CLOSED)); + List attendances = attendanceRepository.findAllBySessionIdsInQuery(sessionIds); - List closedAttendanceIds = isClosedAttendance.get(true).stream() + List attendanceIds = attendances.stream() .map(Attendance::getId) .toList(); - List responses = attendanceRecordRepository.findAllByAttendanceIdsInQueryAndMemberId( - closedAttendanceIds, memberId).stream() - .map(ar -> MemberAttendResponse.closedAttendanceResponse( - sessionMap.get(ar.getAttendance().getSessionId()), ar)) + Map attendanceRecordMap = attendanceRecordRepository.findAllByAttendanceIdsInQueryAndMemberId( + attendanceIds, memberId).stream() + .collect(Collectors.toUnmodifiableMap(AttendanceRecord::getAttendanceId, Function.identity())); + + Map> recordedAttendance = attendances.stream() + .collect(Collectors.partitioningBy(at -> attendanceRecordMap.containsKey(at.getId()))); + + List responses = recordedAttendance.get(true).stream() + .map(at -> MemberAttendResponse.recordedAttendance(sessionMap.get(at.getSessionId()), at, + attendanceRecordMap.get(at.getId()))) .collect(Collectors.toList()); - responses.addAll(isClosedAttendance.get(false).stream() - .map(attendance -> MemberAttendResponse.openedAttendanceResponse(attendance, - sessionMap.get(attendance.getSessionId()), memberId)) + responses.addAll(recordedAttendance.get(false).stream() + .map(at -> MemberAttendResponse.unrecordedAttendance(sessionMap.get(at.getSessionId()), at, memberId)) .toList()); return MemberAttendanceRecordsResponse.of(generationId, responses); @@ -132,4 +135,23 @@ public void updateAttendanceStatus(LocalDateTime sessionStartTime, Attendance at attendanceRecordRepository.saveAll(attendanceRecords); } + + @Transactional + public void updateUnrecordedAttendanceRecord(Long sessionId) { + Attendance attendance = attendanceRepository.findBySessionId(sessionId) + .orElseThrow(() -> new EntityNotFoundException("해당 세션에 대한 출석이 생성되지 않았습니다.")); + + // 출결 입력을 한 부원 + Set attendedMember = attendanceRecordRepository.findAllByAttendanceId(attendance.getId()).stream() + .map(AttendanceRecord::getMemberId) + .collect(Collectors.toUnmodifiableSet()); + + List unrecordedMemberIds = memberService.findActiveMember().stream() + .map(Member::getId) + .filter(id -> !attendedMember.contains(id)) + .map(id -> AttendanceRecord.absentRecord(attendance, id)) + .toList(); + + attendanceRecordRepository.saveAll(unrecordedMemberIds); + } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java index 76642b6c..7b68d9d8 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java @@ -81,6 +81,7 @@ public AddSessionResponse addSession(AddSessionRequest request) throws ImageExce attendanceAdminService.addAttendance(session, Location.location(request.latitude(), request.longitude()), request.attendanceDeadLine(), request.lateDeadLine()); schedulerService.scheduleSessionNotification(savedSession.getSessionDateTime()); + schedulerService.scheduleAbsentRecords(savedSession.getSessionDateTime(), savedSession.getId()); return AddSessionResponse.from(savedSession); } @@ -106,12 +107,14 @@ public void updateSession(UpdateSessionRequest request) { .networking(request.networking()) .build()); - updateSessionDateTime(session, request.sessionDateTime(), request.attendTime().attendanceDeadLine(), request.attendTime().lateDeadLine()); + updateSessionDateTime(session, request.sessionDateTime(), request.attendTime().attendanceDeadLine(), + request.attendTime().lateDeadLine()); sessionRepository.save(session); } @Transactional - public void updateSessionDateTime(Session session, LocalDateTime newDateTime, LocalTime attendanceDeadline, LocalTime lateDeadline) { + public void updateSessionDateTime(Session session, LocalDateTime newDateTime, LocalTime attendanceDeadline, + LocalTime lateDeadline) { Attendance attendance = attendanceRepository.findBySessionId(session.getId()) .orElseGet(() -> Attendance.builder() .session(session) From fe93868a286bec2dac16b9bff3d474e1963bc056 Mon Sep 17 00:00:00 2001 From: Yun Ha Park <127095481+yunhacandy@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:19:51 +0900 Subject: [PATCH 5/7] =?UTF-8?q?Feature:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=94=94=ED=85=8C=EC=9D=BC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 프로젝트 구체 정보 업로드 중 디테일 이미지 업로드 기능 구현 - 디테일 이미지 업로드 메소드 추가 - 디테일 이미지 순서 보장 로직 추가 * refactor: jpa 메서드명 수정 * refactor: detail 이미지 업로드 시 for문 인덱스를 활용하여 순서 관리 * refactor: ProjectImage의 imageOrder 처리 및 기본값 추가 - ProjectImageInfoResponse에 imageOrder에 대한 정보 추가 - 로고와 썸네일 이미지는 imageOrder가 필요하지 않으나 일관성을 위해 기본값 0으로 설정. * refactor: 이미지 순서 관한 로직 리팩토링 - imageOrder의 기본값을 1로 설정 - 기본값 변경으로 인해 프로젝트 상세 정보 중 이미지 가져올때 오름차순 신경 안쓰고 projectId로 한번에 가져오게 수정 - DB에 null 값 최소화하기 위해 원시타입 사용 - 원시타입 사용으로 인해 null 체크 삭제 - 중복되는 로직 통합 * refactor: ProjectImage 엔티티 정적 팩토리 메소드 사용하게 수정 * refactor: project image order에 대한 자료형 통일 및 제약 조건 추가 * refactor: addImage 메서드 제거 및 직접적인 이미지 처리 방식으로 변경 --- .../project/controller/ProjectController.java | 2 +- .../dto/CreateProjectImageRequest.java | 4 ++- .../project/dto/ProjectImageInfoResponse.java | 6 ++-- .../generation/entity/ProjectImage.java | 31 +++++++++++++------ .../service/ProjectImageService.java | 17 ++++++++-- .../generation/service/ProjectService.java | 1 - 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/cotato/csquiz/api/project/controller/ProjectController.java b/src/main/java/org/cotato/csquiz/api/project/controller/ProjectController.java index aa9536da..1024c22f 100644 --- a/src/main/java/org/cotato/csquiz/api/project/controller/ProjectController.java +++ b/src/main/java/org/cotato/csquiz/api/project/controller/ProjectController.java @@ -54,7 +54,7 @@ public ResponseEntity createProject(@RequestBody @Valid C @PostMapping(value = "/images", consumes = "multipart/form-data") public ResponseEntity createProjectImage(@ModelAttribute CreateProjectImageRequest request) throws ImageException { - projectImageService.createProjectImage(request.projectId(), request.logoImage(), request.thumbNailImage()); + projectImageService.createProjectImage(request.projectId(), request.logoImage(), request.thumbNailImage(), request.detailImages()); return ResponseEntity.status(HttpStatus.CREATED).build(); } } diff --git a/src/main/java/org/cotato/csquiz/api/project/dto/CreateProjectImageRequest.java b/src/main/java/org/cotato/csquiz/api/project/dto/CreateProjectImageRequest.java index 0a0bf4cc..0429766a 100644 --- a/src/main/java/org/cotato/csquiz/api/project/dto/CreateProjectImageRequest.java +++ b/src/main/java/org/cotato/csquiz/api/project/dto/CreateProjectImageRequest.java @@ -1,10 +1,12 @@ package org.cotato.csquiz.api.project.dto; +import java.util.List; import org.springframework.web.multipart.MultipartFile; public record CreateProjectImageRequest( Long projectId, MultipartFile logoImage, - MultipartFile thumbNailImage + MultipartFile thumbNailImage, + List detailImages ) { } diff --git a/src/main/java/org/cotato/csquiz/api/project/dto/ProjectImageInfoResponse.java b/src/main/java/org/cotato/csquiz/api/project/dto/ProjectImageInfoResponse.java index b9b7905e..2795722c 100644 --- a/src/main/java/org/cotato/csquiz/api/project/dto/ProjectImageInfoResponse.java +++ b/src/main/java/org/cotato/csquiz/api/project/dto/ProjectImageInfoResponse.java @@ -6,13 +6,15 @@ public record ProjectImageInfoResponse( Long imageId, String imageUrl, - ProjectImageType projectImageType + ProjectImageType projectImageType, + int imageOrder ) { public static ProjectImageInfoResponse from(ProjectImage projectImage) { return new ProjectImageInfoResponse( projectImage.getId(), projectImage.getS3Info().getUrl(), - projectImage.getProjectImageType() + projectImage.getProjectImageType(), + projectImage.getImageOrder() ); } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java index ff628f69..6622d05c 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java @@ -9,7 +9,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.cotato.csquiz.common.entity.S3Info; @@ -35,26 +34,38 @@ public class ProjectImage { @Column(name = "project_id", nullable = false) private Long projectId; - @Builder - public ProjectImage(ProjectImageType projectImageType, S3Info s3Info, Long projectId) { + @Column(name = "project_image_order", nullable = false) + private int imageOrder; + + private ProjectImage(ProjectImageType projectImageType, S3Info s3Info, Long projectId, int imageOrder) { this.projectImageType = projectImageType; this.s3Info = s3Info; this.projectId = projectId; + this.imageOrder = imageOrder; } - public static ProjectImage logoImage(S3Info imageInfo, Long projectId) { + public static ProjectImage logoImage(S3Info s3Info, Long projectId) { return new ProjectImage( ProjectImageType.LOGO, - imageInfo, - projectId - ); + s3Info, + projectId, + 1); } - public static ProjectImage thumbNailImage(S3Info thumbNailInfo, Long projectId) { + public static ProjectImage thumbnailImage(S3Info s3Info, Long projectId) { return new ProjectImage( ProjectImageType.THUMBNAIL, - thumbNailInfo, - projectId + s3Info, + projectId, + 1); + } + + public static ProjectImage detailImage(S3Info imageInfo, Long projectId, int imageOrder) { + return new ProjectImage( + ProjectImageType.DETAIL, + imageInfo, + projectId, + imageOrder ); } } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java index 622f6683..ba1205cf 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java @@ -1,6 +1,7 @@ package org.cotato.csquiz.domain.generation.service; -import static org.cotato.csquiz.common.util.FileUtil.*; +import static org.cotato.csquiz.common.util.FileUtil.convert; +import static org.cotato.csquiz.common.util.FileUtil.convertToWebp; import java.io.File; import java.util.ArrayList; @@ -24,7 +25,8 @@ public class ProjectImageService { private final ProjectImageRepository projectImageRepository; @Transactional - public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNailImage) + public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNailImage, + List detailImages) throws ImageException { List newImages = new ArrayList<>(); @@ -34,7 +36,16 @@ public void createProjectImage(Long projectId, MultipartFile logoImage, Multipar File webpThumbNailImage = convertToWebp(convert(thumbNailImage)); S3Info thumbNailInfo = s3Uploader.uploadFiles(webpThumbNailImage, PROJECT_IMAGE); - newImages.add(ProjectImage.thumbNailImage(thumbNailInfo, projectId)); + newImages.add(ProjectImage.thumbnailImage(thumbNailInfo, projectId)); + + if (detailImages != null && !detailImages.isEmpty()) { + for (int orderIndex = 1; orderIndex <= detailImages.size(); orderIndex++) { + MultipartFile detailImage = detailImages.get(orderIndex - 1); + File webpDetailImage = convertToWebp(convert(detailImage)); + S3Info detailImageInfo = s3Uploader.uploadFiles(webpDetailImage, PROJECT_IMAGE); + newImages.add(ProjectImage.detailImage(detailImageInfo, projectId, orderIndex)); + } + } projectImageRepository.saveAll(newImages); } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectService.java index b737fbd0..234fd85d 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectService.java @@ -27,7 +27,6 @@ @RequiredArgsConstructor public class ProjectService { - private final ProjectImageService projectImageService; private final ProjectMemberService projectMemberService; private final ProjectRepository projectRepository; private final ProjectImageRepository projectImageRepository; From 5795753eaf172b36cb138200c3b663a2fc6db687 Mon Sep 17 00:00:00 2001 From: Yun Ha Park <127095481+yunhacandy@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:36:47 +0900 Subject: [PATCH 6/7] =?UTF-8?q?Fix:=20=EA=B8=B0=EC=A1=B4=EC=97=90=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=8B=9C=20=EC=98=88=EC=99=B8=EA=B0=80=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20(#157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 이미지 순서 0부터 시작하게 변경 * fix: 로고와 썸네일 이미지 존재할 경우 예외처리 발생하게 수정 * feat: Project, ProjectImage, ProjectMember 엔티티에 BaseTimeEntity 추가 * refactor: 이미지 존재 여부 검증 메서드 변경 * refactor: 이미지 존재 여부 검증 메서드 분리 --- .../cotato/csquiz/common/error/ErrorCode.java | 4 +++ .../domain/generation/entity/Project.java | 3 ++- .../generation/entity/ProjectImage.java | 7 ++--- .../generation/entity/ProjectMember.java | 3 ++- .../repository/ProjectImageRepository.java | 4 ++- .../service/ProjectImageService.java | 26 ++++++++++++++++--- 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index bb28d755..ad5e2e05 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -84,6 +84,10 @@ public enum ErrorCode { ALREADY_ATTEND(HttpStatus.CONFLICT, "AT-301", "이미 해당 타입으로 출석한 기록이 있습니다."), ATTENDANCE_NOT_OPEN(HttpStatus.BAD_REQUEST, "AT-401", "출석 시간이 아닙니다."), + //프로젝트 관련 + LOGO_IMAGE_EXIST(HttpStatus.CONFLICT, "PJ-301", "이미 로고 이미지가 존재합니다."), + THUMBNAIL_IMAGE_EXIST(HttpStatus.CONFLICT, "PJ-302", "이미 썸네일 이미지가 존재합니다."), + // 500 오류 -> 서버측에서 처리가 실패한 부분들 WEBSOCKET_SEND_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "소캣 메세지 전송 실패"), IMAGE_PROCESSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-002", "이미지 처리에 실패했습니다."), diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/Project.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/Project.java index c3cf118c..84ef7fc7 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/Project.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/Project.java @@ -9,11 +9,12 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Project { +public class Project extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java index 6622d05c..9eff42ae 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectImage.java @@ -11,13 +11,14 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; import org.cotato.csquiz.common.entity.S3Info; import org.cotato.csquiz.domain.generation.enums.ProjectImageType; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProjectImage { +public class ProjectImage extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -49,7 +50,7 @@ public static ProjectImage logoImage(S3Info s3Info, Long projectId) { ProjectImageType.LOGO, s3Info, projectId, - 1); + 0); } public static ProjectImage thumbnailImage(S3Info s3Info, Long projectId) { @@ -57,7 +58,7 @@ public static ProjectImage thumbnailImage(S3Info s3Info, Long projectId) { ProjectImageType.THUMBNAIL, s3Info, projectId, - 1); + 0); } public static ProjectImage detailImage(S3Info imageInfo, Long projectId, int imageOrder) { diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectMember.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectMember.java index 40cef8fa..e470ac4f 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectMember.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/ProjectMember.java @@ -11,12 +11,13 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; import org.cotato.csquiz.domain.auth.enums.MemberPosition; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProjectMember { +public class ProjectMember extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/cotato/csquiz/domain/generation/repository/ProjectImageRepository.java b/src/main/java/org/cotato/csquiz/domain/generation/repository/ProjectImageRepository.java index 2bfb7486..d321870a 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/repository/ProjectImageRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/repository/ProjectImageRepository.java @@ -8,4 +8,6 @@ public interface ProjectImageRepository extends JpaRepository { List findAllByProjectId(Long projectId); List findAllByProjectIdInAndProjectImageType(List projectIds, ProjectImageType projectImageType); -} + + boolean existsByProjectIdAndProjectImageType(Long projectId, ProjectImageType projectImageType); +} \ No newline at end of file diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java index ba1205cf..d8d75e96 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/ProjectImageService.java @@ -8,9 +8,12 @@ import java.util.List; import lombok.RequiredArgsConstructor; 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.common.error.exception.ImageException; import org.cotato.csquiz.common.s3.S3Uploader; import org.cotato.csquiz.domain.generation.entity.ProjectImage; +import org.cotato.csquiz.domain.generation.enums.ProjectImageType; import org.cotato.csquiz.domain.generation.repository.ProjectImageRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,8 +29,11 @@ public class ProjectImageService { @Transactional public void createProjectImage(Long projectId, MultipartFile logoImage, MultipartFile thumbNailImage, - List detailImages) - throws ImageException { + List detailImages) throws ImageException { + + validateLogoImageExistence(projectId); + validateThumbNailImageExistence(projectId); + List newImages = new ArrayList<>(); File webpLogoImage = convertToWebp(convert(logoImage)); @@ -39,8 +45,8 @@ public void createProjectImage(Long projectId, MultipartFile logoImage, Multipar newImages.add(ProjectImage.thumbnailImage(thumbNailInfo, projectId)); if (detailImages != null && !detailImages.isEmpty()) { - for (int orderIndex = 1; orderIndex <= detailImages.size(); orderIndex++) { - MultipartFile detailImage = detailImages.get(orderIndex - 1); + for (int orderIndex = 0; orderIndex < detailImages.size(); orderIndex++) { + MultipartFile detailImage = detailImages.get(orderIndex); File webpDetailImage = convertToWebp(convert(detailImage)); S3Info detailImageInfo = s3Uploader.uploadFiles(webpDetailImage, PROJECT_IMAGE); newImages.add(ProjectImage.detailImage(detailImageInfo, projectId, orderIndex)); @@ -49,4 +55,16 @@ public void createProjectImage(Long projectId, MultipartFile logoImage, Multipar projectImageRepository.saveAll(newImages); } + + private void validateThumbNailImageExistence(Long projectId) { + if (projectImageRepository.existsByProjectIdAndProjectImageType(projectId, ProjectImageType.THUMBNAIL)) { + throw new AppException(ErrorCode.THUMBNAIL_IMAGE_EXIST); + } + } + + private void validateLogoImageExistence(Long projectId) { + if (projectImageRepository.existsByProjectIdAndProjectImageType(projectId, ProjectImageType.LOGO)) { + throw new AppException(ErrorCode.LOGO_IMAGE_EXIST); + } + } } From 4e1fded67de281a51cc81acfbb1cdbbc6789b651 Mon Sep 17 00:00:00 2001 From: GiHun Nam <52378919+gikhoon@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:08:20 +0900 Subject: [PATCH 7/7] =?UTF-8?q?Fix:=20=ED=82=B9=ED=82=B9=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84,=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20API=20=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/cotato/csquiz/common/config/SecurityConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/cotato/csquiz/common/config/SecurityConfig.java b/src/main/java/org/cotato/csquiz/common/config/SecurityConfig.java index 8ae091d6..e4a054f8 100644 --- a/src/main/java/org/cotato/csquiz/common/config/SecurityConfig.java +++ b/src/main/java/org/cotato/csquiz/common/config/SecurityConfig.java @@ -71,6 +71,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(WHITE_LIST).permitAll() .requestMatchers("/v1/api/education/result/**").hasAnyRole("MEMBER", "EDUCATION", "ADMIN") .requestMatchers("/v1/api/education/from").hasAnyRole("MEMBER", "EDUCATION", "ADMIN") + .requestMatchers(new AntPathRequestMatcher("/v1/api/education/winner", "GET")).hasAnyRole("MEMBER", "EDUCATION", "ADMIN") + .requestMatchers(new AntPathRequestMatcher("/v1/api/education/kings", "GET")).hasAnyRole("MEMBER", "EDUCATION", "ADMIN") .requestMatchers(new AntPathRequestMatcher("/v1/api/education/status", "GET")) .hasAnyRole("MEMBER", "EDUCATION", "ADMIN") .requestMatchers(new AntPathRequestMatcher("/v1/api/education", "GET")).authenticated()