mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 01:19:11 +00:00
for merge
This commit is contained in:
@@ -6,6 +6,7 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,16 @@ public class Meeting {
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 회의 목적
|
||||
*/
|
||||
private String purpose;
|
||||
|
||||
/**
|
||||
* 회의 장소
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 회의 일시
|
||||
*/
|
||||
@@ -106,4 +117,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ public class MeetingDTO {
|
||||
*/
|
||||
private final LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 회의 목적
|
||||
*/
|
||||
private final String purpose;
|
||||
|
||||
/**
|
||||
* 회의 장소
|
||||
*/
|
||||
@@ -78,6 +83,7 @@ public class MeetingDTO {
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.title(meeting.getTitle())
|
||||
.startTime(meeting.getStartedAt() != null ? meeting.getStartedAt() : meeting.getScheduledAt())
|
||||
.purpose(meeting.getPurpose())
|
||||
.endTime(meeting.getEndedAt() != null ? meeting.getEndedAt() : meeting.getEndTime())
|
||||
.location(meeting.getLocation())
|
||||
.agenda(meeting.getDescription())
|
||||
|
||||
@@ -29,7 +29,8 @@ public class MeetingService implements
|
||||
StartMeetingUseCase,
|
||||
EndMeetingUseCase,
|
||||
CancelMeetingUseCase,
|
||||
GetMeetingUseCase {
|
||||
GetMeetingUseCase,
|
||||
InviteParticipantUseCase {
|
||||
|
||||
private final MeetingReader meetingReader;
|
||||
private final MeetingWriter meetingWriter;
|
||||
@@ -72,7 +73,9 @@ public class MeetingService implements
|
||||
Meeting meeting = Meeting.builder()
|
||||
.meetingId(meetingId)
|
||||
.title(command.title())
|
||||
.purpose(command.purpose())
|
||||
.description(command.description())
|
||||
.location(command.location())
|
||||
.scheduledAt(command.scheduledAt())
|
||||
.endTime(command.endTime())
|
||||
.location(command.location())
|
||||
@@ -262,4 +265,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());
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -51,6 +51,8 @@ public class SecurityConfig {
|
||||
.requestMatchers("/health").permitAll()
|
||||
// WebSocket endpoints
|
||||
.requestMatchers("/ws/**").permitAll()
|
||||
// Meeting API endpoints (for testing)
|
||||
.requestMatchers("/api/meetings/**").permitAll()
|
||||
// All other requests require authentication
|
||||
// .anyRequest().authenticated()
|
||||
.anyRequest().permitAll()
|
||||
|
||||
@@ -22,7 +22,7 @@ public class SwaggerConfig {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8081")
|
||||
.url("http://localhost:8082")
|
||||
.description("Local Development"))
|
||||
.addServersItem(new Server()
|
||||
.url("{protocol}://{host}:{port}")
|
||||
|
||||
+55
-1
@@ -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;
|
||||
|
||||
/**
|
||||
* 회의 예약
|
||||
@@ -63,10 +66,12 @@ public class MeetingController {
|
||||
var meetingData = createMeetingUseCase.createMeeting(
|
||||
new CreateMeetingUseCase.CreateMeetingCommand(
|
||||
request.getTitle(),
|
||||
request.getPurpose(),
|
||||
request.getAgenda(),
|
||||
request.getStartTime(),
|
||||
request.getEndTime(),
|
||||
request.getLocation(),
|
||||
request.getLocation(),
|
||||
userId,
|
||||
request.getParticipants(),
|
||||
null // 템플릿 ID는 나중에 적용
|
||||
@@ -144,7 +149,7 @@ public class MeetingController {
|
||||
log.info("회의 시작 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
// meeting id 유효성 검증 필요
|
||||
|
||||
|
||||
var sessionData = startMeetingUseCase.startMeeting(meetingId);
|
||||
var response = SessionResponse.from(sessionData);
|
||||
|
||||
@@ -243,4 +248,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));
|
||||
}
|
||||
}
|
||||
+4
@@ -25,6 +25,10 @@ public class CreateMeetingRequest {
|
||||
@Schema(description = "회의 제목", example = "Q1 전략 회의", required = true)
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "회의 목적은 필수입니다")
|
||||
@Schema(description = "회의 목적", example = "2025년 1분기 신제품 개발 방향 수립", required = true)
|
||||
private String purpose;
|
||||
|
||||
@NotNull(message = "회의 시작 시간은 필수입니다")
|
||||
@Schema(description = "회의 시작 시간", example = "2025-01-25T14:00:00", required = true)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
+37
@@ -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,
|
||||
"참석자 초대가 완료되었습니다. 이메일을 확인해주세요."
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -28,6 +28,9 @@ public class MeetingResponse {
|
||||
@Schema(description = "회의 종료 시간", example = "2025-01-25T16:00:00")
|
||||
private final LocalDateTime endTime;
|
||||
|
||||
@Schema(description = "회의 목적", example = "2025년 1분기 신제품 개발 방향 수립")
|
||||
private final String purpose;
|
||||
|
||||
@Schema(description = "회의 장소", example = "회의실 A")
|
||||
private final String location;
|
||||
|
||||
@@ -58,6 +61,7 @@ public class MeetingResponse {
|
||||
.title(dto.getTitle())
|
||||
.startTime(dto.getStartTime())
|
||||
.endTime(dto.getEndTime())
|
||||
.purpose(dto.getPurpose())
|
||||
.location(dto.getLocation())
|
||||
.agenda(dto.getAgenda())
|
||||
.participants(dto.getParticipants().stream()
|
||||
|
||||
Reference in New Issue
Block a user