diff --git a/src/main/java/com/hatcher/haemo/HaemoApplication.java b/src/main/java/com/hatcher/haemo/HaemoApplication.java index cd40083..b9620e4 100644 --- a/src/main/java/com/hatcher/haemo/HaemoApplication.java +++ b/src/main/java/com/hatcher/haemo/HaemoApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class HaemoApplication { diff --git a/src/main/java/com/hatcher/haemo/comment/dto/CommentDto.java b/src/main/java/com/hatcher/haemo/comment/dto/CommentDto.java new file mode 100644 index 0000000..2bf2f46 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/comment/dto/CommentDto.java @@ -0,0 +1,11 @@ +package com.hatcher.haemo.comment.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record CommentDto(Long commentIdx, + String writer, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd HH:mm", timezone = "Asia/Seoul") + LocalDateTime createdDate, + String content) {} diff --git a/src/main/java/com/hatcher/haemo/common/BaseEntity.java b/src/main/java/com/hatcher/haemo/common/BaseEntity.java index 28e54b3..b90f480 100644 --- a/src/main/java/com/hatcher/haemo/common/BaseEntity.java +++ b/src/main/java/com/hatcher/haemo/common/BaseEntity.java @@ -9,7 +9,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDate; +import java.time.LocalDateTime; @Getter @MappedSuperclass @@ -22,8 +22,8 @@ public class BaseEntity { @CreatedDate @Column(updatable = false) - private LocalDate createdDate; + private LocalDateTime createdDate; @LastModifiedDate - private LocalDate lastModifiedDate; + private LocalDateTime lastModifiedDate; } diff --git a/src/main/java/com/hatcher/haemo/common/constants/RequestURI.java b/src/main/java/com/hatcher/haemo/common/constants/RequestURI.java index a94d20a..286cdec 100644 --- a/src/main/java/com/hatcher/haemo/common/constants/RequestURI.java +++ b/src/main/java/com/hatcher/haemo/common/constants/RequestURI.java @@ -3,4 +3,5 @@ public class RequestURI { public final static String user = "/users"; public final static String recruitment = "/recruitments"; + public final static String notification = "/notifications"; } diff --git a/src/main/java/com/hatcher/haemo/common/enums/BaseResponseStatus.java b/src/main/java/com/hatcher/haemo/common/enums/BaseResponseStatus.java index 3edbbfd..30cc4b8 100644 --- a/src/main/java/com/hatcher/haemo/common/enums/BaseResponseStatus.java +++ b/src/main/java/com/hatcher/haemo/common/enums/BaseResponseStatus.java @@ -36,9 +36,25 @@ public enum BaseResponseStatus { // recruitment(2100-2199) WRONG_RECRUIT_TYPE(false, HttpStatus.NOT_FOUND, "해당 Recruit type을 찾을 수 없습니다."), + INVALID_RECRUITMENT_IDX(false, HttpStatus.NOT_FOUND, "해당 recruitment idx로 recruitment를 찾을 수 없습니다."), + NO_RECRUITMENT_LEADER(false, HttpStatus.CONFLICT, "해당 recruitment의 leader가 아닙니다."), + BLANK_RECRUITMENT_NAME(false, HttpStatus.BAD_REQUEST, "recruitment name이 비었습니다."), + BLANK_RECRUITMENT_TYPE(false, HttpStatus.BAD_REQUEST, "recruitment type이 비었습니다."), + BLANK_PARTICIPANT_LIMIT(false, HttpStatus.BAD_REQUEST, "participant limit이 비었습니다."), + BLANK_CONTACT_URL(false, HttpStatus.BAD_REQUEST, "contact url이 비었습니다."), + BLANK_DESCRIPTION(false, HttpStatus.BAD_REQUEST, "description이 비었습니다."), + LARGER_THAN_CURRENT_PARTICIPANT(false, HttpStatus.CONFLICT, "입력하신 모집 인원이 현재 참여 인원보다 작습니다."), + NOT_RECRUITING_STATUS(false, HttpStatus.CONFLICT, "모집중 상태가 아닙니다."), + ALREADY_DONE_RECRUITMENT(false, HttpStatus.CONFLICT, "해당 띱은 이미 모집인원에 도달했습니다."), + LEADER_ROLE(false, HttpStatus.BAD_REQUEST, "리더는 띱 참여가 불가능합니다."), + NOT_LEADER_ROLE(false, HttpStatus.BAD_REQUEST, "해당 띱의 리더가 아닙니다."), + NOT_MEMBER_ROLE(false, HttpStatus.BAD_REQUEST, "해당 띱의 멤버가 아닙니다."), // comment(2200-2299) + // notification(2300-2399) + INVALID_NOTIFICATION_IDX(false, HttpStatus.NOT_FOUND, "해당 notification idx로 notification을 찾을 수 없습니다."), + /** * 3000: Response 오류 diff --git a/src/main/java/com/hatcher/haemo/notification/application/NotificationService.java b/src/main/java/com/hatcher/haemo/notification/application/NotificationService.java new file mode 100644 index 0000000..a0a76c4 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/application/NotificationService.java @@ -0,0 +1,61 @@ +package com.hatcher.haemo.notification.application; + +import com.hatcher.haemo.common.BaseResponse; +import com.hatcher.haemo.common.exception.BaseException; +import com.hatcher.haemo.notification.domain.Notification; +import com.hatcher.haemo.notification.dto.NotificationDto; +import com.hatcher.haemo.notification.dto.NotificationListResponse; +import com.hatcher.haemo.notification.repository.NotificationRepository; +import com.hatcher.haemo.user.application.UserService; +import com.hatcher.haemo.user.domain.User; +import com.hatcher.haemo.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.hatcher.haemo.common.constants.Constant.ACTIVE; +import static com.hatcher.haemo.common.enums.BaseResponseStatus.*; + +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final UserRepository userRepository; + private final UserService userService; + private final NotificationRepository notificationRepository; + + // 알림 목록 조회 + public BaseResponse getNotificationList() throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + List notificationDtoList = notificationRepository.findByUserAndStatusEquals(user, ACTIVE).stream() + .map(notification -> { + long activeParticipantsCount = notification.getRecruitment().getParticipants().stream() + .filter(participant -> participant.getStatus().equals(ACTIVE)).count(); + return new NotificationDto(notification.getNotificationIdx(), notification.getRecruitment().getName(), notification.getRecruitment().getContactUrl(), + notification.getRecruitment().getLeader().getNickname(), activeParticipantsCount+1, + notification.getRecruitment().getParticipantLimit(), notification.getRecruitment().getDescription());}).toList(); + return new BaseResponse<>(new NotificationListResponse(notificationDtoList)); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // 알림 삭제 + public BaseResponse deleteNotification(Long notificationIdx) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Notification notification = notificationRepository.findById(notificationIdx).orElseThrow(() -> new BaseException(INVALID_NOTIFICATION_IDX)); + notificationRepository.delete(notification); + notification.removeNotificationFromUser(user); + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/hatcher/haemo/notification/domain/Notification.java b/src/main/java/com/hatcher/haemo/notification/domain/Notification.java new file mode 100644 index 0000000..a7ee979 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/domain/Notification.java @@ -0,0 +1,46 @@ +package com.hatcher.haemo.notification.domain; + +import com.hatcher.haemo.common.BaseEntity; +import com.hatcher.haemo.recruitment.domain.Recruitment; +import com.hatcher.haemo.user.domain.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; + +@Entity +@Getter +@DynamicInsert +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long notificationIdx; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, name = "user") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, name = "recruitment") + private Recruitment recruitment; + + public void setUser(User user) { + this.user = user; + user.getNotifications().add(this); + } + + public void removeNotificationFromUser(User user) { + this.user = user; + user.getNotifications().remove(this); + } + + @Builder + public Notification(User user, Recruitment recruitment) { + this.user = user; + this.recruitment = recruitment; + } +} diff --git a/src/main/java/com/hatcher/haemo/notification/dto/NotificationDto.java b/src/main/java/com/hatcher/haemo/notification/dto/NotificationDto.java new file mode 100644 index 0000000..ec49429 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/dto/NotificationDto.java @@ -0,0 +1,9 @@ +package com.hatcher.haemo.notification.dto; + +public record NotificationDto(Long notificationIdx, + String name, + String contactUrl, + String leader, + long participantNumber, + Integer participantLimit, + String description) {} diff --git a/src/main/java/com/hatcher/haemo/notification/dto/NotificationListResponse.java b/src/main/java/com/hatcher/haemo/notification/dto/NotificationListResponse.java new file mode 100644 index 0000000..09a23be --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/dto/NotificationListResponse.java @@ -0,0 +1,5 @@ +package com.hatcher.haemo.notification.dto; + +import java.util.List; + +public record NotificationListResponse(List notificationList) {} diff --git a/src/main/java/com/hatcher/haemo/notification/presentation/NotificationController.java b/src/main/java/com/hatcher/haemo/notification/presentation/NotificationController.java new file mode 100644 index 0000000..1df514e --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/presentation/NotificationController.java @@ -0,0 +1,39 @@ +package com.hatcher.haemo.notification.presentation; + +import com.hatcher.haemo.common.BaseResponse; +import com.hatcher.haemo.common.exception.BaseException; +import com.hatcher.haemo.notification.application.NotificationService; +import com.hatcher.haemo.notification.dto.NotificationListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import static com.hatcher.haemo.common.constants.RequestURI.notification; + + +@RestController +@RequiredArgsConstructor +@RequestMapping(notification) +public class NotificationController { + + private final NotificationService notificationService; + + // 알림 목록 조회 + @GetMapping("") + public BaseResponse getNotificationList() { + try { + return notificationService.getNotificationList(); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // 알림 삭제 + @DeleteMapping("/{notificationIdx}") + public BaseResponse deleteNotification(@PathVariable Long notificationIdx) { + try { + return notificationService.deleteNotification(notificationIdx); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } +} diff --git a/src/main/java/com/hatcher/haemo/notification/repository/NotificationRepository.java b/src/main/java/com/hatcher/haemo/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..3a7a7f3 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/notification/repository/NotificationRepository.java @@ -0,0 +1,11 @@ +package com.hatcher.haemo.notification.repository; + +import com.hatcher.haemo.notification.domain.Notification; +import com.hatcher.haemo.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NotificationRepository extends JpaRepository { + List findByUserAndStatusEquals(User user, String status); +} diff --git a/src/main/java/com/hatcher/haemo/recruitment/application/RecruitmentService.java b/src/main/java/com/hatcher/haemo/recruitment/application/RecruitmentService.java index 7fca929..d9e542f 100644 --- a/src/main/java/com/hatcher/haemo/recruitment/application/RecruitmentService.java +++ b/src/main/java/com/hatcher/haemo/recruitment/application/RecruitmentService.java @@ -1,11 +1,18 @@ package com.hatcher.haemo.recruitment.application; +import com.hatcher.haemo.comment.dto.CommentDto; import com.hatcher.haemo.common.BaseResponse; +import com.hatcher.haemo.common.enums.BaseResponseStatus; import com.hatcher.haemo.common.exception.BaseException; import com.hatcher.haemo.common.enums.RecruitType; +import com.hatcher.haemo.notification.domain.Notification; +import com.hatcher.haemo.notification.repository.NotificationRepository; +import com.hatcher.haemo.recruitment.domain.Participant; import com.hatcher.haemo.recruitment.domain.Recruitment; -import com.hatcher.haemo.recruitment.dto.RecruitmentPostRequest; +import com.hatcher.haemo.recruitment.dto.*; +import com.hatcher.haemo.recruitment.repository.ParticipantRepository; import com.hatcher.haemo.recruitment.repository.RecruitmentRepository; +import com.hatcher.haemo.user.application.AuthService; import com.hatcher.haemo.user.application.UserService; import com.hatcher.haemo.user.domain.User; import com.hatcher.haemo.user.repository.UserRepository; @@ -13,7 +20,14 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static com.hatcher.haemo.common.constants.Constant.Recruitment.RECRUITING; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import static com.hatcher.haemo.common.constants.Constant.ACTIVE; +import static com.hatcher.haemo.common.constants.Constant.INACTIVE; +import static com.hatcher.haemo.common.constants.Constant.Recruitment.*; import static com.hatcher.haemo.common.enums.BaseResponseStatus.*; @Service @@ -23,6 +37,9 @@ public class RecruitmentService { private final UserService userService; private final UserRepository userRepository; private final RecruitmentRepository recruitmentRepository; + private final ParticipantRepository participantRepository; + private final NotificationRepository notificationRepository; + private final AuthService authService; // 모집글 등록 @Transactional(rollbackFor = Exception.class) @@ -33,6 +50,138 @@ public BaseResponse postRecruitment(RecruitmentPostRequest recruitmentPo Recruitment recruitment = new Recruitment(recruitmentPostRequest.name(), leader, RecruitType.getEnumByName(recruitmentPostRequest.type()), recruitmentPostRequest.participantLimit(), recruitmentPostRequest.contactUrl(), recruitmentPostRequest.description()); recruitment.setStatus(RECRUITING); + recruitment.setLeader(leader); + recruitmentRepository.save(recruitment); + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // 띱 목록 조회 + public BaseResponse getRecruitmentList(String type, boolean isParticipant) throws BaseException { + try { + Long userIdx = authService.getUserIdx(); + List recruitmentList = new ArrayList<>(); + if (isParticipant) { // 참여중인 띱 목록 조회 //TODO: Participant active 상태인것만 + if (userIdx != null) { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + // 리더로 있는 recruitment 목록 조회 + Stream leaderRecruitmentStream = user.getRecruitments().stream(); + + // 참여자로 있는 recruitment 목록 조회 + Stream participantRecruitmentStream = user.getParticipants().stream() + .map(Participant::getRecruitment); + + List sortedRecruitmentList = Stream.concat(leaderRecruitmentStream, participantRecruitmentStream) + .sorted(Comparator.comparing(Recruitment::getCreatedDate).reversed()) + .map(recruitment -> new RecruitmentDto(recruitment.getRecruitmentIdx(), recruitment.getType().getDescription(), recruitment.getName(), + recruitment.getLeader().getNickname(), recruitment.getParticipants().size(), recruitment.getParticipantLimit(), recruitment.getDescription(), + recruitment.getLeader().equals(user), recruitment.getStatus().equals(DONE))).toList(); + recruitmentList.addAll(sortedRecruitmentList); + } + } else { + if (type == null) { // 모집중인 띱 목록 조회 + recruitmentList = getRecruitmentList(recruitmentRepository.findByStatusOrderByCreatedDateDesc(RECRUITING), userIdx); + } else { // 관심분야 띱 목록 조회 + recruitmentList = getRecruitmentList(recruitmentRepository.findByTypeAndStatusEqualsOrderByCreatedDateDesc(RecruitType.getEnumByName(type), RECRUITING), userIdx); + } + } + return new BaseResponse<>(new RecruitmentListResponse(recruitmentList)); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + private List getRecruitmentList(List recruitmentRepositoryList, Long userIdx) throws BaseException { + List recruitmentList; + User user = null; + if (userIdx != null) { + user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + } + final User finalUser = user; + + recruitmentList = recruitmentRepositoryList.stream() + .map(recruitment -> { + boolean isLeader = false; + if (finalUser != null) { + isLeader = recruitment.getLeader().equals(finalUser); + } + return new RecruitmentDto(recruitment.getRecruitmentIdx(), recruitment.getType().getDescription(), recruitment.getName(), + recruitment.getLeader().getNickname(), recruitment.getParticipants().size(), recruitment.getParticipantLimit(), recruitment.getDescription(), + isLeader, false); + }).toList(); + return recruitmentList; + } + + // 띱 상세 조회 + public BaseResponse getRecruitment(Long recruitmentIdx) throws BaseException { + try { + Long userIdx = authService.getUserIdx(); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + + boolean isLeader = false; + if (userIdx != null) { + User user = userRepository.findByUserIdx(userIdx).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + isLeader = recruitment.getLeader().equals(user); + } + RecruitmentDetailDto recruitmentDetailDto = new RecruitmentDetailDto(recruitment.getRecruitmentIdx(), recruitment.getType().getDescription(), recruitment.getName(), + recruitment.getLeader().getNickname(), recruitment.getParticipants().size()+1, recruitment.getParticipantLimit(), recruitment.getDescription(), + isLeader, recruitment.getStatus().equals(RECRUITING)); //TODO: participantNumber 구할 때 participant 상태가 active인 것만 세기 + Integer commentCount = recruitment.getComments().size(); + List commentList = recruitment.getComments().stream() + .map(comment -> new CommentDto(comment.getCommentIdx(), comment.getWriter().getNickname(), comment.getCreatedDate(), comment.getContent())).toList(); + + RecruitResponse recruitResponse = new RecruitResponse(recruitmentDetailDto, commentCount, commentList); + return new BaseResponse<>(recruitResponse); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // [리더] 띱 수정 + @Transactional(rollbackFor = Exception.class) + public BaseResponse editRecruitment(Long recruitmentIdx, RecruitmentEditRequest recruitmentEditRequest) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + + validateWriter(user, recruitment); + validateRecruitmentStatus(recruitment.getStatus().equals(DONE), NOT_RECRUITING_STATUS); + + if (recruitmentEditRequest.name() != null) { + if (!recruitmentEditRequest.name().equals("") && !recruitmentEditRequest.name().equals(" ")) + recruitment.modifyName(recruitmentEditRequest.name()); + else throw new BaseException(BLANK_RECRUITMENT_NAME); + } + if (recruitmentEditRequest.type() != null) { + if (!recruitmentEditRequest.type().equals("") && !recruitmentEditRequest.type().equals(" ")) + recruitment.modifyType(RecruitType.getEnumByName(recruitmentEditRequest.type())); + else throw new BaseException(BLANK_RECRUITMENT_TYPE); + } + if (recruitmentEditRequest.participantLimit() != null) { + if (recruitmentEditRequest.participantLimit() < recruitment.getParticipants().size()+1) //TODO: participantNumber 구할 때 participant 상태가 active인 것만 세기 + throw new BaseException(LARGER_THAN_CURRENT_PARTICIPANT); + else if (recruitmentEditRequest.participantLimit() == recruitment.getParticipants().size()) { //TODO: participantNumber 구할 때 participant 상태가 active인 것만 세기 + recruitment.setStatus(DONE); + } else recruitment.modifyParticipantLimit(recruitmentEditRequest.participantLimit()); + } + if (recruitmentEditRequest.contactUrl() != null) { + if (!recruitmentEditRequest.contactUrl().equals("") && !recruitmentEditRequest.contactUrl().equals(" ")) + recruitment.modifyContactUrl(recruitmentEditRequest.contactUrl()); + else throw new BaseException(BLANK_CONTACT_URL); + } + if (recruitmentEditRequest.description() != null) { + if (!recruitmentEditRequest.description().equals("") && !recruitmentEditRequest.description().equals(" ")) + recruitment.modifyDescription(recruitmentEditRequest.description()); + else throw new BaseException(BLANK_DESCRIPTION); + } recruitmentRepository.save(recruitment); return new BaseResponse<>(SUCCESS); } catch (BaseException e) { @@ -41,4 +190,129 @@ public BaseResponse postRecruitment(RecruitmentPostRequest recruitmentPo throw new BaseException(INTERNAL_SERVER_ERROR); } } + + // [멤버] 띱 참여하기 + @Transactional(rollbackFor = Exception.class) + public BaseResponse participate(Long recruitmentIdx) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + validateLeaderRole(recruitment.getLeader().equals(user), LEADER_ROLE); + + if (recruitment.getParticipants().size()+2 == recruitment.getParticipantLimit()) { // 멤버 + 리더 + 현재 참여하려는 유저 //TODO: participantNumber 구할 때 participant 상태가 active인 것만 세기 + createParticipant(user, recruitment); + + recruitment.setStatus(DONE); + recruitmentRepository.save(recruitment); + + createNotifications(recruitment); + } else if (recruitment.getParticipants().size()+2 > recruitment.getParticipantLimit()) { //TODO: participantNumber 구할 때 participant 상태가 active인 것만 세기 + throw new BaseException(ALREADY_DONE_RECRUITMENT); + } else createParticipant(user, recruitment); + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // [리더] 띱 완료 처리 + @Transactional(rollbackFor = Exception.class) + public BaseResponse makeRecruitmentDone(Long recruitmentIdx) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + validateLeaderRole(!recruitment.getLeader().equals(user), NOT_LEADER_ROLE); + validateRecruitmentStatus(recruitment.getStatus().equals(DONE), ALREADY_DONE_RECRUITMENT); + + recruitment.setStatus(DONE); + recruitmentRepository.save(recruitment); + + createNotifications(recruitment); + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // [리더] 띱 모집 취소 처리 + @Transactional(rollbackFor = Exception.class) + public BaseResponse cancelRecruitment(Long recruitmentIdx) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + validateLeaderRole(!recruitment.getLeader().equals(user), NOT_LEADER_ROLE); + validateRecruitmentStatus(recruitment.getStatus().equals(DONE), ALREADY_DONE_RECRUITMENT); + + // 띱 모집 cancelled 처리 + recruitment.setStatus(CANCELLED); + recruitmentRepository.save(recruitment); + + // 해당 띱 참여자 참여 기록 cancelled 처리 + List participantList = participantRepository.findByRecruitmentAndStatusEquals(recruitment, ACTIVE); + for (Participant participant : participantList) { + participant.setStatus(CANCELLED); + participantRepository.save(participant); + } + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + // [멤버] 띱 나가기 + @Transactional(rollbackFor = Exception.class) + public BaseResponse withdrawRecruitment(Long recruitmentIdx) throws BaseException { + try { + User user = userRepository.findById(userService.getUserIdxWithValidation()).orElseThrow(() -> new BaseException(INVALID_USER_IDX)); + Recruitment recruitment = recruitmentRepository.findById(recruitmentIdx).orElseThrow(() -> new BaseException(INVALID_RECRUITMENT_IDX)); + boolean isParticipant = recruitment.getParticipants().stream().anyMatch(participant -> participant.getParticipant().equals(user)); + if (!isParticipant) throw new BaseException(NOT_MEMBER_ROLE); + validateRecruitmentStatus(recruitment.getStatus().equals(DONE), ALREADY_DONE_RECRUITMENT); + + Participant participant = participantRepository.findByParticipantAndRecruitmentAndStatusEquals(user, recruitment, ACTIVE); + participant.setStatus(INACTIVE); + participantRepository.save(participant); + return new BaseResponse<>(SUCCESS); + } catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(INTERNAL_SERVER_ERROR); + } + } + + private static void validateLeaderRole(boolean recruitment, BaseResponseStatus responseStatus) throws BaseException { + if (recruitment) throw new BaseException(responseStatus); + } + + private static void validateRecruitmentStatus(boolean recruitment, BaseResponseStatus responseStatus) throws BaseException { + if (recruitment) throw new BaseException(responseStatus); + } + + private void createNotifications(Recruitment recruitment) { + Notification leaderNotification = new Notification(recruitment.getLeader(), recruitment); + notificationRepository.save(leaderNotification); + leaderNotification.setUser(recruitment.getLeader()); + for (Participant participant : recruitment.getParticipants()) { + Notification participantNotification = new Notification(participant.getParticipant(), recruitment); + notificationRepository.save(participantNotification); + participantNotification.setUser(participant.getParticipant()); + } + } + + private void createParticipant(User user, Recruitment recruitment) { + Participant participant = new Participant(user, recruitment); + participant.setParticipant(user); + participant.setRecruitment(recruitment); + participantRepository.save(participant); + } + + private void validateWriter(User user, Recruitment recruitment) throws BaseException { + validateLeaderRole(!recruitment.getLeader().equals(user), NO_RECRUITMENT_LEADER); + } } diff --git a/src/main/java/com/hatcher/haemo/recruitment/domain/Recruitment.java b/src/main/java/com/hatcher/haemo/recruitment/domain/Recruitment.java index b41c65f..6d44e23 100644 --- a/src/main/java/com/hatcher/haemo/recruitment/domain/Recruitment.java +++ b/src/main/java/com/hatcher/haemo/recruitment/domain/Recruitment.java @@ -50,7 +50,7 @@ public class Recruitment extends BaseEntity { @OneToMany(mappedBy = "recruitment") @Where(clause = "status = 'ACTIVE'") - private List participants = new ArrayList<>(); + private List participants = new ArrayList<>(); // 띱을 나가도 이 리스트에는 있고 participant inactive 처리 @Builder public Recruitment(String name, User leader, RecruitType type, Integer participantLimit, String contactUrl, String description) { @@ -66,4 +66,24 @@ public void setLeader(User leader) { this.leader = leader; leader.getRecruitments().add(this); } + + public void modifyName(String name) { + this.name = name; + } + + public void modifyType(RecruitType type) { + this.type = type; + } + + public void modifyContactUrl(String contactUrl) { + this.contactUrl = contactUrl; + } + + public void modifyDescription(String description) { + this.description = description; + } + + public void modifyParticipantLimit(Integer participantLimit) { + this.participantLimit = participantLimit; + } } diff --git a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitResponse.java b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitResponse.java new file mode 100644 index 0000000..a08375c --- /dev/null +++ b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitResponse.java @@ -0,0 +1,9 @@ +package com.hatcher.haemo.recruitment.dto; + +import com.hatcher.haemo.comment.dto.CommentDto; + +import java.util.List; + +public record RecruitResponse(RecruitmentDetailDto recruitment, + Integer commentCount, + List commentList) {} diff --git a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDetailDto.java b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDetailDto.java new file mode 100644 index 0000000..fcabd1d --- /dev/null +++ b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDetailDto.java @@ -0,0 +1,11 @@ +package com.hatcher.haemo.recruitment.dto; + +public record RecruitmentDetailDto(Long recruitmentIdx, + String type, + String name, + String leader, + Integer participantNumber, + Integer participantLimit, + String description, + boolean isLeader, + boolean isRecruiting) {} diff --git a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDto.java b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDto.java new file mode 100644 index 0000000..c85180e --- /dev/null +++ b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentDto.java @@ -0,0 +1,11 @@ +package com.hatcher.haemo.recruitment.dto; + +public record RecruitmentDto(Long recruitmentIdx, + String type, + String name, + String leader, + Integer participantNumber, + Integer participantLimit, + String description, + boolean isLeader, + boolean isDone) {} diff --git a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentEditRequest.java b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentEditRequest.java new file mode 100644 index 0000000..f8df560 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentEditRequest.java @@ -0,0 +1,7 @@ +package com.hatcher.haemo.recruitment.dto; + +public record RecruitmentEditRequest(String name, + String type, + Integer participantLimit, + String contactUrl, + String description) {} diff --git a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentListResponse.java b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentListResponse.java index ab657e1..1256de4 100644 --- a/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentListResponse.java +++ b/src/main/java/com/hatcher/haemo/recruitment/dto/RecruitmentListResponse.java @@ -1,4 +1,5 @@ package com.hatcher.haemo.recruitment.dto; -public record RecruitmentListResponse() { -} +import java.util.List; + +public record RecruitmentListResponse(List recruitmentList) {} diff --git a/src/main/java/com/hatcher/haemo/recruitment/presentation/RecruitmentController.java b/src/main/java/com/hatcher/haemo/recruitment/presentation/RecruitmentController.java index f4accb5..e76002d 100644 --- a/src/main/java/com/hatcher/haemo/recruitment/presentation/RecruitmentController.java +++ b/src/main/java/com/hatcher/haemo/recruitment/presentation/RecruitmentController.java @@ -3,6 +3,9 @@ import com.hatcher.haemo.common.exception.BaseException; import com.hatcher.haemo.common.BaseResponse; import com.hatcher.haemo.recruitment.application.RecruitmentService; +import com.hatcher.haemo.recruitment.dto.RecruitResponse; +import com.hatcher.haemo.recruitment.dto.RecruitmentEditRequest; +import com.hatcher.haemo.recruitment.dto.RecruitmentListResponse; import com.hatcher.haemo.recruitment.dto.RecruitmentPostRequest; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -26,13 +29,75 @@ public BaseResponse postRecruitment(@RequestBody RecruitmentPostRequest recru } } -// // 모집글 목록 조회 -// @GetMapping("") -// public BaseResponse getRecruitmentList() { -// try { -// return BaseResponse.success(recruitmentService.getRecruitmentList()); -// } catch (BaseException e) { -// throw e; -// } -// } + // 모집중인 띱 목록 조회 + // 관심분야 띱 목록 조회 + // 참여중인 띱 목록 조회 + @GetMapping("") + public BaseResponse getRecruitmentList(@RequestParam(required = false) String type, @RequestParam(required = false) boolean isParticipant) { + try { + return recruitmentService.getRecruitmentList(type, isParticipant); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // 띱 상세 조회 + @GetMapping("/{recruitmentIdx}") + public BaseResponse getRecruitment(@PathVariable Long recruitmentIdx) { + try { + return recruitmentService.getRecruitment(recruitmentIdx); + } catch (BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // [리더] 띱 수정 + @PatchMapping("/{recruitmentIdx}") + public BaseResponse editRecruitment(@PathVariable Long recruitmentIdx, @RequestBody RecruitmentEditRequest recruitmentEditRequest) { + try { + return recruitmentService.editRecruitment(recruitmentIdx, recruitmentEditRequest); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // [멤버] 띱 참여하기 + @PostMapping("/{recruitmentIdx}") + public BaseResponse postRecruitment(@PathVariable Long recruitmentIdx) { + try { + return recruitmentService.participate(recruitmentIdx); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // [리더] 띱 모집완료 처리 + @PatchMapping("/{recruitmentIdx}/done") + public BaseResponse makeRecruitmentDone(@PathVariable Long recruitmentIdx) { + try { + return recruitmentService.makeRecruitmentDone(recruitmentIdx); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // [리더] 띱 모집취소 처리 + @PatchMapping("/{recruitmentIdx}/cancel") + public BaseResponse cancelRecruitment(@PathVariable Long recruitmentIdx) { + try { + return recruitmentService.cancelRecruitment(recruitmentIdx); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } + + // [멤버] 띱 나가기 + @PatchMapping("/{recruitmentIdx}/withdraw") + public BaseResponse withdrawRecruitment(@PathVariable Long recruitmentIdx) { + try { + return recruitmentService.withdrawRecruitment(recruitmentIdx); + } catch(BaseException e) { + return new BaseResponse<>(e.getStatus()); + } + } } diff --git a/src/main/java/com/hatcher/haemo/recruitment/repository/ParticipantRepository.java b/src/main/java/com/hatcher/haemo/recruitment/repository/ParticipantRepository.java new file mode 100644 index 0000000..bfad3c8 --- /dev/null +++ b/src/main/java/com/hatcher/haemo/recruitment/repository/ParticipantRepository.java @@ -0,0 +1,13 @@ +package com.hatcher.haemo.recruitment.repository; + +import com.hatcher.haemo.recruitment.domain.Participant; +import com.hatcher.haemo.recruitment.domain.Recruitment; +import com.hatcher.haemo.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ParticipantRepository extends JpaRepository { + List findByRecruitmentAndStatusEquals(Recruitment recruitment, String status); + Participant findByParticipantAndRecruitmentAndStatusEquals(User user, Recruitment recruitment, String status); +} diff --git a/src/main/java/com/hatcher/haemo/recruitment/repository/RecruitmentRepository.java b/src/main/java/com/hatcher/haemo/recruitment/repository/RecruitmentRepository.java index e032f16..97d0616 100644 --- a/src/main/java/com/hatcher/haemo/recruitment/repository/RecruitmentRepository.java +++ b/src/main/java/com/hatcher/haemo/recruitment/repository/RecruitmentRepository.java @@ -1,7 +1,13 @@ package com.hatcher.haemo.recruitment.repository; +import com.hatcher.haemo.common.enums.RecruitType; import com.hatcher.haemo.recruitment.domain.Recruitment; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface RecruitmentRepository extends JpaRepository { + + List findByStatusOrderByCreatedDateDesc(String status); + List findByTypeAndStatusEqualsOrderByCreatedDateDesc(RecruitType type, String status); } diff --git a/src/main/java/com/hatcher/haemo/user/domain/User.java b/src/main/java/com/hatcher/haemo/user/domain/User.java index 78e55df..f34a21e 100644 --- a/src/main/java/com/hatcher/haemo/user/domain/User.java +++ b/src/main/java/com/hatcher/haemo/user/domain/User.java @@ -2,6 +2,7 @@ import com.hatcher.haemo.comment.domain.Comment; import com.hatcher.haemo.common.BaseEntity; +import com.hatcher.haemo.notification.domain.Notification; import com.hatcher.haemo.recruitment.domain.Participant; import com.hatcher.haemo.recruitment.domain.Recruitment; import jakarta.persistence.*; @@ -45,8 +46,10 @@ public class User extends BaseEntity { private List comments = new ArrayList<>(); @OneToMany(mappedBy = "participant") - @Where(clause = "status = 'ACTIVE'") - private List participants = new ArrayList<>(); // 해당 user가 participant로 있는 recruitment list + private List participants = new ArrayList<>(); // 해당 user가 participant로 있는 recruitment list(leader로 있는 모임은 제외) + + @OneToMany(mappedBy = "user") + private List notifications = new ArrayList<>(); @Builder public User(String loginId, String password, String nickname) {