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

[Feat/#59] 회의실 나가기 기능 및 안건 수정 기능 개발, 기획 변경으로 인한 코드 수정 #63

Merged
merged 11 commits into from
Feb 21, 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
77 changes: 58 additions & 19 deletions src/main/java/org/dnd/timeet/agenda/application/AgendaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.dnd.timeet.agenda.dto.AgendaActionResponse;
import org.dnd.timeet.agenda.dto.AgendaCreateRequest;
import org.dnd.timeet.agenda.dto.AgendaInfoResponse;
import org.dnd.timeet.agenda.dto.AgendaPatchRequest;
import org.dnd.timeet.agenda.dto.AgendaPatchResponse;
import org.dnd.timeet.common.exception.BadRequestError;
import org.dnd.timeet.common.exception.NotFoundError;
import org.dnd.timeet.common.exception.NotFoundError.ErrorCode;
Expand All @@ -39,9 +41,11 @@ public Long createAgenda(Long meetingId, AgendaCreateRequest createDto, Member m
Collections.singletonMap("MeetingId", "Meeting not found")));

// 회의에 참가한 멤버인지 확인
participantRepository.findByMeetingIdAndMemberId(meetingId, member.getId())
.orElseThrow(() -> new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("MemberId", "Member is not a participant of the meeting")));
boolean isParticipantExists = participantRepository.existsByMeetingIdAndMemberId(meetingId, member.getId());
if (!isParticipantExists) {
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("MemberId", "Member is not a participant of the meeting"));
}

Agenda agenda = createDto.toEntity(meeting);
agenda = agendaRepository.save(agenda);
Expand All @@ -59,6 +63,10 @@ public List<Agenda> findAll(Long meetingId) {
}

public AgendaActionResponse changeAgendaStatus(Long meetingId, Long agendaId, AgendaActionRequest actionRequest) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));

Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("AgendaId", "Agenda not found")));
Expand All @@ -73,31 +81,40 @@ public AgendaActionResponse changeAgendaStatus(Long meetingId, Long agendaId, Ag
Collections.singletonMap("Action", "Invalid action"));
}

// 안건 시작 요청 전 첫 번째 안건이 시작되었는지 확인
if (action == AgendaAction.START && agenda.getOrderNum() != 1) {
// 첫 번째 안건의 시작 여부 확인
boolean isFirstAgendaStarted = agendaRepository.existsByMeetingIdAndOrderNumAndStatus(
meetingId, 1, AgendaStatus.COMPLETED);

if (!isFirstAgendaStarted) {
throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION,
Collections.singletonMap("AgendaOrder", "First agenda has not been started yet"));
}
}

switch (action) {
case START:
case START -> {
// 첫번째 안건의 시작 시간을 회의 시작 시간으로 설정
meeting.updateStartTimeOnFirstAgendaStart(agenda);
agenda.start();
break;
case PAUSE:
agenda.pause();
break;
case RESUME:
agenda.resume();
break;
case END:
agenda.complete();
break;
case MODIFY:
}
case PAUSE -> agenda.pause();
case RESUME -> agenda.resume();
case END -> agenda.complete();
case MODIFY -> {
LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration());
Duration duration = DurationUtils.convertLocalTimeToDuration(modifiedDuration);
agenda.extendDuration(duration);
// 회의 시간 추가
addMeetingTotalActualDuration(meetingId, duration);
break;
default:
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("Action", "Invalid action"));
}
default -> throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("Action", "Invalid action"));
}
// 변경 사항 저장
Agenda savedAgenda = agendaRepository.save(agenda);
meetingRepository.save(meeting);

Duration currentDuration = savedAgenda.calculateCurrentDuration();
Duration remainingDuration = agenda.calculateRemainingTime();
Expand Down Expand Up @@ -146,4 +163,26 @@ public AgendaInfoResponse findAgendas(Long meetingId) {

return new AgendaInfoResponse(meeting, agendaList);
}

public AgendaPatchResponse patchAgenda(Long meetingId, Long agendaId, AgendaPatchRequest patchRequest) {
// 회의 존재 여부만 확인
boolean meetingExists = meetingRepository.existsById(meetingId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

existsById 활용하여 컬럼 존재 여부 확인 좋습니다. 👍

if (!meetingExists) {
throw new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found"));
}

Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("AgendaId", "Agenda not found")));
if (agenda.getStatus() != AgendaStatus.PENDING) {
throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION,
Collections.singletonMap("AgendaStatus", "Agenda is not PENDING status"));
}

agenda.update(patchRequest.getTitle(),
DurationUtils.convertLocalTimeToDuration(patchRequest.getAllocatedDuration()));

return new AgendaPatchResponse(agenda);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.dnd.timeet.agenda.dto.AgendaActionResponse;
import org.dnd.timeet.agenda.dto.AgendaCreateRequest;
import org.dnd.timeet.agenda.dto.AgendaInfoResponse;
import org.dnd.timeet.agenda.dto.AgendaPatchRequest;
import org.dnd.timeet.agenda.dto.AgendaPatchResponse;
import org.dnd.timeet.common.security.CustomUserDetails;
import org.dnd.timeet.common.utils.ApiUtils;
import org.dnd.timeet.common.utils.ApiUtils.ApiResult;
Expand All @@ -19,6 +21,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -81,4 +84,15 @@ public ResponseEntity deleteAgenda(

return ResponseEntity.noContent().build();
}

@PatchMapping("/{meeting-id}/agendas/{agenda-id}")
@Operation(summary = "안건 수정", description = "지정된 ID에 해당하는 안건을 수정한다.")
public ResponseEntity<ApiResult<AgendaPatchResponse>> deleteAgenda(
@PathVariable("meeting-id") Long meetingId,
@PathVariable("agenda-id") Long agendaId,
@RequestBody AgendaPatchRequest patchRequest) {
AgendaPatchResponse response = agendaService.patchAgenda(meetingId, agendaId, patchRequest);

return ResponseEntity.ok(ApiUtils.success(response));
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/dnd/timeet/agenda/domain/Agenda.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,10 @@ public void cancel() {
this.delete();
}

public void update(String title, Duration allocatedDuration) {
this.title = title;
this.allocatedDuration = allocatedDuration;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ public interface AgendaRepository extends JpaRepository<Agenda, Long> {
List<Agenda> findByMeetingId(Long meetingId);

Optional<Agenda> findByIdAndMeetingId(Long agendaId, Long meetingId);

boolean existsByMeetingIdAndOrderNum(Long meetingId, Integer orderNum);

boolean existsByMeetingIdAndOrderNumAndStatus(Long meetingId, Integer orderNum, AgendaStatus status);

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class AgendaActionResponse {
private String currentDuration; // 현재까지 진행된 시간
private String remainingDuration; // 남은 시간

private Integer orderNum;
private String timestamp; // 수정 시간

public AgendaActionResponse(Agenda agenda, Duration currentDuration, Duration remainingDuration) {
Expand All @@ -36,7 +35,6 @@ public AgendaActionResponse(Agenda agenda, Duration currentDuration, Duration re
this.currentDuration = DurationUtils.formatDuration(currentDuration);
this.remainingDuration = DurationUtils.formatDuration(remainingDuration);

this.orderNum = agenda.getOrderNum();
this.timestamp = DateTimeUtils.formatLocalDateTime(LocalDateTime.now());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,12 @@ public class AgendaCreateRequest {
@Schema(description = "안건 소요 시간", example = "01:20:00")
private LocalTime allocatedDuration;

@NotNull(message = "안건 순서는 반드시 입력되어야 합니다")
@Schema(description = "안건 순서", example = "1")
private Integer orderNum;

public Agenda toEntity(Meeting meeting) {
return Agenda.builder()
.meeting(meeting)
.title(this.title)
.type(this.type.equals("AGENDA") ? AgendaType.AGENDA : AgendaType.BREAK)
.allocatedDuration(DurationUtils.convertLocalTimeToDuration(this.allocatedDuration))
.orderNum(this.orderNum)
.build();
}
}
22 changes: 22 additions & 0 deletions src/main/java/org/dnd/timeet/agenda/dto/AgendaPatchRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.dnd.timeet.agenda.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.format.annotation.DateTimeFormat;

@Schema(description = "안건 수정 요청")
@Getter
@Setter
@NoArgsConstructor
public class AgendaPatchRequest {

@Schema(description = "안건 제목", example = "브레인스토밍")
private String title;

@DateTimeFormat(pattern = "HH:mm:ss")
@Schema(description = "안건 소요 시간", example = "01:20:00")
private LocalTime allocatedDuration;
}
31 changes: 31 additions & 0 deletions src/main/java/org/dnd/timeet/agenda/dto/AgendaPatchResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.dnd.timeet.agenda.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.dnd.timeet.agenda.domain.Agenda;
import org.dnd.timeet.common.utils.DurationUtils;

@Schema(description = "안건 수정 응답")
@Getter
@Setter
public class AgendaPatchResponse {

@Schema(description = "안건 id", example = "12")
private Long agendaId;

@Schema(description = "안건 제목", example = "안건 1")
private String title;

@NotNull(message = "안건 소요 시간은 반드시 입력되어야 합니다")
@Schema(description = "안건 소요 시간", example = "01:20:00")
private String allocatedDuration;

public AgendaPatchResponse(Agenda agenda) {
this.agendaId = agenda.getId();
this.title = agenda.getTitle();
this.allocatedDuration = DurationUtils.formatDuration(agenda.getAllocatedDuration());
}

}
4 changes: 2 additions & 2 deletions src/main/java/org/dnd/timeet/common/utils/DurationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public class DurationUtils {
* @return Duration 객체
*/
public static Duration convertLocalTimeToDuration(LocalTime time) {
int totalMinutes = time.getHour() * 60 + time.getMinute();
return Duration.ofMinutes(totalMinutes);
int totalSeconds = time.toSecondOfDay(); // 시, 분, 초를 모두 초 단위로 변환
return Duration.ofSeconds(totalSeconds); // 변환된 초를 바탕으로 Duration 생성
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package org.dnd.timeet.meeting.application;

import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.common.exception.NotFoundError;
import org.dnd.timeet.meeting.domain.Meeting;
import org.dnd.timeet.meeting.domain.MeetingRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand All @@ -18,29 +13,18 @@ public class MeetingAsyncService {
private final MeetingRepository meetingRepository;
private final Logger logger = LoggerFactory.getLogger(MeetingAsyncService.class);

@Transactional
@Async // 비동기 작업 실행시 발생하는 에러 처리
public void startScheduledMeeting(Long meetingId) {
try {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));

meeting.startMeeting();
meetingRepository.save(meeting);
} catch (Exception e) {
logger.error("Error starting scheduled meeting", e);
}
}

@Transactional
@Async // 비동기 작업 실행시 발생하는 에러 처리
public void endScheduledMeeting(Meeting meeting) {
try {
meeting.endMeeting();
meetingRepository.save(meeting);
} catch (Exception e) {
logger.error("Error starting scheduled meeting", e);
}
}
// @Transactional
// @Async // 비동기 작업 실행시 발생하는 에러 처리
// public void startScheduledMeeting(Long meetingId) {
// try {
// Meeting meeting = meetingRepository.findById(meetingId)
// .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
// Collections.singletonMap("MeetingId", "Meeting not found")));
//
// meeting.startMeeting();
// meetingRepository.save(meeting);
// } catch (Exception e) {
// logger.error("Error starting scheduled meeting", e);
// }
// }
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
package org.dnd.timeet.meeting.application;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.common.exception.BadRequestError;
import org.dnd.timeet.meeting.domain.Meeting;
import org.dnd.timeet.meeting.domain.MeetingRepository;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@EnableScheduling
Expand All @@ -24,31 +15,16 @@ public class MeetingScheduler {
private final ScheduledExecutorService scheduledExecutorService;
private final MeetingRepository meetingRepository;

public void scheduleMeetingStart(Long meetingId, LocalDateTime startTime) {
long delay = ChronoUnit.MILLIS.between(LocalDateTime.now(), startTime);
if (delay < 0) {
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("startTime", "startTime is past"));
}

// 스케줄러 생성
scheduledExecutorService.schedule(() ->
meetingAsyncService.startScheduledMeeting(meetingId), delay, TimeUnit.MILLISECONDS);
}

@Scheduled(fixedRate = 60000) // 60000ms = 1분
public void scheduleMeetingEnd() {
List<Meeting> meetingsByStatusInProgress = meetingRepository.findMeetingsByStatusInProgress();

LocalDateTime now = LocalDateTime.now();

meetingsByStatusInProgress.forEach(meeting -> {
// 남은 시간이 0이거나 음수인 경우 회의를 종료
Duration remainingTime = meeting.calculateRemainingTime();
if (remainingTime.isZero() || remainingTime.isNegative()) {
meetingAsyncService.endScheduledMeeting(meeting);
}
});
}
// public void scheduleMeetingStart(Long meetingId, LocalDateTime startTime) {
// long delay = ChronoUnit.MILLIS.between(LocalDateTime.now(), startTime);
// if (delay < 0) {
// throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
// Collections.singletonMap("startTime", "startTime is past"));
// }
//
// // 스케줄러 생성
// scheduledExecutorService.schedule(() ->
// meetingAsyncService.startScheduledMeeting(meetingId), delay, TimeUnit.MILLISECONDS);
// }

}
Loading
Loading