Skip to content

Commit

Permalink
Merge pull request #57 from dnd-side-project/dev
Browse files Browse the repository at this point in the history
dev -> main
  • Loading branch information
Starlight258 authored Feb 19, 2024
2 parents d73b071 + 1261af1 commit a493db8
Show file tree
Hide file tree
Showing 17 changed files with 205 additions and 67 deletions.
File renamed without changes.
51 changes: 47 additions & 4 deletions src/main/java/org/dnd/timeet/agenda/application/AgendaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import org.dnd.timeet.agenda.domain.Agenda;
import org.dnd.timeet.agenda.domain.AgendaAction;
import org.dnd.timeet.agenda.domain.AgendaRepository;
import org.dnd.timeet.agenda.domain.AgendaStatus;
import org.dnd.timeet.agenda.dto.AgendaActionRequest;
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.common.exception.BadRequestError;
import org.dnd.timeet.common.exception.NotFoundError;
import org.dnd.timeet.common.exception.NotFoundError.ErrorCode;
Expand All @@ -31,7 +33,7 @@ public class AgendaService {
private final AgendaRepository agendaRepository;
private final ParticipantRepository participantRepository;

public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto, Member member) {
public Long createAgenda(Long meetingId, AgendaCreateRequest createDto, Member member) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));
Expand All @@ -42,8 +44,13 @@ public Agenda createAgenda(Long meetingId, AgendaCreateRequest createDto, Member
Collections.singletonMap("MemberId", "Member is not a participant of the meeting")));

Agenda agenda = createDto.toEntity(meeting);
agenda = agendaRepository.save(agenda);

return agendaRepository.save(agenda);
// 회의 시간 추가
addMeetingTotalActualDuration(meetingId,
DurationUtils.convertLocalTimeToDuration(createDto.getAllocatedDuration()));

return agenda.getId();
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -81,7 +88,10 @@ public AgendaActionResponse changeAgendaStatus(Long meetingId, Long agendaId, Ag
break;
case MODIFY:
LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration());
agenda.extendDuration(DurationUtils.convertLocalTimeToDuration(modifiedDuration));
Duration duration = DurationUtils.convertLocalTimeToDuration(modifiedDuration);
agenda.extendDuration(duration);
// 회의 시간 추가
addMeetingTotalActualDuration(meetingId, duration);
break;
default:
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Expand All @@ -99,8 +109,41 @@ public void cancelAgenda(Long meetingId, Long agendaId) {
Agenda agenda = agendaRepository.findByIdAndMeetingId(agendaId, meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("AgendaId", "Agenda not found")));
agenda.delete();
if (agenda.getStatus() != AgendaStatus.PENDING) {
throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION,
Collections.singletonMap("AgendaStatus", "Agenda is not PENDING status"));
}
agenda.cancel();

subtractMeetingTotalActualDuration(meetingId, agenda.getAllocatedDuration());
}

public void addMeetingTotalActualDuration(Long meetingId, Duration additionalDuration) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));

Duration newTotalDuration = meeting.getTotalActualDuration().plus(additionalDuration);
meeting.updateTotalActualDuration(newTotalDuration);
meetingRepository.save(meeting);
}

public void subtractMeetingTotalActualDuration(Long meetingId, Duration subtractedDuration) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));

Duration newTotalDuration = meeting.getTotalActualDuration().minus(subtractedDuration);
meeting.updateTotalActualDuration(newTotalDuration);
meetingRepository.save(meeting);
}

public AgendaInfoResponse findAgendas(Long meetingId) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));
List<Agenda> agendaList = agendaRepository.findByMeetingId(meetingId);

return new AgendaInfoResponse(meeting, agendaList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.agenda.application.AgendaService;
import org.dnd.timeet.agenda.domain.Agenda;
import org.dnd.timeet.agenda.dto.AgendaActionRequest;
import org.dnd.timeet.agenda.dto.AgendaActionResponse;
import org.dnd.timeet.agenda.dto.AgendaCreateRequest;
Expand Down Expand Up @@ -42,21 +39,18 @@ public ResponseEntity<ApiResult<Long>> createMeeting(
@PathVariable("meeting-id") Long meetingId,
@RequestBody @Valid AgendaCreateRequest agendaCreateRequest,
@AuthenticationPrincipal CustomUserDetails userDetails) {
Agenda savedAgenda = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember());
Long agendaId = agendaService.createAgenda(meetingId, agendaCreateRequest, userDetails.getMember());

return ResponseEntity.ok(ApiUtils.success(savedAgenda.getId()));
return ResponseEntity.ok(ApiUtils.success(agendaId));
}

@GetMapping("/{meeting-id}/agendas")
@Operation(summary = "모든 안건 조회", description = "모든 안건을 조회한다.")
public ResponseEntity getAgendas(
public ResponseEntity<ApiResult<AgendaInfoResponse>> getAgendas(
@PathVariable("meeting-id") Long meetingId) {
List<AgendaInfoResponse> agendaInfoResponseList = agendaService.findAll(meetingId)
.stream()
.map(a -> new AgendaInfoResponse(a, a.calculateCurrentDuration(), a.calculateRemainingTime()))
.collect(Collectors.toList());
AgendaInfoResponse response = agendaService.findAgendas(meetingId);

return ResponseEntity.ok(ApiUtils.success(agendaInfoResponseList));
return ResponseEntity.ok(ApiUtils.success(response));
}

/*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/dnd/timeet/agenda/domain/Agenda.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private void validateTransition(AgendaStatus... validPreviousStatuses) {
Collections.singletonMap("AgendaStatus", "Invalid status transition"));
}

public void cancelAgenda() {
public void cancel() {
this.status = AgendaStatus.CANCELED;
this.delete();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public class AgendaCreateRequest {
private String type;

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

@NotNull(message = "안건 순서는 반드시 입력되어야 합니다")
Expand Down
63 changes: 44 additions & 19 deletions src/main/java/org/dnd/timeet/agenda/dto/AgendaInfoResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,65 @@

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Duration;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.dnd.timeet.agenda.domain.Agenda;
import org.dnd.timeet.common.utils.DurationUtils;
import org.dnd.timeet.meeting.domain.Meeting;

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

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

@Schema(description = "안건 제목", example = "안건 1")
private String title;
@Schema(description = "회의 남은 시간", example = "00:03:00")
private String remainingTime;

@Schema(description = "안건 종류", example = "AGENDA")
private String type;
private List<AgendaResponse> agendaResponse;

@Schema(description = "현재까지 소요된 시간", example = "00:36")
private String currentDuration;
public AgendaInfoResponse(Meeting meeting, List<Agenda> agendaList) {
this.meetingId = meeting.getId();
this.remainingTime = DurationUtils.formatDuration(meeting.calculateRemainingTime());
this.agendaResponse = agendaList.stream()
.map(agenda -> new AgendaResponse(agenda, agenda.calculateCurrentDuration(),
agenda.calculateRemainingTime()))
.toList();
}

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

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

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

@Schema(description = "안건 종류", example = "AGENDA")
private String type;

@Schema(description = "현재까지 소요된 시간", example = "00:36:00")
private String currentDuration;

@Schema(description = "남은 시간", example = "00:24")
private String remainingDuration;
@Schema(description = "남은 시간", example = "00:24:00")
private String remainingDuration;

@Schema(description = "안건 상태", example = "INPROGRESS")
private String status;
@Schema(description = "안건 상태", example = "INPROGRESS")
private String status;

public AgendaInfoResponse(Agenda agenda, Duration currentDuration, Duration remainingDuration) {
this.agendaId = agenda.getId();
this.title = agenda.getTitle();
this.type = agenda.getType().name();
this.currentDuration = DurationUtils.formatDuration(currentDuration);
this.remainingDuration = DurationUtils.formatDuration(remainingDuration);
this.status = agenda.getStatus().name();
public AgendaResponse(Agenda agenda, Duration currentDuration, Duration remainingDuration) {
this.agendaId = agenda.getId();
this.title = agenda.getTitle();
this.type = agenda.getType().name();
this.currentDuration = DurationUtils.formatDuration(currentDuration);
this.remainingDuration = DurationUtils.formatDuration(remainingDuration);
this.status = agenda.getStatus().name();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package org.dnd.timeet.common.exception;


import java.util.Map;
import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.util.Map;

/**
* HTTP 상태 코드 403 (Forbidden) : 금지됨 인증은 되었지만, 리소스에 접근할 권한이 없을때 발생합니다.
* HTTP 상태 코드 403 (Forbidden) : 금지됨. 인증은 되었지만, 리소스에 접근할 권한이 없을때 발생합니다.
*/
@Getter
public class ForbiddenError extends ApiException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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;
Expand All @@ -11,15 +12,12 @@
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MeetingAsyncService {

private final MeetingRepository meetingRepository;
private final Logger logger = LoggerFactory.getLogger(MeetingAsyncService.class);

public MeetingAsyncService(MeetingRepository meetingRepository) {
this.meetingRepository = meetingRepository;
}

@Transactional
@Async // 비동기 작업 실행시 발생하는 에러 처리
public void startScheduledMeeting(Long meetingId) {
Expand All @@ -34,4 +32,15 @@ public void startScheduledMeeting(Long meetingId) {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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 @@ -17,6 +22,7 @@ public class MeetingScheduler {

private final MeetingAsyncService meetingAsyncService;
private final ScheduledExecutorService scheduledExecutorService;
private final MeetingRepository meetingRepository;

public void scheduleMeetingStart(Long meetingId, LocalDateTime startTime) {
long delay = ChronoUnit.MILLIS.between(LocalDateTime.now(), startTime);
Expand All @@ -29,4 +35,20 @@ public void scheduleMeetingStart(Long meetingId, LocalDateTime startTime) {
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);
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.agenda.domain.AgendaRepository;
import org.dnd.timeet.agenda.domain.AgendaStatus;
import org.dnd.timeet.agenda.domain.AgendaType;
import org.dnd.timeet.agenda.dto.AgendaReportInfoResponse;
import org.dnd.timeet.common.exception.BadRequestError;
import org.dnd.timeet.common.exception.ForbiddenError;
import org.dnd.timeet.common.exception.NotFoundError;
import org.dnd.timeet.common.exception.NotFoundError.ErrorCode;
import org.dnd.timeet.meeting.domain.Meeting;
Expand Down Expand Up @@ -49,12 +51,27 @@ public Meeting createMeeting(MeetingCreateRequest createDto, Member member) {
return meeting;
}

public void endMeeting(Long meetingId) {
public void endMeeting(Long meetingId, Long memberId) {
Meeting meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));
// 회의의 방장인지 확인
if (!Objects.equals(meeting.getHostMember().getId(), memberId)) {
throw new ForbiddenError(ForbiddenError.ErrorCode.ROLE_BASED_ACCESS_ERROR,
Collections.singletonMap("MemberId", "Member is not the host of the meeting"));
}

meeting.endMeeting();
// 회의에서 진행중인 안건들도 모두 종료되도록 하기
agendaRepository.findByMeetingId(meetingId)
.forEach(agenda -> {
if (agenda.getStatus().equals(AgendaStatus.INPROGRESS) ||
agenda.getStatus().equals(AgendaStatus.PAUSED)) {
agenda.complete();
} else if (agenda.getStatus().equals(AgendaStatus.PENDING)) {
agenda.cancel();
}
});
}

public Meeting addParticipantToMeeting(Long meetingId, Member member) {
Expand Down
Loading

0 comments on commit a493db8

Please sign in to comment.