mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 13:46:24 +00:00
Merge pull request #10 from hwanny1128/feat/notification-noti_request
fix: meeting 참석자 데이터 정규화
This commit is contained in:
commit
6cdad89ad3
File diff suppressed because it is too large
Load Diff
BIN
meeting/logs/meeting-service.log.2025-10-26.0.gz
Normal file
BIN
meeting/logs/meeting-service.log.2025-10-26.0.gz
Normal file
Binary file not shown.
@ -45,6 +45,8 @@ public class MeetingService implements
|
|||||||
private final MinutesWriter minutesWriter;
|
private final MinutesWriter minutesWriter;
|
||||||
private final CacheService cacheService;
|
private final CacheService cacheService;
|
||||||
private final EventPublisher eventPublisher;
|
private final EventPublisher eventPublisher;
|
||||||
|
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter participantWriter;
|
||||||
|
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader participantReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 생성
|
* 회의 생성
|
||||||
@ -96,6 +98,12 @@ public class MeetingService implements
|
|||||||
// 5. 회의 저장
|
// 5. 회의 저장
|
||||||
Meeting savedMeeting = meetingWriter.save(meeting);
|
Meeting savedMeeting = meetingWriter.save(meeting);
|
||||||
|
|
||||||
|
// 5-1. 참석자 목록 저장
|
||||||
|
if (command.participants() != null && !command.participants().isEmpty()) {
|
||||||
|
participantWriter.saveParticipants(meetingId, command.participants());
|
||||||
|
log.debug("Participants saved: meetingId={}, count={}", meetingId, command.participants().size());
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 캐시 저장 (TTL: 10분)
|
// 6. 캐시 저장 (TTL: 10분)
|
||||||
try {
|
try {
|
||||||
cacheService.cacheMeeting(meetingId, savedMeeting, 600);
|
cacheService.cacheMeeting(meetingId, savedMeeting, 600);
|
||||||
@ -382,22 +390,19 @@ public class MeetingService implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 이미 참석자로 등록되었는지 확인
|
// 이미 참석자로 등록되었는지 확인
|
||||||
if (meeting.getParticipants() != null && meeting.getParticipants().contains(command.email())) {
|
if (participantReader.existsParticipant(command.meetingId(), command.email())) {
|
||||||
log.warn("Email {} is already a participant of meeting {}", command.email(), command.meetingId());
|
log.warn("Email {} is already a participant of meeting {}", command.email(), command.meetingId());
|
||||||
throw new BusinessException(ErrorCode.DUPLICATE_RESOURCE);
|
throw new BusinessException(ErrorCode.DUPLICATE_RESOURCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 참석자 목록에 추가
|
// 참석자 저장
|
||||||
meeting.addParticipant(command.email());
|
participantWriter.saveParticipant(command.meetingId(), command.email());
|
||||||
|
|
||||||
// 저장
|
|
||||||
meetingWriter.save(meeting);
|
|
||||||
|
|
||||||
// TODO: 실제 이메일 발송 구현 필요
|
// TODO: 실제 이메일 발송 구현 필요
|
||||||
// 이메일 발송 서비스 호출
|
// 이메일 발송 서비스 호출
|
||||||
// emailService.sendInvitation(command.email(), meeting, command.frontendUrl());
|
// emailService.sendInvitation(command.email(), meeting, command.frontendUrl());
|
||||||
// 현재는 로그만 남기고 성공으로 처리
|
// 현재는 로그만 남기고 성공으로 처리
|
||||||
log.info("Invitation email would be sent to {} for meeting {} (Frontend URL: {})",
|
log.info("Invitation email would be sent to {} for meeting {} (Frontend URL: {})",
|
||||||
command.email(), meeting.getTitle(), command.frontendUrl());
|
command.email(), meeting.getTitle(), command.frontendUrl());
|
||||||
|
|
||||||
log.info("Participant invited successfully: {} to meeting {}", command.email(), command.meetingId());
|
log.info("Participant invited successfully: {} to meeting {}", command.email(), command.meetingId());
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참석자 조회 인터페이스
|
||||||
|
*/
|
||||||
|
public interface ParticipantReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID로 참석자 목록 조회
|
||||||
|
*/
|
||||||
|
List<String> findParticipantsByMeetingId(String meetingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID로 참여 회의 목록 조회
|
||||||
|
*/
|
||||||
|
List<String> findMeetingsByParticipant(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 회의에 특정 사용자가 참석자로 등록되어 있는지 확인
|
||||||
|
*/
|
||||||
|
boolean existsParticipant(String meetingId, String userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참석자 저장 인터페이스
|
||||||
|
*/
|
||||||
|
public interface ParticipantWriter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의에 참석자 추가
|
||||||
|
*/
|
||||||
|
void saveParticipant(String meetingId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의에 참석자 목록 일괄 저장
|
||||||
|
*/
|
||||||
|
void saveParticipants(String meetingId, List<String> userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의에서 참석자 삭제
|
||||||
|
*/
|
||||||
|
void deleteParticipant(String meetingId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의의 모든 참석자 삭제
|
||||||
|
*/
|
||||||
|
void deleteAllParticipants(String meetingId);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package com.unicorn.hgzero.meeting.infra.gateway;
|
|||||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
|
||||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -24,48 +25,72 @@ import java.util.stream.Collectors;
|
|||||||
public class MeetingGateway implements MeetingReader, MeetingWriter {
|
public class MeetingGateway implements MeetingReader, MeetingWriter {
|
||||||
|
|
||||||
private final MeetingJpaRepository meetingJpaRepository;
|
private final MeetingJpaRepository meetingJpaRepository;
|
||||||
|
private final ParticipantReader participantReader;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Meeting> findById(String meetingId) {
|
public Optional<Meeting> findById(String meetingId) {
|
||||||
return meetingJpaRepository.findById(meetingId)
|
return meetingJpaRepository.findById(meetingId)
|
||||||
.map(MeetingEntity::toDomain);
|
.map(this::enrichWithParticipants);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Meeting> findByOrganizerId(String organizerId) {
|
public List<Meeting> findByOrganizerId(String organizerId) {
|
||||||
return meetingJpaRepository.findByOrganizerId(organizerId).stream()
|
return meetingJpaRepository.findByOrganizerId(organizerId).stream()
|
||||||
.map(MeetingEntity::toDomain)
|
.map(this::enrichWithParticipants)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Meeting> findByStatus(String status) {
|
public List<Meeting> findByStatus(String status) {
|
||||||
return meetingJpaRepository.findByStatus(status).stream()
|
return meetingJpaRepository.findByStatus(status).stream()
|
||||||
.map(MeetingEntity::toDomain)
|
.map(this::enrichWithParticipants)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status) {
|
public List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status) {
|
||||||
return meetingJpaRepository.findByOrganizerIdAndStatus(organizerId, status).stream()
|
return meetingJpaRepository.findByOrganizerIdAndStatus(organizerId, status).stream()
|
||||||
.map(MeetingEntity::toDomain)
|
.map(this::enrichWithParticipants)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
public List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
return meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
return meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||||
.map(MeetingEntity::toDomain)
|
.map(this::enrichWithParticipants)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Meeting> findByTemplateId(String templateId) {
|
public List<Meeting> findByTemplateId(String templateId) {
|
||||||
return meetingJpaRepository.findByTemplateId(templateId).stream()
|
return meetingJpaRepository.findByTemplateId(templateId).stream()
|
||||||
.map(MeetingEntity::toDomain)
|
.map(this::enrichWithParticipants)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meeting 엔티티를 도메인으로 변환하면서 participants 정보 추가
|
||||||
|
*/
|
||||||
|
private Meeting enrichWithParticipants(MeetingEntity entity) {
|
||||||
|
Meeting meeting = entity.toDomain();
|
||||||
|
List<String> participants = participantReader.findParticipantsByMeetingId(entity.getMeetingId());
|
||||||
|
return Meeting.builder()
|
||||||
|
.meetingId(meeting.getMeetingId())
|
||||||
|
.title(meeting.getTitle())
|
||||||
|
.purpose(meeting.getPurpose())
|
||||||
|
.description(meeting.getDescription())
|
||||||
|
.scheduledAt(meeting.getScheduledAt())
|
||||||
|
.endTime(meeting.getEndTime())
|
||||||
|
.location(meeting.getLocation())
|
||||||
|
.startedAt(meeting.getStartedAt())
|
||||||
|
.endedAt(meeting.getEndedAt())
|
||||||
|
.status(meeting.getStatus())
|
||||||
|
.organizerId(meeting.getOrganizerId())
|
||||||
|
.participants(participants)
|
||||||
|
.templateId(meeting.getTemplateId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Meeting save(Meeting meeting) {
|
public Meeting save(Meeting meeting) {
|
||||||
MeetingEntity entity = MeetingEntity.fromDomain(meeting);
|
MeetingEntity entity = MeetingEntity.fromDomain(meeting);
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingParticipantEntity;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingParticipantJpaRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참석자 Gateway 구현체
|
||||||
|
* ParticipantReader, ParticipantWriter 인터페이스 구현
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ParticipantGateway implements ParticipantReader, ParticipantWriter {
|
||||||
|
|
||||||
|
private final MeetingParticipantJpaRepository participantRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<String> findParticipantsByMeetingId(String meetingId) {
|
||||||
|
return participantRepository.findByMeetingId(meetingId).stream()
|
||||||
|
.map(MeetingParticipantEntity::getUserId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<String> findMeetingsByParticipant(String userId) {
|
||||||
|
return participantRepository.findByUserId(userId).stream()
|
||||||
|
.map(MeetingParticipantEntity::getMeetingId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean existsParticipant(String meetingId, String userId) {
|
||||||
|
return participantRepository.existsByMeetingIdAndUserId(meetingId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void saveParticipant(String meetingId, String userId) {
|
||||||
|
if (!participantRepository.existsByMeetingIdAndUserId(meetingId, userId)) {
|
||||||
|
MeetingParticipantEntity participant = MeetingParticipantEntity.builder()
|
||||||
|
.meetingId(meetingId)
|
||||||
|
.userId(userId)
|
||||||
|
.invitationStatus("PENDING")
|
||||||
|
.attended(false)
|
||||||
|
.build();
|
||||||
|
participantRepository.save(participant);
|
||||||
|
log.debug("Participant saved: meetingId={}, userId={}", meetingId, userId);
|
||||||
|
} else {
|
||||||
|
log.debug("Participant already exists: meetingId={}, userId={}", meetingId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void saveParticipants(String meetingId, List<String> userIds) {
|
||||||
|
if (userIds == null || userIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MeetingParticipantEntity> participants = userIds.stream()
|
||||||
|
.filter(userId -> !participantRepository.existsByMeetingIdAndUserId(meetingId, userId))
|
||||||
|
.map(userId -> MeetingParticipantEntity.builder()
|
||||||
|
.meetingId(meetingId)
|
||||||
|
.userId(userId)
|
||||||
|
.invitationStatus("PENDING")
|
||||||
|
.attended(false)
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!participants.isEmpty()) {
|
||||||
|
participantRepository.saveAll(participants);
|
||||||
|
log.debug("Participants saved: meetingId={}, count={}", meetingId, participants.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteParticipant(String meetingId, String userId) {
|
||||||
|
participantRepository.deleteById(new com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingParticipantId(meetingId, userId));
|
||||||
|
log.debug("Participant deleted: meetingId={}, userId={}", meetingId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteAllParticipants(String meetingId) {
|
||||||
|
participantRepository.deleteByMeetingId(meetingId);
|
||||||
|
log.debug("All participants deleted: meetingId={}", meetingId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -59,12 +59,16 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
@Column(name = "organizer_id", length = 50, nullable = false)
|
@Column(name = "organizer_id", length = 50, nullable = false)
|
||||||
private String organizerId;
|
private String organizerId;
|
||||||
|
|
||||||
@Column(name = "participants", columnDefinition = "TEXT")
|
|
||||||
private String participants;
|
|
||||||
|
|
||||||
@Column(name = "template_id", length = 50)
|
@Column(name = "template_id", length = 50)
|
||||||
private String templateId;
|
private String templateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 참석자 목록 (일대다 관계)
|
||||||
|
*/
|
||||||
|
@OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@Builder.Default
|
||||||
|
private List<MeetingParticipantEntity> participants = new ArrayList<>();
|
||||||
|
|
||||||
public Meeting toDomain() {
|
public Meeting toDomain() {
|
||||||
return Meeting.builder()
|
return Meeting.builder()
|
||||||
.meetingId(this.meetingId)
|
.meetingId(this.meetingId)
|
||||||
@ -78,7 +82,11 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
.endedAt(this.endedAt)
|
.endedAt(this.endedAt)
|
||||||
.status(this.status)
|
.status(this.status)
|
||||||
.organizerId(this.organizerId)
|
.organizerId(this.organizerId)
|
||||||
.participants(parseParticipants(this.participants))
|
.participants(this.participants != null
|
||||||
|
? this.participants.stream()
|
||||||
|
.map(MeetingParticipantEntity::getUserId)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
: List.of())
|
||||||
.templateId(this.templateId)
|
.templateId(this.templateId)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -96,7 +104,6 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
.endedAt(meeting.getEndedAt())
|
.endedAt(meeting.getEndedAt())
|
||||||
.status(meeting.getStatus())
|
.status(meeting.getStatus())
|
||||||
.organizerId(meeting.getOrganizerId())
|
.organizerId(meeting.getOrganizerId())
|
||||||
.participants(formatParticipants(meeting.getParticipants()))
|
|
||||||
.templateId(meeting.getTemplateId())
|
.templateId(meeting.getTemplateId())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -110,18 +117,4 @@ public class MeetingEntity extends BaseTimeEntity {
|
|||||||
this.status = "COMPLETED";
|
this.status = "COMPLETED";
|
||||||
this.endedAt = LocalDateTime.now();
|
this.endedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> parseParticipants(String participants) {
|
|
||||||
if (participants == null || participants.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return Arrays.asList(participants.split(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatParticipants(List<String> participants) {
|
|
||||||
if (participants == null || participants.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return String.join(",", participants);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 참석자 Entity
|
||||||
|
* meeting_id와 user_id를 복합키로 사용하여 다대다 관계 표현
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "meeting_participants")
|
||||||
|
@IdClass(MeetingParticipantId.class)
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MeetingParticipantEntity extends BaseTimeEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID (복합키)
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@Column(name = "meeting_id", length = 50)
|
||||||
|
private String meetingId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID (이메일) (복합키)
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@Column(name = "user_id", length = 100)
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초대 상태 (PENDING, ACCEPTED, DECLINED)
|
||||||
|
*/
|
||||||
|
@Column(name = "invitation_status", length = 20)
|
||||||
|
@Builder.Default
|
||||||
|
private String invitationStatus = "PENDING";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참석 여부
|
||||||
|
*/
|
||||||
|
@Column(name = "attended")
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean attended = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 엔티티와의 관계
|
||||||
|
*/
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "meeting_id", insertable = false, updatable = false)
|
||||||
|
private MeetingEntity meeting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초대 수락
|
||||||
|
*/
|
||||||
|
public void accept() {
|
||||||
|
this.invitationStatus = "ACCEPTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초대 거절
|
||||||
|
*/
|
||||||
|
public void decline() {
|
||||||
|
this.invitationStatus = "DECLINED";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참석 처리
|
||||||
|
*/
|
||||||
|
public void markAsAttended() {
|
||||||
|
this.attended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 참석자 복합키
|
||||||
|
* meeting_id와 user_id를 복합키로 사용
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MeetingParticipantId implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID
|
||||||
|
*/
|
||||||
|
private String meetingId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID (이메일)
|
||||||
|
*/
|
||||||
|
private String userId;
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingParticipantEntity;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingParticipantId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 참석자 JPA Repository
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface MeetingParticipantJpaRepository extends JpaRepository<MeetingParticipantEntity, MeetingParticipantId> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID로 참석자 목록 조회
|
||||||
|
*/
|
||||||
|
List<MeetingParticipantEntity> findByMeetingId(String meetingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID로 참여 회의 목록 조회
|
||||||
|
*/
|
||||||
|
List<MeetingParticipantEntity> findByUserId(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID와 초대 상태로 참석자 목록 조회
|
||||||
|
*/
|
||||||
|
List<MeetingParticipantEntity> findByMeetingIdAndInvitationStatus(String meetingId, String invitationStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID로 참석자 전체 삭제
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM MeetingParticipantEntity p WHERE p.meetingId = :meetingId")
|
||||||
|
void deleteByMeetingId(@Param("meetingId") String meetingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 ID와 사용자 ID로 참석자 존재 여부 확인
|
||||||
|
*/
|
||||||
|
boolean existsByMeetingIdAndUserId(String meetingId, String userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
-- 회의 참석자 테이블 생성
|
||||||
|
CREATE TABLE IF NOT EXISTS meeting_participants (
|
||||||
|
meeting_id VARCHAR(50) NOT NULL,
|
||||||
|
user_id VARCHAR(100) NOT NULL,
|
||||||
|
invitation_status VARCHAR(20) DEFAULT 'PENDING',
|
||||||
|
attended BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (meeting_id, user_id),
|
||||||
|
CONSTRAINT fk_meeting_participants_meeting
|
||||||
|
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 기존 meetings 테이블의 participants 데이터를 meeting_participants 테이블로 마이그레이션
|
||||||
|
INSERT INTO meeting_participants (meeting_id, user_id, invitation_status, attended, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
m.meeting_id,
|
||||||
|
TRIM(participant) as user_id,
|
||||||
|
'PENDING' as invitation_status,
|
||||||
|
FALSE as attended,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM meetings m
|
||||||
|
CROSS JOIN LATERAL unnest(string_to_array(m.participants, ',')) AS participant
|
||||||
|
WHERE m.participants IS NOT NULL AND m.participants != '';
|
||||||
|
|
||||||
|
-- meetings 테이블에서 participants 컬럼 삭제
|
||||||
|
ALTER TABLE meetings DROP COLUMN IF EXISTS participants;
|
||||||
|
|
||||||
|
-- 인덱스 생성
|
||||||
|
CREATE INDEX idx_meeting_participants_user_id ON meeting_participants(user_id);
|
||||||
|
CREATE INDEX idx_meeting_participants_invitation_status ON meeting_participants(invitation_status);
|
||||||
|
CREATE INDEX idx_meeting_participants_meeting_id_status ON meeting_participants(meeting_id, invitation_status);
|
||||||
|
|
||||||
|
-- 코멘트 추가
|
||||||
|
COMMENT ON TABLE meeting_participants IS '회의 참석자 정보';
|
||||||
|
COMMENT ON COLUMN meeting_participants.meeting_id IS '회의 ID';
|
||||||
|
COMMENT ON COLUMN meeting_participants.user_id IS '사용자 ID (이메일)';
|
||||||
|
COMMENT ON COLUMN meeting_participants.invitation_status IS '초대 상태 (PENDING, ACCEPTED, DECLINED)';
|
||||||
|
COMMENT ON COLUMN meeting_participants.attended IS '참석 여부';
|
||||||
Loading…
x
Reference in New Issue
Block a user