Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 방 수정 기능 & 스케줄러 부분 분리 및 리팩토링(#605) #606

Merged
merged 7 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/main/java/corea/exception/ExceptionType.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum ExceptionType {
ALREADY_APPLY(HttpStatus.BAD_REQUEST, "해당 방에 이미 참여했습니다."),
NOT_ALREADY_APPLY(HttpStatus.BAD_REQUEST, "아직 참여하지 않은 방입니다."),
ROOM_STATUS_INVALID(HttpStatus.BAD_REQUEST, "방이 마감되었습니다."),
MEMBER_IS_NOT_MANAGER(HttpStatus.BAD_REQUEST, "매니저가 아닙니다."),
ROOM_PARTICIPANT_EXCEED(HttpStatus.BAD_REQUEST, "방 참여 인원 수가 최대입니다."),
PARTICIPANT_SIZE_LACK(HttpStatus.BAD_REQUEST, "참여 인원이 부족하여 매칭을 진행할 수 없습니다."),
PARTICIPANT_SIZE_LACK_DUE_TO_PULL_REQUEST(HttpStatus.BAD_REQUEST, "pull request 미제출로 인해 인원이 부족하여 매칭을 진행할 수 없습니다."),
Expand Down
23 changes: 9 additions & 14 deletions backend/src/main/java/corea/room/controller/RoomController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@
import corea.auth.annotation.AccessedMember;
import corea.auth.annotation.LoginMember;
import corea.auth.domain.AuthInfo;
import corea.room.dto.RoomCreateRequest;
import corea.room.dto.RoomParticipantResponses;
import corea.room.dto.RoomResponse;
import corea.room.dto.RoomResponses;
import corea.room.dto.*;
import corea.room.service.RoomService;
import corea.scheduler.service.AutomaticMatchingService;
import corea.scheduler.service.AutomaticUpdateService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -22,20 +17,23 @@
public class RoomController implements RoomControllerSpecification {

private final RoomService roomService;
private final AutomaticUpdateService automaticUpdateService;
private final AutomaticMatchingService automaticMatchingService;

@PostMapping
public ResponseEntity<RoomResponse> create(@LoginMember AuthInfo authInfo, @RequestBody RoomCreateRequest request) {
RoomResponse response = roomService.create(authInfo.getId(), request);

automaticMatchingService.matchOnRecruitmentDeadline(response);
automaticUpdateService.updateAtReviewDeadline(response);

return ResponseEntity.created(URI.create(String.format("/rooms/%d", response.id())))
.body(response);
}

@PutMapping
public ResponseEntity<RoomResponse> update(@LoginMember AuthInfo authInfo, @RequestBody RoomUpdateRequest request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명세서 있는 건가요?

RoomResponse response = roomService.update(authInfo.getId(), request);

return ResponseEntity.ok()
.body(response);
}

@GetMapping("/{id}")
public ResponseEntity<RoomResponse> room(@PathVariable long id, @AccessedMember AuthInfo authInfo) {
RoomResponse response = roomService.findOne(id, authInfo.getId());
Expand All @@ -58,9 +56,6 @@ public ResponseEntity<RoomResponses> participatedRooms(@LoginMember AuthInfo aut
public ResponseEntity<Void> delete(@PathVariable long id, @LoginMember AuthInfo authInfo) {
roomService.delete(id, authInfo.getId());

automaticMatchingService.cancel(id);
automaticUpdateService.cancel(id);

return ResponseEntity.noContent()
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import corea.exception.ExceptionType;
import corea.global.annotation.ApiErrorResponses;
import corea.matchresult.dto.MatchResultResponses;
import corea.room.dto.RoomCreateRequest;
import corea.room.dto.RoomParticipantResponses;
import corea.room.dto.RoomResponse;
import corea.room.dto.RoomResponses;
import corea.room.dto.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -26,6 +23,16 @@ public interface RoomControllerSpecification {
@ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND)
ResponseEntity<RoomResponse> create(AuthInfo authInfo, RoomCreateRequest request);

@Operation(summary = "새로운 방을 수정합니다.",
description = "상호 리뷰 인원을 모을 수 있는 방을 수정합니다. <br>" +
"요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " +
"이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다. <br>" +
"JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " +
"<br><br>**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " +
"토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.")
@ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND)
ResponseEntity<RoomResponse> update(AuthInfo authInfo, RoomUpdateRequest request);

@Operation(summary = "방 상세 정보를 반환합니다.",
description = "상세 페이지에 디스플레이 되는 방 상세 정보를 반환합니다. <br>" +
"요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " +
Expand Down
76 changes: 76 additions & 0 deletions backend/src/main/java/corea/room/dto/RoomUpdateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package corea.room.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import corea.member.domain.Member;
import corea.room.domain.Room;
import corea.room.domain.RoomClassification;
import corea.room.domain.RoomStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;
import java.util.List;

@Schema(description = "방 수정 요청")
public record RoomUpdateRequest(
@Schema(description = "방 ID", example = "99")
@NotBlank
long roomId,

@Schema(description = "방 제목", example = "MVC를 아시나요?")
@NotBlank
String title,

@Schema(description = "방 내용", example = "MVC 패턴을 아시나요?")
String content,

@Schema(description = "repository 링크", example = "https://github.com/example/java-racingcar")
@NotBlank
String repositoryLink,

@Schema(description = "썸네일 링크", example = "https://gongu.copyright.or.kr/gongu/wrt/cmmn/wrtFileImageView.do?wrtSn=13301655&filePath=L2Rpc2sxL25ld2RhdGEvMjAyMS8yMS9DTFMxMDAwNC8xMzMwMTY1NV9XUlRfMjFfQ0xTMTAwMDRfMjAyMTEyMTNfMQ==&thumbAt=Y&thumbSe=b_tbumb&wrtTy=10004")
String thumbnailLink,

@Schema(description = "상호 리뷰 인원", example = "2")
@NotNull
int matchingSize,

@Schema(description = "중심으로 리뷰하면 좋은 키워드", example = "[\"TDD\", \"클린코드\"]")
List<String> keywords,

@Schema(description = "제한 참여 인원", example = "200")
@NotNull
int limitedParticipants,

@Schema(description = "모집 마감일", example = "2024-07-30 15:00")
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime recruitmentDeadline,

@Schema(description = "리뷰 마감일", example = "2024-08-10 23:59")
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
LocalDateTime reviewDeadline,

@Schema(description = "방이 속하는 분야", example = "BE")
@NotNull
RoomClassification classification
) {

private static final int INITIAL_PARTICIPANTS_SIZE = 1;
private static final RoomStatus INITIAL_ROOM_STATUS = RoomStatus.OPEN;

public Room toEntity(Member manager) {
return new Room(
roomId,
title, content,
matchingSize, repositoryLink,
thumbnailLink, keywords,
INITIAL_PARTICIPANTS_SIZE, limitedParticipants,
manager, recruitmentDeadline,
reviewDeadline, classification,
INITIAL_ROOM_STATUS
);
}
}
84 changes: 84 additions & 0 deletions backend/src/main/java/corea/room/service/RoomAutomaticService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package corea.room.service;

import corea.room.domain.Room;
import corea.scheduler.domain.*;
import corea.scheduler.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

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

private final AutomaticUpdateWriter automaticUpdateWriter;
private final AutomaticUpdateReader automaticUpdateReader;

private final AutomaticMatchingWriter automaticMatchingWriter;
private final AutomaticMatchingReader automaticMatchingReader;

private final AutomaticMatchingScheduler automaticMatchingScheduler;
private final AutomaticUpdateScheduler automaticUpdateScheduler;

@Transactional
public void updateTime(Room updateRoom) {
AutomaticMatching automaticMatching = automaticMatchingReader.findWithRoom(updateRoom);
AutomaticUpdate automaticUpdate = automaticUpdateReader.findWithRoom(updateRoom);

automaticMatchingWriter.updateTime(automaticMatching, updateRoom.getRecruitmentDeadline());
automaticUpdateWriter.updateTime(automaticUpdate, updateRoom.getReviewDeadline());

automaticMatchingScheduler.modifyTask(updateRoom);
automaticUpdateScheduler.modifyTask(updateRoom);
}

@Transactional
public void createAutomatic(Room room) {
automaticMatchingWriter.create(room);
automaticUpdateWriter.create(room);

automaticMatchingScheduler.matchOnRecruitmentDeadline(room);
automaticUpdateScheduler.updateAtReviewDeadline(room);
}

@Transactional
public void deleteAutomatic(Room room) {
AutomaticMatching automaticMatching = automaticMatchingReader.findWithRoom(room);
AutomaticUpdate automaticUpdate = automaticUpdateReader.findWithRoom(room);

automaticMatchingWriter.delete(automaticMatching);
automaticUpdateWriter.delete(automaticUpdate);

automaticMatchingScheduler.cancel(room.getId());
automaticUpdateScheduler.cancel(room.getId());
}

@EventListener(ApplicationReadyEvent.class)
public void schedulePendingAutomaticMatching() {
List<AutomaticMatching> matchings = automaticMatchingReader.findAllByStatus(ScheduleStatus.PENDING);

log.info("{}개의 방에 대해 자동 매칭 재예약 시작", matchings.size());

matchings.forEach(automaticMatchingScheduler::matchOnRecruitmentDeadline);

log.info("{}개의 방에 대해 자동 매칭 재예약 완료", matchings.size());
}

@EventListener(ApplicationReadyEvent.class)
public void schedulePendingAutomaticUpdate() {
List<AutomaticUpdate> updates = automaticUpdateReader.findAllByStatus(ScheduleStatus.PENDING);

log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 시작", updates.size());

updates.forEach(automaticUpdateScheduler::updateAtReviewDeadline);

log.info("{}개의 방에 대해 자동 상태 업데이트 재예약 완료", updates.size());
}
}
35 changes: 21 additions & 14 deletions backend/src/main/java/corea/room/service/RoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,10 @@
import corea.participation.domain.ParticipationStatus;
import corea.participation.repository.ParticipationRepository;
import corea.room.domain.Room;
import corea.room.domain.RoomClassification;
import corea.room.domain.RoomStatus;
import corea.room.dto.*;
import corea.room.repository.RoomRepository;
import corea.scheduler.domain.AutomaticMatching;
import corea.scheduler.domain.AutomaticUpdate;
import corea.scheduler.repository.AutomaticMatchingRepository;
import corea.scheduler.repository.AutomaticUpdateRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -46,8 +38,8 @@ public class RoomService {
private final MatchResultRepository matchResultRepository;
private final ParticipationRepository participationRepository;
private final FailedMatchingRepository failedMatchingRepository;
private final AutomaticMatchingRepository automaticMatchingRepository;
private final AutomaticUpdateRepository automaticUpdateRepository;
private final RoomAutomaticService roomAutomaticService;


@Transactional
public RoomResponse create(long memberId, RoomCreateRequest request) {
Expand All @@ -59,12 +51,28 @@ public RoomResponse create(long memberId, RoomCreateRequest request) {
Participation participation = new Participation(room, manager);

participationRepository.save(participation);
automaticMatchingRepository.save(new AutomaticMatching(room.getId(), request.recruitmentDeadline()));
automaticUpdateRepository.save(new AutomaticUpdate(room.getId(), request.reviewDeadline()));
roomAutomaticService.createAutomatic(room);

return RoomResponse.of(room, participation.getMemberRole(), ParticipationStatus.MANAGER);
}

@Transactional
public RoomResponse update(long memberId, RoomUpdateRequest request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5

RoomUpdateResponse를 분리하는 건 어떠신가요?

RoomUpdateRequest가 있기도 하고,
update 메서드에서 MemberRoleParticipationStatus까지 반환해야 하는 이유가 없는 것 같아서요. 👀

방을 업데이트하는 로직인데 참여 여부를 확인하고 있는 것도 살짝 어색하게 느껴지고요.
(이미 방장임이 확인됨)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

방을 업데이트하는 로직인데 참여 여부를 확인하고 있는 것도 살짝 어색하게 느껴지고요.
(이미 방장임이 확인됨)

�우선, 저는 방장이 무조건 리뷰어로 참여만 하는거 자체를 경계하고 있습니다.
방장이 MANAGER 인건 명확하나, REVEIWER 인건 명확한지 궁금합니다.
( 지금 코드적 으로 명확히 분리가 안되서 더 그런거 같기도 해용 )

RoomUpdateResponse를 분리하는 건 어떠신 가요?

위의 관점도 그렇고, 명확하게 RoomResponse 도 프론트와 얘기해서 좀 더 분리가 잘 되는게 아니라면
당장은 불필요한 느낌입니당.

Room room = getRoom(request.roomId());
if (room.isNotMatchingManager(memberId)) {
throw new CoreaException(ExceptionType.MEMBER_IS_NOT_MANAGER);
}
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND));

Room updatedRoom = roomRepository.save(request.toEntity(member));
Participation participation = participationRepository.findByRoomIdAndMemberId(updatedRoom.getId(), memberId)
.orElseThrow(() -> new CoreaException(ExceptionType.NOT_ALREADY_APPLY));

roomAutomaticService.updateTime(updatedRoom);
return RoomResponse.of(updatedRoom, participation.getMemberRole(), ParticipationStatus.MANAGER);
}

private void validateDeadLine(LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline) {
LocalDateTime currentDateTime = LocalDateTime.now();

Expand Down Expand Up @@ -114,8 +122,7 @@ public void delete(long roomId, long memberId) {

roomRepository.delete(room);
participationRepository.deleteAllByRoomId(roomId);
automaticMatchingRepository.deleteByRoomId(roomId);
automaticUpdateRepository.deleteByRoomId(roomId);
roomAutomaticService.deleteAutomatic(room);
}

private void validateDeletionAuthority(Room room, long memberId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package corea.scheduler.domain;

import corea.exception.CoreaException;
import corea.exception.ExceptionType;
import corea.room.domain.Room;
import corea.scheduler.repository.AutomaticMatchingRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AutomaticMatchingReader {

private final AutomaticMatchingRepository automaticMatchingRepository;

public AutomaticMatching findWithRoom(Room room) {
return automaticMatchingRepository.findByRoomId(room.getId())
.orElseThrow(() -> new CoreaException(ExceptionType.AUTOMATIC_MATCHING_NOT_FOUND));
}

public List<AutomaticMatching> findAllByStatus(ScheduleStatus status) {
return automaticMatchingRepository.findAllByStatus(status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package corea.scheduler.domain;

import corea.room.domain.Room;
import corea.scheduler.repository.AutomaticMatchingRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Component
@RequiredArgsConstructor
@Transactional
public class AutomaticMatchingWriter {

private final AutomaticMatchingRepository automaticMatchingRepository;

public AutomaticMatching updateTime(AutomaticMatching automaticMatching, LocalDateTime matchingStartTime) {
AutomaticMatching updateEntity = new AutomaticMatching(
automaticMatching.getId(),
automaticMatching.getRoomId(),
matchingStartTime,
automaticMatching.getStatus()
);
return automaticMatchingRepository.save(updateEntity);
}

public AutomaticMatching create(Room room) {
AutomaticMatching entity = new AutomaticMatching(room.getId(),room.getRecruitmentDeadline());
return automaticMatchingRepository.save(entity);
}

public void delete(AutomaticMatching automaticMatching) {
automaticMatchingRepository.delete(automaticMatching);
}
}
Loading
Loading