diff --git a/src/main/java/com/runningmate/backend/club/Club.java b/src/main/java/com/runningmate/backend/club/Club.java index 4fa97c0..b0489e5 100644 --- a/src/main/java/com/runningmate/backend/club/Club.java +++ b/src/main/java/com/runningmate/backend/club/Club.java @@ -1,5 +1,6 @@ package com.runningmate.backend.club; +import com.runningmate.backend.entity.BaseTimeEntity; import com.runningmate.backend.member.Member; import jakarta.persistence.*; import lombok.*; @@ -7,14 +8,13 @@ import java.util.*; -//게시판, 일정, 멤버(클럽장, 부클럽장등등 역할), 순위, 채팅방, 위치, 배경사진, 클럽 사진, @Entity @Getter @Table(name = "club") @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class Club { +public class Club extends BaseTimeEntity { public static final String DEFAULT_BACKGROUND_PIC = "https://storage.googleapis.com/runningmate-bucket/Screenshot%202024-06-20%20at%203.46.14%E2%80%AFPM.png"; @Id @@ -23,7 +23,8 @@ public class Club { private UUID id; private String title; //TODO: updateTitle method - private String detail; //TODO: updateDetail method + + private String description; //TODO: updateDetail method @Column(columnDefinition = "geography(Point, 4326)") private Point location; @@ -38,7 +39,7 @@ public class Club { @OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default - private Set members = new HashSet<>(); + private List members = new ArrayList<>(); // @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) // @Builder.Default @@ -46,7 +47,7 @@ public class Club { @OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default - private List clubs = new ArrayList<>(); + private List schedules = new ArrayList<>(); public void updateProfilePic(String url) { this.profile_pic = url; @@ -55,4 +56,18 @@ public void updateProfilePic(String url) { public void updateBackgroundPic(String url) { this.background_pic = url; } + + public void addMember(ClubMemberEntity clubMemberEntity) { + this.members.add(clubMemberEntity); + } + + public void removeMember(ClubMemberEntity clubMemberEntity) { this.members.remove(clubMemberEntity);} + + public void addClubSchedule(ClubScheduleEntity clubScheduleEntity) { this.schedules.add(clubScheduleEntity);} + + public void removeClubSchedule(ClubScheduleEntity clubScheduleEntity) { this.schedules.remove(clubScheduleEntity);} + + public void updateTitle(String title) { this.title = title;} + + public void updateDescription(String description) { this.description = description;} } diff --git a/src/main/java/com/runningmate/backend/club/ClubMemberEntity.java b/src/main/java/com/runningmate/backend/club/ClubMemberEntity.java index 1eff079..066b7e4 100644 --- a/src/main/java/com/runningmate/backend/club/ClubMemberEntity.java +++ b/src/main/java/com/runningmate/backend/club/ClubMemberEntity.java @@ -28,6 +28,8 @@ public class ClubMemberEntity { @Builder.Default private ClubRole role = ClubRole.MEMBER; - + public void changeRole(ClubRole clubRole) { + this.role = clubRole; + } } diff --git a/src/main/java/com/runningmate/backend/club/ClubScheduleEntity.java b/src/main/java/com/runningmate/backend/club/ClubScheduleEntity.java index dbcfca5..e0f8880 100644 --- a/src/main/java/com/runningmate/backend/club/ClubScheduleEntity.java +++ b/src/main/java/com/runningmate/backend/club/ClubScheduleEntity.java @@ -1,6 +1,6 @@ package com.runningmate.backend.club; -import com.runningmate.backend.member.Member; +import com.runningmate.backend.club.dto.CreateOrUpdateClubScheduleRequestDto; import com.runningmate.backend.schedule.Schedule; import jakarta.persistence.*; import lombok.*; @@ -24,4 +24,8 @@ public class ClubScheduleEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "club_id") private Club club; + + public void update(CreateOrUpdateClubScheduleRequestDto updateDto) { + this.schedule.update(updateDto.getCreateOrUpdateScheduleRequestDto()); + } } diff --git a/src/main/java/com/runningmate/backend/club/controller/ClubController.java b/src/main/java/com/runningmate/backend/club/controller/ClubController.java new file mode 100644 index 0000000..0c11f09 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/controller/ClubController.java @@ -0,0 +1,134 @@ +package com.runningmate.backend.club.controller; + +import com.runningmate.backend.club.dto.*; +import com.runningmate.backend.club.service.ClubService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/club") +@RequiredArgsConstructor +public class ClubController { + /*TODO: join club(check already in), leave club(owner can't leave), delete club, change owner, + remove member, update title/description, get club info, create new schedule, update schedule + */ + private final ClubService clubService; + + @PostMapping + @ResponseStatus(value = HttpStatus.CREATED) + public ClubResponseDto createNewClub(@RequestBody ClubRequestDto clubRequestDto, + @AuthenticationPrincipal UserDetails userDetails) { + ClubResponseDto clubResponseDto = clubService.createClub(clubRequestDto, userDetails.getUsername()); + return clubResponseDto; + } + + @GetMapping("/{clubId}") + @ResponseStatus(value = HttpStatus.OK) + public ClubResponseDto getClub(@PathVariable(name = "clubId") UUID clubId, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.getClub(clubId); + } + + @GetMapping("/{clubId}/schedules") + @ResponseStatus(value = HttpStatus.OK) + public ListClubScheduleEntityResponseDto getClubSchedules(@PathVariable(name = "clubId") UUID clubId, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.getClubSchedules(clubId, userDetails.getUsername()); + } + + @PostMapping("/{clubId}/schedules") + @ResponseStatus(HttpStatus.OK) + public ClubScheduleEntityResponseDto createClubSchedule(@PathVariable(name = "clubId") UUID clubId, + @Valid @RequestBody CreateOrUpdateClubScheduleRequestDto requestDto, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.createClubSchedule(requestDto, clubId, userDetails.getUsername()); + } + + @PutMapping("/{clubId}/schedules/{scheduleId}") + @ResponseStatus(HttpStatus.OK) + public ClubScheduleEntityResponseDto updateClubSchedule(@PathVariable(name = "clubId") UUID clubId, + @Valid @RequestBody CreateOrUpdateClubScheduleRequestDto requestDto, + @PathVariable(name = "scheduleId") Long scheduleId, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.updateClubSchedule(requestDto, clubId, scheduleId, userDetails.getUsername()); + } + + @DeleteMapping("/{clubId}/schedules/{scheduleId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeClubSchedule(@PathVariable(name = "clubId") UUID clubId, + @PathVariable(name = "scheduleId") Long scheduleId, + @AuthenticationPrincipal UserDetails userDetails) { + clubService.removeClubSchedule(clubId, scheduleId, userDetails.getUsername()); + } + + @DeleteMapping("/{clubId}/delete") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void removeClub(@PathVariable(name = "clubId") UUID clubId, + @AuthenticationPrincipal UserDetails userDetails) { + clubService.removeClub(clubId, userDetails.getUsername()); + } + + @PutMapping("/{clubId}/members/{memberId}/role") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void changeMemberRole(@PathVariable(name = "clubId") UUID clubId, + @PathVariable(name = "memberId") Long memberId, + @Valid @RequestBody ChangeRoleRequestDto changeRoleDto, + @AuthenticationPrincipal UserDetails userDetails) { + clubService.changeMemberRole(changeRoleDto, clubId, memberId, userDetails.getUsername()); + } + + @DeleteMapping("/{clubId}/members/{membersId}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void removeMemberFromClub(@PathVariable(name = "clubId") UUID clubId, + @PathVariable(name = "memberId") Long memberId, + @AuthenticationPrincipal UserDetails userDetails) { + clubService.removeMemberFromClub(clubId, memberId, userDetails.getUsername()); + } + + @PostMapping("/{clubId}/join") + @ResponseStatus(value = HttpStatus.CREATED) + public ClubResponseDto joinClub(@PathVariable(name = "clubId") UUID clubId, + @AuthenticationPrincipal UserDetails userDetails) { + ClubResponseDto clubResponseDto = clubService.addUserToClub(clubId, userDetails.getUsername()); + return clubResponseDto; + } + + @DeleteMapping("/{clubId}/leave") + @ResponseStatus(value = HttpStatus.OK) + public ClubResponseDto leaveClub(@PathVariable(name = "clubId") UUID clubId, + @AuthenticationPrincipal UserDetails userDetails) { + ClubResponseDto clubResponseDto = clubService.removeSelfFromClub(clubId, userDetails.getUsername()); + return clubResponseDto; + } + + @PutMapping("/{clubId}/title-description") + @ResponseStatus(HttpStatus.OK) + public ClubResponseDto updateTitleAndDescription(@PathVariable(name = "clubId") UUID clubId, + @Valid @RequestBody UpdateTitleDescriptionRequestDto updateDto, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.updateTitleAndDescription(clubId, updateDto, userDetails.getUsername()); + } + + + @PutMapping("/{clubId}/profile-pic") + @ResponseStatus(HttpStatus.OK) + public ClubResponseDto updateProfilePic(@PathVariable(name = "clubId") UUID clubId, + @Valid @RequestBody UploadImageRequestDto newImageDto, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.updateProfilePic(clubId, newImageDto.getNewImageUrl(), userDetails.getUsername()); + } + + @PutMapping("/{clubId}/background-pic") + @ResponseStatus(HttpStatus.OK) + public ClubResponseDto updateBackgroundPic(@PathVariable(name = "clubId") UUID clubId, + @Valid @RequestBody UploadImageRequestDto newImageDto, + @AuthenticationPrincipal UserDetails userDetails) { + return clubService.updateBackgroundPic(clubId, newImageDto.getNewImageUrl(), userDetails.getUsername()); + } +} diff --git a/src/main/java/com/runningmate/backend/club/dto/ChangeRoleRequestDto.java b/src/main/java/com/runningmate/backend/club/dto/ChangeRoleRequestDto.java new file mode 100644 index 0000000..3715b42 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/ChangeRoleRequestDto.java @@ -0,0 +1,16 @@ +package com.runningmate.backend.club.dto; + +import com.runningmate.backend.club.ClubRole; +import com.runningmate.backend.validation.ValidEnum; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ChangeRoleRequestDto { + @NotNull(message = "New role must not be null") + @ValidEnum(enumClass = ClubRole.class, message = "Invalid role. Allowed roles are MEMBER, OWNER, MODERATOR.") + private ClubRole clubRole; +} diff --git a/src/main/java/com/runningmate/backend/club/dto/ClubRequestDto.java b/src/main/java/com/runningmate/backend/club/dto/ClubRequestDto.java new file mode 100644 index 0000000..b3e8dbe --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/ClubRequestDto.java @@ -0,0 +1,36 @@ +package com.runningmate.backend.club.dto; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.runningmate.backend.club.Club; +import com.runningmate.backend.route.dto.CoordinateDto; +import com.runningmate.backend.utils.PointCreator; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.locationtech.jts.geom.Point; + +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ClubRequestDto { + @NotEmpty(message = "Title must not be empty or null") + @Size(max = 100, message = "Title must not exceed 100 characters") + private String title; + + @NotEmpty(message = "Detail must not be empty or null") + @Size(max = 500, message = "Detail must not exceed 500 characters") + private String description; + + @NotNull(message = "Location must not be null") + private CoordinateDto location; + + public Club toEntity() { + return Club.builder() + .title(this.title) + .description(this.description) + .location(PointCreator.createPoint(this.location.getLongitude(), this.location.getLatitude())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/runningmate/backend/club/dto/ClubResponseDto.java b/src/main/java/com/runningmate/backend/club/dto/ClubResponseDto.java new file mode 100644 index 0000000..2cfa26d --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/ClubResponseDto.java @@ -0,0 +1,48 @@ +package com.runningmate.backend.club.dto; + +import com.runningmate.backend.club.Club; +import com.runningmate.backend.club.ClubMemberEntity; +import com.runningmate.backend.member.dto.MemberDto; +import com.runningmate.backend.route.dto.CoordinateDto; +import com.runningmate.backend.schedule.Schedule; +import com.runningmate.backend.utils.PointCreator; +import lombok.*; +import org.locationtech.jts.geom.Point; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ClubResponseDto { + private UUID id; + private String title; + private String description; + private CoordinateDto location; + private String profilePic; + private String backgroundPic; + private List clubMembers; + private List schedules; + + public static ClubResponseDto fromEntity(Club club) { + return ClubResponseDto.builder() + .id(club.getId()) + .title(club.getTitle()) + .description(club.getDescription()) + .location(PointCreator.toCoordinateDto(club.getLocation())) + .profilePic(club.getProfile_pic()) + .backgroundPic(club.getBackground_pic()) + .clubMembers(club.getMembers().stream() + .map(ClubMemberEntity::getMember) + .map(MemberDto::fromEntity) + .collect(Collectors.toList())) + .schedules(club.getSchedules().stream() + .map(clubScheduleEntity -> ClubScheduleEntityResponseDto.fromEntity(clubScheduleEntity)) + .collect(Collectors.toList())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/runningmate/backend/club/dto/ClubScheduleEntityResponseDto.java b/src/main/java/com/runningmate/backend/club/dto/ClubScheduleEntityResponseDto.java new file mode 100644 index 0000000..6dcb70c --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/ClubScheduleEntityResponseDto.java @@ -0,0 +1,25 @@ +package com.runningmate.backend.club.dto; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.runningmate.backend.club.ClubScheduleEntity; +import com.runningmate.backend.schedule.dto.ScheduleResponseDto; +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ClubScheduleEntityResponseDto { + private Long id; + @JsonUnwrapped + private ScheduleResponseDto schedule; + + public static ClubScheduleEntityResponseDto fromEntity(ClubScheduleEntity clubScheduleEntity) { + return ClubScheduleEntityResponseDto.builder() + .id(clubScheduleEntity.getId()) + .schedule(ScheduleResponseDto.fromEntity(clubScheduleEntity.getSchedule())) + .build(); + + } +} diff --git a/src/main/java/com/runningmate/backend/club/dto/CreateOrUpdateClubScheduleRequestDto.java b/src/main/java/com/runningmate/backend/club/dto/CreateOrUpdateClubScheduleRequestDto.java new file mode 100644 index 0000000..7d457f7 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/CreateOrUpdateClubScheduleRequestDto.java @@ -0,0 +1,13 @@ +package com.runningmate.backend.club.dto; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.runningmate.backend.schedule.dto.CreateOrUpdateScheduleRequestDto; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CreateOrUpdateClubScheduleRequestDto{ + @JsonUnwrapped + private CreateOrUpdateScheduleRequestDto createOrUpdateScheduleRequestDto; +} diff --git a/src/main/java/com/runningmate/backend/club/dto/ListClubScheduleEntityResponseDto.java b/src/main/java/com/runningmate/backend/club/dto/ListClubScheduleEntityResponseDto.java new file mode 100644 index 0000000..0fec795 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/ListClubScheduleEntityResponseDto.java @@ -0,0 +1,14 @@ +package com.runningmate.backend.club.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ListClubScheduleEntityResponseDto { + private List clubScheduleEntityResponseDtos; +} diff --git a/src/main/java/com/runningmate/backend/club/dto/UpdateTitleDescriptionRequestDto.java b/src/main/java/com/runningmate/backend/club/dto/UpdateTitleDescriptionRequestDto.java new file mode 100644 index 0000000..0dcda75 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/UpdateTitleDescriptionRequestDto.java @@ -0,0 +1,18 @@ +package com.runningmate.backend.club.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UpdateTitleDescriptionRequestDto { + @NotEmpty(message = "Title must not be empty or null") + @Size(max = 100, message = "Title must not exceed 100 characters") + private String title; + + @NotEmpty(message = "Detail must not be empty or null") + @Size(max = 500, message = "Detail must not exceed 500 characters") + private String description; +} diff --git a/src/main/java/com/runningmate/backend/club/dto/UploadImageRequestDto.java b/src/main/java/com/runningmate/backend/club/dto/UploadImageRequestDto.java new file mode 100644 index 0000000..059df78 --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/dto/UploadImageRequestDto.java @@ -0,0 +1,19 @@ +package com.runningmate.backend.club.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UploadImageRequestDto { + + @Size(max = 2000, message = "The URL must not exceed 2000 characters") + @NotEmpty(message = "The URL must not be empty") + private String newImageUrl; +} diff --git a/src/main/java/com/runningmate/backend/club/repository/ClubMemberEntityRepository.java b/src/main/java/com/runningmate/backend/club/repository/ClubMemberEntityRepository.java index 4fd1ff7..c0127e8 100644 --- a/src/main/java/com/runningmate/backend/club/repository/ClubMemberEntityRepository.java +++ b/src/main/java/com/runningmate/backend/club/repository/ClubMemberEntityRepository.java @@ -1,7 +1,12 @@ package com.runningmate.backend.club.repository; +import com.runningmate.backend.club.Club; import com.runningmate.backend.club.ClubMemberEntity; +import com.runningmate.backend.member.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ClubMemberEntityRepository extends JpaRepository { + Optional findByClubAndMember(Club club, Member member); } diff --git a/src/main/java/com/runningmate/backend/club/repository/ClubScheduleEntityRepository.java b/src/main/java/com/runningmate/backend/club/repository/ClubScheduleEntityRepository.java index a875d42..deeb794 100644 --- a/src/main/java/com/runningmate/backend/club/repository/ClubScheduleEntityRepository.java +++ b/src/main/java/com/runningmate/backend/club/repository/ClubScheduleEntityRepository.java @@ -4,5 +4,9 @@ import com.runningmate.backend.club.ClubScheduleEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.UUID; + public interface ClubScheduleEntityRepository extends JpaRepository { + List findByClubId(UUID clubId); } diff --git a/src/main/java/com/runningmate/backend/club/service/ClubScheduleEntityService.java b/src/main/java/com/runningmate/backend/club/service/ClubScheduleEntityService.java index f27b1d1..71ab578 100644 --- a/src/main/java/com/runningmate/backend/club/service/ClubScheduleEntityService.java +++ b/src/main/java/com/runningmate/backend/club/service/ClubScheduleEntityService.java @@ -2,23 +2,36 @@ import com.runningmate.backend.club.Club; import com.runningmate.backend.club.ClubScheduleEntity; +import com.runningmate.backend.club.dto.CreateOrUpdateClubScheduleRequestDto; import com.runningmate.backend.club.repository.ClubScheduleEntityRepository; import com.runningmate.backend.schedule.Schedule; +import com.runningmate.backend.schedule.dto.CreateOrUpdateScheduleRequestDto; +import com.runningmate.backend.schedule.repository.ScheduleRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +import java.util.UUID; @Service @RequiredArgsConstructor public class ClubScheduleEntityService { private final ClubScheduleEntityRepository clubScheduleEntityRepository; + private final ScheduleRepository scheduleRepository; @Transactional - public ClubScheduleEntity createClubSchedule(Club club, Schedule schedule) { + public ClubScheduleEntity createClubSchedule(Club club, CreateOrUpdateClubScheduleRequestDto clubScheduleDto) { + CreateOrUpdateScheduleRequestDto scheduleDto = clubScheduleDto.getCreateOrUpdateScheduleRequestDto(); + Schedule schedule = scheduleRepository.save(Schedule.builder() + .name(scheduleDto.getTitle()) + .description(scheduleDto.getDescription()) + .dateTime(scheduleDto.getDateTime()) + .location(scheduleDto.getLocation()) + .build()); + ClubScheduleEntity clubScheduleEntity = ClubScheduleEntity.builder() .club(club) .schedule(schedule) @@ -27,12 +40,12 @@ public ClubScheduleEntity createClubSchedule(Club club, Schedule schedule) { } @Transactional(readOnly = true) - public Optional getClubSchedule(Long id) { + public Optional getClubScheduleById(Long id) { return clubScheduleEntityRepository.findById(id); } @Transactional(readOnly = true) - public List getClubSchedules(Long clubId) { + public List getClubSchedules(UUID clubId) { return clubScheduleEntityRepository.findByClubId(clubId); } diff --git a/src/main/java/com/runningmate/backend/club/service/ClubService.java b/src/main/java/com/runningmate/backend/club/service/ClubService.java new file mode 100644 index 0000000..3ad1d5a --- /dev/null +++ b/src/main/java/com/runningmate/backend/club/service/ClubService.java @@ -0,0 +1,275 @@ +package com.runningmate.backend.club.service; + +import com.runningmate.backend.club.Club; +import com.runningmate.backend.club.ClubMemberEntity; +import com.runningmate.backend.club.ClubRole; +import com.runningmate.backend.club.ClubScheduleEntity; +import com.runningmate.backend.club.dto.*; +import com.runningmate.backend.club.repository.ClubMemberEntityRepository; +import com.runningmate.backend.club.repository.ClubRepository; +import com.runningmate.backend.exception.BadRequestException; +import com.runningmate.backend.exception.ExistsConflictException; +import com.runningmate.backend.exception.NoPermissionException; +import com.runningmate.backend.exception.ResourceNotFoundException; +import com.runningmate.backend.member.Member; +import com.runningmate.backend.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ClubService { + + private final ClubRepository clubRepository; + private final ClubScheduleEntityService clubScheduleEntityService; + private final ClubMemberEntityRepository clubMemberEntityRepository; + private final MemberService memberService; + + @Transactional + public ClubResponseDto createClub(ClubRequestDto clubRequestDto, String username) { + //TODO: Discuss adding maximum clubs one can create + Member member = memberService.getMemberByUsername(username); + Club club = clubRepository.save(clubRequestDto.toEntity()); + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.save(createClubMemberEntity(club, member, ClubRole.OWNER)); + club.addMember(clubMemberEntity); + + return ClubResponseDto.fromEntity(club); + } + + @Transactional + public ClubResponseDto getClub(UUID clubId) { + Club club = clubRepository.findById(clubId).orElseThrow(() -> new ResourceNotFoundException("Club not found")); + return ClubResponseDto.fromEntity(club); + } + + @Transactional + public ListClubScheduleEntityResponseDto getClubSchedules(UUID clubId, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + //Check if the user is in the club + clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You must first join the club to view club schedules")); + + List clubScheduleEntities = clubScheduleEntityService.getClubSchedules(clubId); + List clubSchedules = clubScheduleEntities.stream() + .map(clubScheduleEntity -> ClubScheduleEntityResponseDto.fromEntity(clubScheduleEntity)) + .collect(Collectors.toList()); + return new ListClubScheduleEntityResponseDto(clubSchedules); + } + + @Transactional + public ClubScheduleEntityResponseDto createClubSchedule(CreateOrUpdateClubScheduleRequestDto createOrUpdateClubScheduleRequestDto, + UUID clubId, + String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + //Check if the user is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You are not a member of this club")); + //Check if user is the owner of the club + if (clubMemberEntity.getRole() != ClubRole.OWNER && clubMemberEntity.getRole() != ClubRole.MODERATOR) { + throw new NoPermissionException("You do not have permission to create a schedule at this club"); + } + ClubScheduleEntity clubScheduleEntity = clubScheduleEntityService.createClubSchedule(club, createOrUpdateClubScheduleRequestDto); + return ClubScheduleEntityResponseDto.fromEntity(clubScheduleEntity); + } + + @Transactional + public ClubScheduleEntityResponseDto updateClubSchedule(CreateOrUpdateClubScheduleRequestDto createOrUpdateClubScheduleRequestDto, + UUID clubId, + Long scheduleId, + String username) { + ClubScheduleEntity clubScheduleEntity = clubScheduleEntityService.getClubScheduleById(scheduleId) + .orElseThrow(() -> new ResourceNotFoundException("Schedule not found")); + Club club = clubScheduleEntity.getClub(); + //Check whether the schedule is club's + if(!club.getId().equals(clubId)) { + throw new ResourceNotFoundException("Schedule does not belong to the specified club"); + } + Member member = memberService.getMemberByUsername(username); + //Check if the user is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You are not a member of this club")); + //Check if user is the owner of the club + if (clubMemberEntity.getRole() != ClubRole.OWNER && clubMemberEntity.getRole() != ClubRole.MODERATOR) { + throw new NoPermissionException("You do not have permission to create a schedule at this club"); + } + + clubScheduleEntity.update(createOrUpdateClubScheduleRequestDto); + return ClubScheduleEntityResponseDto.fromEntity(clubScheduleEntity); + } + + @Transactional + public void removeClubSchedule(UUID clubId, Long scheduleId, String username) { + ClubScheduleEntity clubScheduleEntity = clubScheduleEntityService.getClubScheduleById(scheduleId) + .orElseThrow(() -> new ResourceNotFoundException("Schedule not found")); + Club club = clubScheduleEntity.getClub(); + //Check whether the schedule is club's + if(!club.getId().equals(clubId)) { + throw new ResourceNotFoundException("Schedule does not belong to the specified club"); + } + Member member = memberService.getMemberByUsername(username); + //Check if the user is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You are not a member of this club")); + //Check if user is the owner of the club + if (clubMemberEntity.getRole() != ClubRole.OWNER && clubMemberEntity.getRole() != ClubRole.MODERATOR) { + throw new NoPermissionException("You do not have permission to create a schedule at this club"); + } + clubScheduleEntityService.deleteClubSchedule(clubScheduleEntity.getId()); + club.removeClubSchedule(clubScheduleEntity); + } + + @Transactional + public void removeClub(UUID clubId, String username) { + Member member = memberService.getMemberByUsername(username); + //Check if club exists + Club club = getClubById(clubId); + //Check if the user is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("Member is not in this club")); + //Check if user is the owner of the club + if (clubMemberEntity.getRole() != ClubRole.OWNER) { + throw new NoPermissionException("Member does not have permission to delete the club"); + } + + clubRepository.delete(club); + } + + @Transactional + public void changeMemberRole(ChangeRoleRequestDto changeRoleRequestDto, UUID clubId, Long memberId, String username) { + Member owner = memberService.getMemberByUsername(username); + Member member = memberService.getMemberById(memberId); + Club club = getClubById(clubId); + //Check if requester is in the club + ClubMemberEntity clubOwnerEntity = clubMemberEntityRepository.findByClubAndMember(club, owner) + .orElseThrow(() -> new ResourceNotFoundException("You do not belong to this club")); + //Check if request is the owner of the club + if(clubOwnerEntity.getRole() != ClubRole.OWNER) { + throw new NoPermissionException("You must be the owner to change roles"); + } + //Check if member is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("Member does not belong to this club")); + + clubMemberEntity.changeRole(changeRoleRequestDto.getClubRole()); + } + + @Transactional + public void removeMemberFromClub(UUID clubId, Long memberId, String username) { + Member owner = memberService.getMemberByUsername(username); + Member member = memberService.getMemberById(memberId); + Club club = getClubById(clubId); + //Check if requester is in the club + ClubMemberEntity clubOwnerEntity = clubMemberEntityRepository.findByClubAndMember(club, owner) + .orElseThrow(() -> new NoPermissionException("You do not belong to this club")); + //Check if request is the owner of the club + if(clubOwnerEntity.getRole() != ClubRole.OWNER && clubOwnerEntity.getRole() != ClubRole.MODERATOR) { + throw new NoPermissionException("You must be the owner to change roles"); + } + //Check if member is in the club. If the member is not return without throwing an exception. + Optional clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member); + if(clubMemberEntity.isEmpty()) { + return; + } + //Can't remove owner + if(clubMemberEntity.get().getRole() == ClubRole.OWNER) { + throw new BadRequestException("You cannot remove owner of the club"); + } + + clubMemberEntityRepository.delete(clubMemberEntity.get()); + } + + @Transactional + public ClubResponseDto addUserToClub(UUID clubId, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + if(clubMemberEntityRepository.findByClubAndMember(club, member).isPresent()) { + throw new ExistsConflictException("Member is already in this club"); + } + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.save(createClubMemberEntity(club, member, ClubRole.MEMBER)); + club.addMember(clubMemberEntity); + + return ClubResponseDto.fromEntity(club); + } + + @Transactional + public ClubResponseDto removeSelfFromClub(UUID clubId, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + //Check if requester is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You do not belong to this club")); + if(clubMemberEntity.getRole() == ClubRole.OWNER) { + throw new BadRequestException("You cannot remove owner of the club"); + } + clubMemberEntityRepository.delete(clubMemberEntity); + club.removeMember(clubMemberEntity); + + return ClubResponseDto.fromEntity(club); + } + + @Transactional + public ClubResponseDto updateTitleAndDescription(UUID clubId, UpdateTitleDescriptionRequestDto updateDto, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + //Check if requester is in the club + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("You do not belong to this club")); + //Check if requester is owner of the club + if(clubMemberEntity.getRole() == ClubRole.OWNER) { + throw new BadRequestException("You cannot remove owner of the club"); + } + club.updateTitle(updateDto.getTitle()); + club.updateDescription(updateDto.getDescription()); + + return ClubResponseDto.fromEntity(club); + } + + @Transactional + public ClubResponseDto updateProfilePic(UUID clubId, String url, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("This club does not have user " + username + " as member.")); + + if (clubMemberEntity.getRole() != ClubRole.OWNER) { + throw new IllegalArgumentException("Only the owner can update the profile picture"); + } + + club.updateProfilePic(url); + return ClubResponseDto.fromEntity(clubRepository.save(club)); + } + + @Transactional + public ClubResponseDto updateBackgroundPic(UUID clubId, String url, String username) { + Member member = memberService.getMemberByUsername(username); + Club club = getClubById(clubId); + ClubMemberEntity clubMemberEntity = clubMemberEntityRepository.findByClubAndMember(club, member) + .orElseThrow(() -> new ResourceNotFoundException("This club does not have user " + username + " as member.")); + + if (clubMemberEntity.getRole() != ClubRole.OWNER) { + throw new IllegalArgumentException("Only the owner can update the background picture"); + } + + club.updateBackgroundPic(url); + return ClubResponseDto.fromEntity(clubRepository.save(club)); + } + + private ClubMemberEntity createClubMemberEntity(Club club, Member member, ClubRole clubRole) { + return ClubMemberEntity.builder().club(club).member(member).role(clubRole).build(); + } + + + + @Transactional(readOnly = true) + private Club getClubById(UUID clubId) { + return clubRepository.findById(clubId).orElseThrow(() -> new ResourceNotFoundException("Club not found")); + } +}