diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java index b2d5e38..a4e0758 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java @@ -2,7 +2,6 @@ import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.user.entity.SportType; -import com.be.sportizebe.domain.user.entity.User; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -13,14 +12,13 @@ public record ClubCreateRequest( @Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType, @Schema(description = "최대 정원", example = "20") Integer maxMembers) { - public Club toEntity(User user, String clubImage) { + public Club toEntity(String clubImage) { return Club.builder() .name(name) .introduce(introduce) .clubType(clubType) .maxMembers(maxMembers) .clubImage(clubImage) - .leader(user) .build(); } } diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index 142c9a4..a62d0ff 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -39,10 +39,6 @@ public class Club extends BaseTimeEntity { private String clubImage; // 동호회 사진 URL - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "leader_id", nullable = false) - private User leader; // 동호회장 - @OneToOne(mappedBy = "club", fetch = FetchType.LAZY) private ChatRoom chatRoom; // 동호회 채팅방 @@ -50,6 +46,33 @@ public class Club extends BaseTimeEntity { @Builder.Default private List members = new ArrayList<>(); + /** + * 동호회장(LEADER) 조회 + * ClubMember에서 LEADER 역할을 가진 멤버를 찾아 반환 + */ + public ClubMember getLeaderMember() { + return members.stream() + .filter(member -> member.getRole() == ClubMember.ClubRole.LEADER) + .findFirst() + .orElse(null); + } + + /** + * 동호회장 User 조회 + */ + public User getLeader() { + ClubMember leaderMember = getLeaderMember(); + return leaderMember != null ? leaderMember.getUser() : null; + } + + /** + * 특정 사용자가 동호회장인지 확인 + */ + public boolean isLeader(Long userId) { + ClubMember leaderMember = getLeaderMember(); + return leaderMember != null && leaderMember.getUser().getId() == userId; + } + public void update(String name, String introduce, Integer maxMembers, SportType clubType) { this.name = name; this.introduce = introduce; diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index 529f9c6..c96d6ae 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -49,7 +49,7 @@ public ClubResponse createClub(ClubCreateRequest request, MultipartFile image, L } // 동호회 엔티티 생성 - Club club = request.toEntity(user, clubImageUrl); + Club club = request.toEntity(clubImageUrl); clubRepository.save(club); // 동호회 멤버 테이블에 방장(동호회 생성자) 추가 @@ -58,6 +58,7 @@ public ClubResponse createClub(ClubCreateRequest request, MultipartFile image, L .user(user) .role(ClubMember.ClubRole.LEADER) .build(); + club.getMembers().add(leaderMember); clubMemberRepository.save(leaderMember); // 동호회 단체 채팅방 생성 @@ -73,7 +74,7 @@ public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, Long user .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); // 동호회 방장만 수정 가능하도록 검증 - if (club.getLeader().getId() != userId) { + if (!club.isLeader(userId)) { throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); } @@ -97,7 +98,7 @@ public ClubImageResponse updateClubImage(Long clubId, MultipartFile image, Long .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); // 동호회 방장만 수정 가능하도록 검증 - if (club.getLeader().getId() != userId) { + if (!club.isLeader(userId)) { throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); } diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java b/src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java new file mode 100644 index 0000000..409cc5b --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java @@ -0,0 +1,54 @@ +package com.be.sportizebe.domain.notification.entity; + +import com.be.sportizebe.domain.club.entity.Club; +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "join_club_request", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "club_id"}) +}) +public class JoinClubRequest extends BaseTimeEntity { + + public enum JoinClubRequestStatus { + PENDING, // 대기 + ACCEPTED, // 승인 + REJECTED // 거절 + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; // 가입 신청자 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "club_id", nullable = false) + private Club club; // 가입 신청 대상 동호회 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private JoinClubRequestStatus status = JoinClubRequestStatus.PENDING; + + // 가입 승인 시 상태 변경 + public void accept() { + this.status = JoinClubRequestStatus.ACCEPTED; + } + + // 가입 거절 시 상태 변경 + public void reject() { + this.status = JoinClubRequestStatus.REJECTED; + } +} diff --git a/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java new file mode 100644 index 0000000..c1c420e --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/notification/entity/Notification.java @@ -0,0 +1,61 @@ +package com.be.sportizebe.domain.notification.entity; + +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "notifications", indexes = { + @Index(name = "idx_notification_receiver", columnList = "receiver_id"), + @Index(name = "idx_notification_is_read", columnList = "receiver_id, is_read") +}) +public class Notification extends BaseTimeEntity { + + public enum NotificationType { + JOIN_REQUEST, // 가입 신청 (동호회장에게) + JOIN_APPROVED, // 가입 승인 (신청자에게) + JOIN_REJECTED, // 가입 거절 (신청자에게) + CHAT, // 새 채팅 메시지 + COMMENT // 새 댓글 + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "receiver_id", nullable = false) + private User receiver; // 알림 수신자 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NotificationType type; + + @Column(nullable = false) + private String message; // 알림 메시지 + + @Column(nullable = false) + @Builder.Default + private Boolean isRead = false; + + // 가입 신청 관련 알림용 (JOIN_REQUEST, JOIN_APPROVED, JOIN_REJECTED) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "join_request_id") + private JoinClubRequest joinClubRequest; + + // 댓글 알림용 - targetId와 targetType으로 다형성 처리 + // COMMENT: postId, CHAT: chatRoomId 등 + private Long targetId; + + public void markAsRead() { + this.isRead = true; + } +} diff --git a/src/main/java/com/be/sportizebe/domain/user/entity/User.java b/src/main/java/com/be/sportizebe/domain/user/entity/User.java index 4d72df5..68c2fc2 100644 --- a/src/main/java/com/be/sportizebe/domain/user/entity/User.java +++ b/src/main/java/com/be/sportizebe/domain/user/entity/User.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.user.entity; +import com.be.sportizebe.domain.club.entity.ClubMember; import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.global.common.BaseTimeEntity; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -62,6 +63,10 @@ public class User extends BaseTimeEntity { @Builder.Default private List posts = new ArrayList<>(); // 작성한 게시글 목록 + @OneToMany(mappedBy = "user") + @Builder.Default + private List clubMemberships = new ArrayList<>(); // 가입한 동호회 목록 + public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }