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

dev -> main #68

Merged
merged 30 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8e81df5
[#-] feat: Update docker compose for db production
FacerAin Feb 20, 2024
bcafe14
[#-] feat: Update docker compose for db production
FacerAin Feb 20, 2024
ec1a0d1
[#-] feat: Update docker compose for db production
FacerAin Feb 20, 2024
c5bc1b2
[#59] feat: Implement leaving meeting feature
Starlight258 Feb 20, 2024
ae8b707
[#59] refactor: Refactor member retrieving feature to allow even with…
Starlight258 Feb 20, 2024
9827e35
[#59] refactor: Refactor meeting retrieving feature to allow even wit…
Starlight258 Feb 20, 2024
f7f9838
[#60] feat: Implement patching agenda feature
Starlight258 Feb 20, 2024
cc7148a
[#-] refactor: Enhance convertLocalTimeToDuration to include seconds …
Starlight258 Feb 20, 2024
91b584a
Merge pull request #61 from dnd-side-project/feat/deploy-docker-compose
FacerAin Feb 20, 2024
1f5d808
[#62] feat: Add google oauth2 Login
FacerAin Feb 20, 2024
cea9e07
[#-] feat: Add host information to participant lookup API
Starlight258 Feb 20, 2024
4fa3ea9
[#-] remove: remove meeting auto-end scheduling due to plan changes
Starlight258 Feb 20, 2024
d710069
[#-] remove: Remove meeting start scheduling feature due to plan changes
Starlight258 Feb 20, 2024
fe854e8
[#65] feat: Add changeAgendaOrder
FacerAin Feb 20, 2024
0c6aa2c
[#-] feat: Implement starting automatically with the first agenda ini…
Starlight258 Feb 20, 2024
ec84858
Merge branch 'dev' into feat/meeting-agenda-updates
Starlight258 Feb 20, 2024
88dcac3
[#65] feat: Fix changeAgendaOrder for verify agendaIds size
FacerAin Feb 20, 2024
2a07f89
[#-] remove: Remove orderNum from request and response dto
Starlight258 Feb 20, 2024
ef7d32a
Merge pull request #63 from dnd-side-project/feat/meeting-agenda-updates
Starlight258 Feb 21, 2024
f6d9e56
Merge pull request #64 from dnd-side-project/feat/google-oauth-login
Starlight258 Feb 21, 2024
85ea8e8
Merge branch 'dev' into feat/agenda-order
Starlight258 Feb 21, 2024
b563cdc
Merge pull request #66 from dnd-side-project/feat/agenda-order
Starlight258 Feb 21, 2024
c20bfaf
[#-] feat: Update meeting remaining time tp current duration
Starlight258 Feb 21, 2024
50f5e06
[#-] feat: Added a boolean value to identify whether a participant is…
Starlight258 Feb 21, 2024
2a12dde
[#-] refactor: Standardize time format to include seconds in all requ…
Starlight258 Feb 21, 2024
fd62d71
[#-] feat: Generate random imageNum for members
Starlight258 Feb 21, 2024
0e8e825
[#-] feat: set initial values of imageNum for members to be between 1…
Starlight258 Feb 21, 2024
d9884c9
[#-] feat: Implement agenda shortcut feature
Starlight258 Feb 21, 2024
d8c9b1d
[#-] refactor: Change imageNum type from Long to Integer
Starlight258 Feb 21, 2024
a27d3f4
Merge pull request #67 from dnd-side-project/feat/meeting-enhancements
Starlight258 Feb 21, 2024
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
48 changes: 33 additions & 15 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
version: '3.7'
services:
db:
image: mysql:8-oracle
restart: always
environment:
MYSQL_DATABASE: '{MYSQL_DATABASE}'
MYSQL_USER: '${MYSQL_USER}'
MYSQL_PASSWORD: '${MYSQL_PASSWORD}'
MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASSWORD}'
database:
container_name: mysql_db
image: mysql:8
restart: unless-stopped
env_file:
- .env
ports:
- '3306:3306'
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d
command:
- "mysqld"
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
networks:
- backend_network

application:
container_name: backend
deploy:
restart_policy:
condition: on-failure
max_attempts: 5
delay: 3s

app:
image: syw5141/dnd-10th-2-backend:latest
ports:
- "8080:8080"
env_file:
- .env
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql_db:3306/${MYSQL_DATABASE}?useSSL=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
depends_on:
- db
- database
networks:
- backend_network

# λ³Όλ₯¨ μ •μ˜ (κΈ°λ³Έ μ„€μ • μ‚¬μš©)
volumes:
db_data:
networks:
backend_network:
118 changes: 99 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 @@ -3,6 +3,7 @@
import java.time.Duration;
import java.time.LocalTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.dnd.timeet.agenda.domain.Agenda;
Expand All @@ -13,6 +14,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 @@ -33,17 +36,22 @@ public class AgendaService {
private final AgendaRepository agendaRepository;
private final ParticipantRepository participantRepository;

@Transactional
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")));

// νšŒμ˜μ— μ°Έκ°€ν•œ 멀버인지 확인
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);
List<Agenda> agendaList = agendaRepository.findByMeetingId(meetingId);
agenda.setOrderNum(agendaList.size() + 1);
agenda = agendaRepository.save(agenda);

// 회의 μ‹œκ°„ μΆ”κ°€
Expand All @@ -59,6 +67,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 +85,47 @@ 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 EXTEND -> {
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"));
}
case REDUCE -> {
LocalTime modifiedDuration = LocalTime.parse(actionRequest.getModifiedDuration());
Duration duration = DurationUtils.convertLocalTimeToDuration(modifiedDuration);
agenda.reduceDuration(duration);
// 회의 μ‹œκ°„ κ°μ†Œ
subtractMeetingTotalActualDuration(meetingId, duration);
}
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 @@ -143,7 +171,59 @@ public AgendaInfoResponse findAgendas(Long meetingId) {
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("MeetingId", "Meeting not found")));
List<Agenda> agendaList = agendaRepository.findByMeetingId(meetingId);
agendaList.sort(Comparator.comparing(Agenda::getOrderNum));
return new AgendaInfoResponse(meeting, agendaList);
}

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

if (agendaList.size() != agendaIds.size()) {
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("AgendaIds", "Agenda Ids size is not matched"));
}

if (agendaList.size() != agendaIds.stream().distinct().count()) {
throw new BadRequestError(BadRequestError.ErrorCode.VALIDATION_FAILED,
Collections.singletonMap("AgendaIds", "Agenda Ids are not unique"));
}

for (int i = 0; i < agendaIds.size(); i++) {
Long agendaId = agendaIds.get(i);
Agenda agenda = agendaList.stream()
.filter(a -> a.getId().equals(agendaId))
.findFirst()
.orElseThrow(() -> new NotFoundError(ErrorCode.RESOURCE_NOT_FOUND,
Collections.singletonMap("AgendaId", "Agenda Id " + agendaId + " not found")));
agenda.setOrderNum(i + 1);
}
agendaList.sort(Comparator.comparing(Agenda::getOrderNum));

return new AgendaInfoResponse(meeting, agendaList);
}

public AgendaPatchResponse patchAgenda(Long meetingId, Long agendaId, AgendaPatchRequest patchRequest) {
// 회의 쑴재 μ—¬λΆ€λ§Œ 확인
boolean meetingExists = meetingRepository.existsById(meetingId);
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,9 @@
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.AgendaOrderRequest;
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 +22,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 +85,25 @@ public ResponseEntity deleteAgenda(

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

@PatchMapping("/{meeting-id}/agendas/order")
@Operation(summary = "μ•ˆκ±΄ μˆœμ„œ λ³€κ²½", description = "μ•ˆκ±΄μ˜ μˆœμ„œλ₯Ό λ³€κ²½ν•œλ‹€.")
public ResponseEntity<ApiResult<AgendaInfoResponse>> changeAgendaOrder(
@PathVariable("meeting-id") Long meetingId,
@RequestBody @Valid AgendaOrderRequest agendaOrderRequest) {
AgendaInfoResponse agendaInfoResponse = agendaService.changeAgendaOrder(meetingId,
agendaOrderRequest.getAgendaIds());
return ResponseEntity.ok(ApiUtils.success(agendaInfoResponse));
}

@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));
}
}
17 changes: 17 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 @@ -75,6 +75,9 @@ public Agenda(Meeting meeting, String title, AgendaType type, Duration allocated
this.orderNum = orderNum;
}

public void setOrderNum(Integer orderNum) {
this.orderNum = orderNum;
}

public void start() {
validateTransition(AgendaStatus.PENDING);
Expand Down Expand Up @@ -102,6 +105,15 @@ public void extendDuration(Duration extension) {
}
}

public void reduceDuration(Duration reduction) {
if (this.status != AgendaStatus.COMPLETED && this.allocatedDuration.compareTo(reduction) > 0) {
this.allocatedDuration = this.allocatedDuration.minus(reduction);
} else {
throw new BadRequestError(BadRequestError.ErrorCode.WRONG_REQUEST_TRANSMISSION,
Collections.singletonMap("AgendaDuration", "Invalid duration reduction"));
}
}

public void complete() {
validateTransition(AgendaStatus.INPROGRESS, AgendaStatus.PAUSED);

Expand Down Expand Up @@ -158,5 +170,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
@@ -1,7 +1,7 @@
package org.dnd.timeet.agenda.domain;

public enum AgendaAction {
START, PAUSE, RESUME, END, MODIFY;
START, PAUSE, RESUME, END, EXTEND, REDUCE;

@Override
public String toString() {
Expand Down
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 @@ -10,7 +10,8 @@
@Setter
@NoArgsConstructor
public class AgendaActionRequest {

private String action;
private String modifiedDuration; // "HH:MM" ν˜•μ‹, optional
private String modifiedDuration;

}
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 @@ -35,8 +34,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();
}
}
Loading
Loading