Skip to content
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 build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

import com.whylog.server.domain.meeting.dto.MeetingRequest;
import com.whylog.server.domain.meeting.dto.MeetingResponse;
import com.whylog.server.domain.meeting.enums.MeetingStatus;
import com.whylog.server.domain.meeting.service.MeetingCommandService;
import com.whylog.server.domain.meeting.service.MeetingQueryService;
import com.whylog.server.global.apiPayload.ApiResponse;
import com.whylog.server.global.auth.annotation.CurrentMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,41 +29,73 @@
@Tag(name = "Meeting", description = "회의 관련 API")
public class MeetingController {

private final MeetingCommandService meetingCommandService;
private final MeetingQueryService meetingQueryService;

@GetMapping("/teams/{teamId}/meetings")
@Operation(summary = "회의 목록 조회 API", description = "특정 팀의 회의 목록을 조회하는 API입니다. (status: ONGOING/COMPLETED)")
@Operation(summary = "회의 목록 조회 API", description = """

특정 팀의 회의 목록을 조회하는 API입니다.
status: ONGOING/COMPLETED
- status는 필수가 아니며, 기본값은 COMPLETED 입니다.

elapse : 경과시간
- 시:분:초 형태
- 완료된 회의라면 null로 반환함( 표시할 필요 없으니까 )

페이징 없습니다.

""")
public ApiResponse<List<MeetingResponse.MeetingListDTO>> getMeetings(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long teamId,
@RequestParam(required = false) String status) {
return ApiResponse.onSuccess(null);
}
@RequestParam(required = false, defaultValue = "COMPLETED") MeetingStatus status) {

@PostMapping("/meetings/{meetingId}/join")
@Operation(summary = "회의 입장 API", description = "특정 회의에 입장하는 API입니다.")
public ApiResponse<MeetingResponse.MeetingJoinResponseDTO> joinMeeting(
@PathVariable Long meetingId) {
return ApiResponse.onSuccess(null);
return ApiResponse.onSuccess(meetingQueryService.getMeetings(teamId, status));
}

// @PostMapping("/meetings/{meetingId}/join")
// @Operation(summary = "회의 입장 API", description = "특정 회의에 입장하는 API입니다.")
// public ApiResponse<MeetingResponse.MeetingJoinResponseDTO> joinMeeting(
// @PathVariable Long meetingId) {
// return ApiResponse.onSuccess(null);
// }

@PostMapping("/teams/{teamId}/meetings")
@Operation(summary = "회의 생성 API", description = "새로운 회의를 생성하는 API입니다.")
@Operation(summary = "회의 생성 API", description = """

새로운 회의를 생성하는 API입니다. 생성하면 실시긴 회의방이 하나 생성됩니다.
해당 API는 방 생성만 담당합니다. 회의 참여를 위해서는 웹소켓 연결이 필요합니다.

""")
public ApiResponse<MeetingResponse.MeetingCreateResponseDTO> createMeeting(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long teamId,
@Valid @RequestBody MeetingRequest.MeetingCreateDTO request) {
return ApiResponse.onSuccess(null);
MeetingResponse.MeetingCreateResponseDTO result = meetingCommandService.makeMeetingRoom(memberId, teamId, request);
return ApiResponse.onSuccess(result);
}

@PatchMapping("/meetings/{meetingId}/end")
@Operation(summary = "회의 종료 API", description = "진행 중인 회의를 종료하는 API입니다.")
@Operation(summary = "회의 종료 API", description = """
진행 중인 회의를 종료하는 API입니다.
회의 종료 시 실시간 회의 참여자들에게 종료를 알리는 웹소켓 메시지를 전송합니다.
""")
public ApiResponse<MeetingResponse.MeetingEndResponseDTO> endMeeting(
@Parameter(hidden= true) @CurrentMember Long memberId,
@PathVariable Long meetingId) {
return ApiResponse.onSuccess(null);
return ApiResponse.onSuccess(meetingCommandService.endMeeting(memberId, meetingId));
}

@GetMapping("/meetings/{meetingId}")
@Operation(summary = "회의 기본 정보 조회 API", description = "특정 회의의 기본 정보를 조회하는 API입니다.")
@Operation(summary = "회의 기본 정보 조회 API", description = """
특정 회의의 기본 정보를 조회하는 API입니다.
회의명, 날짜, 기간, 참여자 정보를 제공합니다.
본 API에서 분석결과, 대화기록은 제공하지 않습니다.
""")
public ApiResponse<MeetingResponse.MeetingDetailDTO> getMeetingDetail(
@PathVariable Long meetingId) {
return ApiResponse.onSuccess(null);
return ApiResponse.onSuccess(meetingQueryService.getMeetingDefaultInfo(meetingId));
}

@GetMapping("/meetings/{meetingId}/history")
Expand All @@ -82,10 +119,10 @@ public ApiResponse<MeetingResponse.AudioDTO> getAudio(
return ApiResponse.onSuccess(null);
}

@GetMapping("/meetings/{meetingId}/applications")
@Operation(summary = "적용사항 목록 조회 API", description = "특정 회의의 적용사항 목록을 조회하는 API입니다.")
public ApiResponse<List<MeetingResponse.ApplicationDTO>> getApplications(
@PathVariable Long meetingId) {
return ApiResponse.onSuccess(null);
}
// @GetMapping("/meetings/{meetingId}/applications")
// @Operation(summary = "적용사항 목록 조회 API", description = "특정 회의의 적용사항 목록을 조회하는 API입니다.")
// public ApiResponse<List<MeetingResponse.ApplicationDTO>> getApplications(
// @PathVariable Long meetingId) {
// return ApiResponse.onSuccess(null);
// }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.whylog.server.domain.meeting.dto;

import com.whylog.server.domain.meeting.enums.MeetingStatus;
import com.whylog.server.domain.meeting.socket.MeetingParticipant;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -24,7 +26,11 @@ public static class MeetingListDTO {
private String name;

@Schema(description = "회의 상태", example = "ONGOING")
private String status;
private MeetingStatus status;

@Schema(description = "경과시간 (시:분:초)", example = "00:00:00")
private String elapse;

}

@Getter
Expand Down Expand Up @@ -112,7 +118,7 @@ public static class MeetingDetailDTO {
private Integer memberCount;

@Schema(description = "회의 참여자 목록", example = "[1, 2, 3]")
private List<Long> members;
private List<MeetingParticipantInfo> members;
}

@Getter
Expand Down Expand Up @@ -210,4 +216,23 @@ public static class ApplicationDTO {
@Schema(description = "적용사항명", example = "Redis 기술 변경")
private String name;
}

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "미팅 내 참여자 정보")
public static class MeetingParticipantInfo{

@Schema(description = "멤버 id", example = "1")
private Long memberId;

@Schema(description = "유저이름", example = "아무개")
private String name;

@Schema(description = "프로필이미지", example = "https://example.com/profile/user-1.jpg")
private String profileImage;

}

}
71 changes: 56 additions & 15 deletions src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
package com.whylog.server.domain.meeting.entity;

import com.whylog.server.domain.decision.entity.Decision;
import com.whylog.server.domain.meeting.dto.MeetingRequest;
import com.whylog.server.domain.meeting.enums.MeetingStatus;
import com.whylog.server.domain.team.entity.Team;
import com.whylog.server.global.entity.BaseEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.*;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand Down Expand Up @@ -46,8 +41,54 @@ public class Meeting extends BaseEntity {
@Column(name = "end_date_time")
private LocalDateTime endDateTime;

// @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
// private final List<MeetingMember> meetingMembers = new ArrayList<>();
@Builder
private Meeting(String name, Team team) {
this.name = name;
this.team = team;
this.startDateTime = LocalDateTime.now();
this.endDateTime = null;
}

public static Meeting create(MeetingRequest.MeetingCreateDTO dto, Team team) {
return Meeting.builder()
.name(dto.getName())
.team(team)
.build();
}

public MeetingStatus getStatus(){
return this.endDateTime == null ? MeetingStatus.ONGOING : MeetingStatus.COMPLETED;
}

public boolean isOngoing(){
return this.endDateTime == null;
}

public LocalDateTime endMeeting() {
this.endDateTime = LocalDateTime.now();
return this.endDateTime;
}

public Long getDuration() {
if(this.endDateTime == null) return null;
return ChronoUnit.MINUTES.between(startDateTime, endDateTime);
}

public String getElapse() {

Duration duration = Duration.between(startDateTime, LocalDateTime.now());

long seconds = duration.getSeconds();

long hours = seconds / 3600;
long minutes = (seconds % 3600) / 60;
long secs = seconds % 60;

return String.format("%02d:%02d:%02d", hours, minutes, secs);
}

@OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<MeetingMember> meetingMembers = new ArrayList<>();
//
// @OneToOne(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
// private final MeetingAnalysis meetingAnalyses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -38,5 +39,20 @@ public class MeetingMember extends BaseEntity {
@Enumerated(EnumType.STRING)
private MeetingRole role;


@Builder
private MeetingMember(MeetingMemberId id, Meeting meeting, Member member, MeetingRole role) {
this.id = id;
this.meeting = meeting;
this.member = member;
this.role = role;
}

public static MeetingMember create(Meeting meeting, Member member, MeetingRole role) {
return MeetingMember.builder()
.id(new MeetingMemberId(meeting.getId(), member.getId()))
.meeting(meeting)
.member(member)
.role(role)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public class MeetingMemberId implements Serializable {

@Column(name = "member_id")
private Long memberId;

public MeetingMemberId(Long meetingId, Long memberId) {
this.meetingId = meetingId;
this.memberId = memberId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.whylog.server.domain.meeting.enums;

public enum MeetingStatus {

ONGOING,
COMPLETED
;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whylog.server.domain.meeting.exception;

import com.whylog.server.global.apiPayload.exception.GeneralException;

public class MeetingAlreadyEndedException extends GeneralException {

public MeetingAlreadyEndedException() {
super(MeetingErrorCode.MEETING_ALREADY_ENDED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.whylog.server.domain.meeting.exception;

import com.whylog.server.global.apiPayload.code.BaseErrorCode;
import com.whylog.server.global.apiPayload.code.ErrorReasonDTO;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum MeetingErrorCode implements BaseErrorCode {

MEETING_NOT_FOUND(HttpStatus.NOT_FOUND, "MEETING_404", "존재하지 않는 회의입니다."),
MEETING_ALREADY_ENDED(HttpStatus.CONFLICT, "MEETING_409", "이미 종료된 회의입니다."),
MEETING_INVALID_MEMBER(HttpStatus.CONFLICT, "MEETING_410", "회의에 소속된 참여자가 아닙니다."),
;

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.isSuccess(false)
.code(code)
.message(message)
.build();
}

@Override
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.httpStatus(httpStatus)
.isSuccess(false)
.code(code)
.message(message)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.whylog.server.domain.meeting.exception;

import com.whylog.server.global.apiPayload.exception.GeneralException;

public class MeetingInvalidMemberException extends GeneralException {
public MeetingInvalidMemberException() {
super(MeetingErrorCode.MEETING_INVALID_MEMBER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whylog.server.domain.meeting.exception;

import com.whylog.server.global.apiPayload.exception.GeneralException;

public class MeetingNotFoundException extends GeneralException {

public MeetingNotFoundException() {
super(MeetingErrorCode.MEETING_NOT_FOUND);
}
}
Loading
Loading