diff --git a/src/main/java/org/example/tackit/config/CommonDataInitializer.java b/src/main/java/org/example/tackit/config/CommonDataInitializer.java index bd9388f..cdb0faa 100644 --- a/src/main/java/org/example/tackit/config/CommonDataInitializer.java +++ b/src/main/java/org/example/tackit/config/CommonDataInitializer.java @@ -24,7 +24,7 @@ public class CommonDataInitializer implements CommandLineRunner { // args를 꼭 배열로 넘겨야 하는 건 아니고 가변적으로 받을 수 있다는 의도를 나타냄 @Override public void run(String... args) throws Exception { - if (memberRepository.findByEmail("admin").isEmpty()) { + if (memberRepository.findByEmail("contact.tackit@gmail.com").isEmpty()) { Member admin = Member.builder() .email("contact.tackit@gmail.com") .password(passwordEncoder.encode("admin1")) // BCrypt 인코딩 diff --git a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java b/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java index 756a131..d7c030f 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java +++ b/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java @@ -1,6 +1,8 @@ package org.example.tackit.domain.auth.login.repository; +import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Org.OrgStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -41,4 +43,10 @@ public interface MemberOrgRepository extends JpaRepository { // 조직 ID와 멤버 ID로 가입 여부 확인 (중복 가입 방지용) boolean existsByMemberIdAndOrganizationId(Long memberId, Long orgId); + + Optional findByMemberIdAndOrganizationId(Long memberId, Long orgId); + + boolean existsByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status); + + Optional findByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status); } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/entity/Event.java b/src/main/java/org/example/tackit/domain/entity/Event.java new file mode 100644 index 0000000..d756916 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/entity/Event.java @@ -0,0 +1,92 @@ +package org.example.tackit.domain.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.tackit.domain.entity.Org.Organization; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "event") +public class Event{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private LocalDateTime startsAt; + + @Column(nullable = false) + private LocalDateTime endsAt; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "color_chip", nullable = false) + private String colorChip; + + @Enumerated(EnumType.STRING) + @Column(name = "event_scope", nullable = false) + private EventScope eventScope; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "org_id", nullable = false) + private Organization organization; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member creator; + + @OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true) + private List participants = new ArrayList<>(); + + @Builder + public Event(String title, LocalDateTime startsAt, LocalDateTime endsAt, LocalDateTime createdAt, LocalDateTime updatedAt, + String description, String colorChip, EventScope eventScope, + Organization organization, Member creator) { + this.title = title; + this.startsAt = startsAt; + this.endsAt = endsAt; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.description = description; + this.colorChip = colorChip; + this.eventScope = eventScope; + this.organization = organization; + this.creator = creator; + } + + public void update(String title, LocalDateTime startsAt, LocalDateTime endsAt, + String description, String colorChip, EventScope eventScope) { + if (title != null) this.title = title; + if (startsAt != null) this.startsAt = startsAt; + if (endsAt != null) this.endsAt = endsAt; + if (description != null) this.description = description; + if (colorChip != null) this.colorChip = colorChip; + if (eventScope != null) this.eventScope = eventScope; + } + + // 참여자 목록 초기화 (참여자 수정 시 사용) + public void clearParticipants() { + this.participants.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/entity/EventParticipant.java b/src/main/java/org/example/tackit/domain/entity/EventParticipant.java new file mode 100644 index 0000000..8544392 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/entity/EventParticipant.java @@ -0,0 +1,48 @@ +package org.example.tackit.domain.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +@Table(name = "event_participant") +public class EventParticipant { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_participant_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_org_id", nullable = false) + private MemberOrg memberOrg; + + @CreatedDate + private LocalDateTime createdAt; + + @Builder + public EventParticipant(Event event, MemberOrg memberOrg) { + this.event = event; + this.memberOrg = memberOrg; + } + + // 연관관계 편의 메서드 + public void assignEvent(Event event) { + this.event = event; + event.getParticipants().add(this); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/entity/EventScope.java b/src/main/java/org/example/tackit/domain/entity/EventScope.java new file mode 100644 index 0000000..8b9beef --- /dev/null +++ b/src/main/java/org/example/tackit/domain/entity/EventScope.java @@ -0,0 +1,6 @@ +package org.example.tackit.domain.entity; + +public enum EventScope { + PARTIAL, + ALL +} diff --git a/src/main/java/org/example/tackit/domain/event/controller/EventController.java b/src/main/java/org/example/tackit/domain/event/controller/EventController.java new file mode 100644 index 0000000..ad0b77d --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/controller/EventController.java @@ -0,0 +1,125 @@ +package org.example.tackit.domain.event.controller; + +import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.auth.login.security.CustomUserDetails; +import org.example.tackit.domain.event.dto.EventCreateReqDto; +import org.example.tackit.domain.event.dto.EventDetailResDto; +import org.example.tackit.domain.event.dto.EventSimpleResDto; +import org.example.tackit.domain.event.dto.EventUpdateReqDto; +import org.example.tackit.domain.event.service.EventService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/events") +public class EventController { + + private final EventService eventService; + + /** + * 일정 생성 + */ + @PostMapping + public ResponseEntity createEvent( + @RequestBody EventCreateReqDto reqDto, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + // TODO 인증 정보가 없습니다 코드 공통 로직 처리 + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + Long eventId = eventService.createEvent(reqDto, userDetails.getId()); + + // TODO ResponseEntity 커스텀 공통 양식 추가 + return ResponseEntity.status(HttpStatus.CREATED).body(eventId); + } + + /** + * 일정 수정 + */ + @PatchMapping("/{eventId}") + public ResponseEntity updateEvent( + @PathVariable Long eventId, + @RequestBody EventUpdateReqDto reqDto, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + eventService.updateEvent(eventId, reqDto, userDetails.getId()); + return ResponseEntity.status(HttpStatus.OK).body(eventId); + } + + /** + * 월간 일정 조회 + */ + @GetMapping("/monthly") + public ResponseEntity getMonthlyEvents( + @RequestParam Long orgId, + @RequestParam int year, + @RequestParam int month, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + List events = eventService.getMonthlyEvents(orgId, year, month, userDetails.getId()); + return ResponseEntity.status(HttpStatus.OK).body(events); + } + + /** + * 다가오는 일정(사이드바) 조회 + */ + @GetMapping("/upcoming") + public ResponseEntity getUpcomingEvents( + @RequestParam Long orgId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + List events = eventService.getUpcomingEvents(orgId, userDetails.getId()); + return ResponseEntity.status(HttpStatus.OK).body(events); + } + + /** + * 일정 상세 조회 + */ + @GetMapping("/{eventId:[0-9]+}") + public ResponseEntity getEventDetail( + @PathVariable Long eventId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + EventDetailResDto detail = eventService.getEventDetail(eventId, userDetails.getId()); + return ResponseEntity.status(HttpStatus.OK).body(detail); + } + + /** + * 일정 삭제 + */ + @DeleteMapping("/{eventId}") + public ResponseEntity deleteEvent( + @PathVariable Long eventId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + if (userDetails == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); + } + + eventService.deleteEvent(eventId, userDetails.getId()); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java new file mode 100644 index 0000000..3f49ec4 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java @@ -0,0 +1,39 @@ +package org.example.tackit.domain.event.dto; + +import jakarta.validation.constraints.*; +import lombok.*; +import org.example.tackit.domain.entity.EventScope; +import org.example.tackit.domain.entity.Org.OrgType; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class EventCreateReqDto { + + @NotNull(message = "조직 ID는 필수입니다.") + private Long orgId; + + @NotBlank(message = "일정 제목은 필수입니다.") + private String title; + + @NotNull(message = "시작 시간은 필수입니다.") + private LocalDateTime startsAt; + + @NotNull(message = "종료 시간은 필수입니다.") + private LocalDateTime endsAt; + + private String description; + + @NotNull(message = "참여자 범위(eventScope)는 필수입니다.") + private EventScope eventScope; + + @NotNull(message = "참여자 목록 필드는 필수입니다.") + private List participants; + + @NotBlank(message = "색상 코드는 필수입니다.") + private String colorChip; +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java new file mode 100644 index 0000000..3a9e787 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java @@ -0,0 +1,22 @@ +package org.example.tackit.domain.event.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EventDetailResDto { + + private Long eventId; + private String title; + private LocalDateTime startsAt; + private LocalDateTime endsAt; + private String description; + private String colorChip; + private List participants; +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java new file mode 100644 index 0000000..8461f46 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java @@ -0,0 +1,14 @@ +package org.example.tackit.domain.event.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EventParticipantDto { + private Long orgMemberId; + private String profileImageUrl; + private String nickname; +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java new file mode 100644 index 0000000..928c8b0 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java @@ -0,0 +1,16 @@ +package org.example.tackit.domain.event.dto; + +import lombok.*; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EventSimpleResDto { + private Long eventId; + private String title; + private LocalDateTime startsAt; + private LocalDateTime endsAt; + private String colorChip; +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventUpdateReqDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventUpdateReqDto.java new file mode 100644 index 0000000..df14164 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/dto/EventUpdateReqDto.java @@ -0,0 +1,21 @@ +package org.example.tackit.domain.event.dto; + +import lombok.*; +import org.example.tackit.domain.entity.EventScope; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class EventUpdateReqDto { + private String title; + private LocalDateTime startsAt; + private LocalDateTime endsAt; + private String description; + private EventScope eventScope; + private List participants; + private String colorChip; +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java b/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java new file mode 100644 index 0000000..e6c81e0 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java @@ -0,0 +1,30 @@ +package org.example.tackit.domain.event.repository; + +import org.example.tackit.domain.entity.Event; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface EventRepository extends JpaRepository { + + // 지정한 날짜 사이의 일정 조회 + @Query("SELECT e FROM Event e " + + "WHERE e.organization.id = :orgId " + // 조직 ID 조건 추가 + "AND e.startsAt <= :endDateTime AND e.endsAt >= :startDateTime " + + "ORDER BY e.startsAt ASC") + List findAllByOrganizationIdAndDateRange( + @Param("orgId") Long orgId, + @Param("startDateTime") LocalDateTime startDateTime, + @Param("endDateTime") LocalDateTime endDateTime + ); + + // 현재 시간 이후의 일정을 시작 시간 오름차순 조회 + List findByOrganizationIdAndStartsAtAfterOrderByStartsAtAsc( + Long orgId, + LocalDateTime now + ); +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/service/EventService.java b/src/main/java/org/example/tackit/domain/event/service/EventService.java new file mode 100644 index 0000000..e7129bf --- /dev/null +++ b/src/main/java/org/example/tackit/domain/event/service/EventService.java @@ -0,0 +1,221 @@ +package org.example.tackit.domain.event.service; + +import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.Organization.repository.OrganizationRepository; +import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; +import org.example.tackit.domain.auth.login.repository.MemberRepository; +import org.example.tackit.domain.entity.*; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Org.OrgStatus; +import org.example.tackit.domain.entity.Org.Organization; +import org.example.tackit.domain.event.dto.*; +import org.example.tackit.domain.event.repository.EventRepository; +import org.example.tackit.global.exception.ErrorCode; +import org.example.tackit.global.exception.MemberNotFoundException; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class EventService { + + private final EventRepository eventRepository; + private final OrganizationRepository organizationRepository; + private final MemberOrgRepository memberOrgRepository; + + // 일정 생성 + @Transactional + public Long createEvent(EventCreateReqDto reqDto, Long requesterId) { + MemberOrg memberOrg = validateExecutive(reqDto.getOrgId(), requesterId); + + Member creator = memberOrg.getMember(); + + Organization organization = organizationRepository.findById(reqDto.getOrgId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 조직입니다.")); + + Event event = Event.builder() + .organization(organization) + .creator(creator) + .title(reqDto.getTitle()) + .startsAt(reqDto.getStartsAt()) + .endsAt(reqDto.getEndsAt()) + .description(reqDto.getDescription()) + .colorChip(reqDto.getColorChip()) + .eventScope(reqDto.getEventScope()) + .build(); + + // 참여자 추가 + addParticipants(event, reqDto.getParticipants()); + + return eventRepository.save(event).getId(); + } + + // 일정 수정 + @Transactional + public void updateEvent(Long eventId, EventUpdateReqDto reqDto, Long requesterId) { + Event event = findEventOrThrow(eventId); + + validateExecutive(event.getOrganization().getId(), requesterId); + + event.update( + reqDto.getTitle(), + reqDto.getStartsAt(), + reqDto.getEndsAt(), + reqDto.getDescription(), + reqDto.getColorChip(), + reqDto.getEventScope() + ); + + // 참여자 목록 수정 (기존의 데이터 전부 삭제 후 다시 추가) + if (reqDto.getParticipants() != null) { + event.clearParticipants(); + addParticipants(event, reqDto.getParticipants()); + } + } + + // 일정 삭제 + @Transactional + public void deleteEvent(Long eventId, Long requesterId) { + Event event = findEventOrThrow(eventId); + + validateExecutive(event.getOrganization().getId(), requesterId); + + eventRepository.delete(event); + } + + // 월간 일정 조회 + public List getMonthlyEvents(Long orgId, int year, int month, Long requesterId) { + validateMembership(orgId, requesterId); + + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDateTime = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDateTime = yearMonth.atEndOfMonth().atTime(LocalTime.MAX); + + List events = eventRepository.findAllByOrganizationIdAndDateRange(orgId, startDateTime, endDateTime); + + return events.stream() + .map(event -> EventSimpleResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .colorChip(event.getColorChip()) + .build()) + .collect(Collectors.toList()); + } + + // 일정 상세 조회 + public EventDetailResDto getEventDetail(Long eventId, Long requesterId) { + Event event = findEventOrThrow(eventId); + + validateMembership(event.getOrganization().getId(), requesterId); + + List participantDtos = event.getParticipants().stream() + .map(ep -> EventParticipantDto.builder() + .orgMemberId(ep.getMemberOrg().getId()) + .profileImageUrl(ep.getMemberOrg().getProfileImageUrl()) + .nickname(ep.getMemberOrg().getNickname()) + .build()) + .collect(Collectors.toList()); + + return EventDetailResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .description(event.getDescription()) + .colorChip(event.getColorChip()) + .participants(participantDtos) + .build(); + } + + // 다가오는 일정 조회 + public List getUpcomingEvents(Long orgId, Long requesterId) { + validateMembership(orgId, requesterId); + + List events = eventRepository.findByOrganizationIdAndStartsAtAfterOrderByStartsAtAsc( + orgId, + LocalDateTime.now() + ); + + return events.stream() + .map(event -> EventSimpleResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .colorChip(event.getColorChip()) + .build()) + .collect(Collectors.toList()); + } + + // 이벤트 참가자 추가 메서드 + private void addParticipants(Event event, List memberOrgIds) { + if (memberOrgIds == null || memberOrgIds.isEmpty()) return; + + List memberOrgs = memberOrgRepository.findAllById(memberOrgIds); + + // 개수 검증 + if (memberOrgs.size() != memberOrgIds.size()) { + throw new IllegalArgumentException("존재하지 않는 부원 ID가 포함되어 있습니다."); + } + + for (MemberOrg memberOrg : memberOrgs) { + // 소속 동아리 일치 여부 검증 + if (!memberOrg.getOrganization().getId().equals(event.getOrganization().getId())) { + throw new IllegalArgumentException("해당 동아리의 소속 부원이 아닙니다."); + } + + // 참여자 생성 + EventParticipant participant = EventParticipant.builder() + .event(event) + .memberOrg(memberOrg) + .build(); + + participant.assignEvent(event); + } + } + + // 이벤트 존재 확인 메서드 + private Event findEventOrThrow(Long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 일정입니다.")); + } + + // 활동 중인 멤버(운영진 포함)인지 확인하는 메서드 + private void validateMembership(Long orgId, Long memberId) { + boolean isActiveMember = memberOrgRepository.existsByMemberIdAndOrganizationIdAndOrgStatus( + memberId, + orgId, + OrgStatus.ACTIVE + ); + + if (!isActiveMember) { + throw new IllegalArgumentException("해당 조직의 활동 중인 회원만 접근할 수 있습니다."); + } + } + + // 활동 중인 운영진인지 확인하는 메서드 + private MemberOrg validateExecutive(Long orgId, Long memberId) { + MemberOrg memberOrg = memberOrgRepository.findByMemberIdAndOrganizationIdAndOrgStatus( + memberId, + orgId, + OrgStatus.ACTIVE + ).orElseThrow(() -> new IllegalArgumentException("해당 조직의 활동 중인 회원만 접근할 수 있습니다.")); + + if (memberOrg.getMemberRole() != MemberRole.EXECUTIVE) { + throw new IllegalArgumentException("해당 조직의 운영진만 일정을 관리할 수 있습니다."); + } + + return memberOrg; + } +}