회의 참석자 초대 API 개발

This commit is contained in:
cyjadela
2025-10-24 13:50:02 +09:00
parent b819727edf
commit d55b2cd7af
7 changed files with 453 additions and 1 deletions
@@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
@@ -106,4 +107,14 @@ public class Meeting {
public boolean isInProgress() {
return "IN_PROGRESS".equals(this.status);
}
/**
* 참석자 추가
*/
public void addParticipant(String participantEmail) {
if (this.participants == null) {
this.participants = new ArrayList<>();
}
this.participants.add(participantEmail);
}
}
@@ -27,7 +27,8 @@ public class MeetingService implements
StartMeetingUseCase,
EndMeetingUseCase,
CancelMeetingUseCase,
GetMeetingUseCase {
GetMeetingUseCase,
InviteParticipantUseCase {
private final MeetingReader meetingReader;
private final MeetingWriter meetingWriter;
@@ -200,4 +201,43 @@ public class MeetingService implements
return meetingReader.findByOrganizerIdAndStatus(organizerId, status);
}
/**
* 회의 참석자 초대
*/
@Override
@Transactional
public void inviteParticipant(InviteParticipantCommand command) {
log.info("Inviting participant to meeting: {}, email: {}", command.meetingId(), command.email());
// 회의 조회
Meeting meeting = meetingReader.findById(command.meetingId())
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 회의 상태 검증 (예약됨 또는 진행중인 회의만 초대 가능)
if ("COMPLETED".equals(meeting.getStatus()) || "CANCELLED".equals(meeting.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 이미 참석자로 등록되었는지 확인
if (meeting.getParticipants() != null && meeting.getParticipants().contains(command.email())) {
log.warn("Email {} is already a participant of meeting {}", command.email(), command.meetingId());
throw new BusinessException(ErrorCode.DUPLICATE_RESOURCE);
}
// 참석자 목록에 추가
meeting.addParticipant(command.email());
// 저장
meetingWriter.save(meeting);
// TODO: 실제 이메일 발송 구현 필요
// 이메일 발송 서비스 호출
// emailService.sendInvitation(command.email(), meeting, command.frontendUrl());
// 현재는 로그만 남기고 성공으로 처리
log.info("Invitation email would be sent to {} for meeting {} (Frontend URL: {})",
command.email(), meeting.getTitle(), command.frontendUrl());
log.info("Participant invited successfully: {} to meeting {}", command.email(), command.meetingId());
}
}
@@ -0,0 +1,23 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
/**
* 회의 참석자 초대 UseCase
*/
public interface InviteParticipantUseCase {
/**
* 회의 참석자 초대
* 이메일로 초대장 발송
*/
void inviteParticipant(InviteParticipantCommand command);
/**
* 참석자 초대 명령
*/
record InviteParticipantCommand(
String meetingId,
String email,
String inviterName,
String frontendUrl
) {}
}
@@ -4,7 +4,9 @@ import com.unicorn.hgzero.common.dto.ApiResponse;
import com.unicorn.hgzero.meeting.biz.dto.MeetingDTO;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
import com.unicorn.hgzero.meeting.infra.dto.request.CreateMeetingRequest;
import com.unicorn.hgzero.meeting.infra.dto.request.InviteParticipantRequest;
import com.unicorn.hgzero.meeting.infra.dto.request.SelectTemplateRequest;
import com.unicorn.hgzero.meeting.infra.dto.response.InviteParticipantResponse;
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingResponse;
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
import io.swagger.v3.oas.annotations.Operation;
@@ -35,6 +37,7 @@ public class MeetingController {
private final EndMeetingUseCase endMeetingUseCase;
private final GetMeetingUseCase getMeetingUseCase;
private final CancelMeetingUseCase cancelMeetingUseCase;
private final InviteParticipantUseCase inviteParticipantUseCase;
/**
* 회의 예약
@@ -241,4 +244,53 @@ public class MeetingController {
return ResponseEntity.ok(ApiResponse.success(null));
}
/**
* 회의 참석자 초대
*
* @param meetingId 회의 ID
* @param userId 사용자 ID
* @param userName 사용자명
* @param userEmail 사용자 이메일
* @param request 참석자 초대 요청
* @return 초대 결과
*/
@Operation(
summary = "회의 참석자 초대",
description = "진행 중이거나 예약된 회의에 새로운 참석자를 초대합니다. 초대된 참석자에게는 회의록 프론트엔드 URL과 함께 이메일이 발송됩니다.",
security = @SecurityRequirement(name = "bearerAuth")
)
@PostMapping("/{meetingId}/invite")
public ResponseEntity<ApiResponse<InviteParticipantResponse>> inviteParticipant(
@Parameter(description = "회의 ID", required = true)
@PathVariable String meetingId,
@Parameter(description = "사용자 ID", required = true)
@RequestHeader("X-User-Id") String userId,
@Parameter(description = "사용자명", required = true)
@RequestHeader("X-User-Name") String userName,
@Parameter(description = "사용자 이메일", required = true)
@RequestHeader("X-User-Email") String userEmail,
@Valid @RequestBody InviteParticipantRequest request) {
log.info("참석자 초대 요청 - meetingId: {}, email: {}, inviter: {}",
meetingId, request.getEmail(), userName);
// 프론트엔드 URL 생성 (실제 환경에서는 설정에서 가져와야 함)
String frontendUrl = "https://meeting.hgzero.com/meeting/" + meetingId;
inviteParticipantUseCase.inviteParticipant(
new InviteParticipantUseCase.InviteParticipantCommand(
meetingId,
request.getEmail(),
userName,
frontendUrl
)
);
var response = InviteParticipantResponse.of(meetingId, request.getEmail());
log.info("참석자 초대 완료 - meetingId: {}, email: {}", meetingId, request.getEmail());
return ResponseEntity.ok(ApiResponse.success(response));
}
}
@@ -0,0 +1,23 @@
package com.unicorn.hgzero.meeting.infra.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 참석자 초대 요청 DTO
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "참석자 초대 요청")
public class InviteParticipantRequest {
@NotBlank(message = "이메일 주소는 필수입니다")
@Email(message = "올바른 이메일 형식이 아닙니다")
@Schema(description = "초대할 참석자 이메일", example = "newparticipant@example.com", required = true)
private String email;
}
@@ -0,0 +1,37 @@
package com.unicorn.hgzero.meeting.infra.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 참석자 초대 응답 DTO
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "참석자 초대 응답")
public class InviteParticipantResponse {
@Schema(description = "회의 ID", example = "123e4567-e89b-12d3-a456-426614174000")
private String meetingId;
@Schema(description = "초대된 참석자 이메일", example = "newparticipant@example.com")
private String invitedEmail;
@Schema(description = "초대 성공 여부", example = "true")
private boolean success;
@Schema(description = "메시지", example = "참석자 초대가 완료되었습니다. 이메일을 확인해주세요.")
private String message;
public static InviteParticipantResponse of(String meetingId, String email) {
return new InviteParticipantResponse(
meetingId,
email,
true,
"참석자 초대가 완료되었습니다. 이메일을 확인해주세요."
);
}
}