diff --git a/src/main/java/org/sopt/makers/operation/common/ExceptionMessage.java b/src/main/java/org/sopt/makers/operation/common/ExceptionMessage.java index 89bee636..dce46c71 100644 --- a/src/main/java/org/sopt/makers/operation/common/ExceptionMessage.java +++ b/src/main/java/org/sopt/makers/operation/common/ExceptionMessage.java @@ -26,7 +26,8 @@ public enum ExceptionMessage { END_LECTURE("이미 종료된 세션입니다."), NO_SUB_LECTURE_EQUAL_ROUND("해당 라운드와 일치하는 출석 세션이 없습니다."), FAULT_DATE_FORMATTER("잘못된 날짜 형식입니다."), - DUPLICATED_MEMBER("이미 존재하는 회원입니다."); + DUPLICATED_MEMBER("이미 존재하는 회원입니다."), + INVALID_ALARM("존재하지 않는 알림입니다."); private final String name; } diff --git a/src/main/java/org/sopt/makers/operation/common/ResponseMessage.java b/src/main/java/org/sopt/makers/operation/common/ResponseMessage.java index cdb62d00..26d7391c 100644 --- a/src/main/java/org/sopt/makers/operation/common/ResponseMessage.java +++ b/src/main/java/org/sopt/makers/operation/common/ResponseMessage.java @@ -32,7 +32,14 @@ public enum ResponseMessage { SUCCESS_GET_LECTURE("세션 상세 조회 성공"), SUCCESS_START_ATTENDANCE("출석 시작 성공"), SUCCESS_GET_MEMBERS("유저 리스트 조회 성공"), - SUCCESS_DELETE_LECTURE("세션 삭제 성공"); + SUCCESS_DELETE_LECTURE("세션 삭제 성공"), + + /** alarm **/ + SUCCESS_CREATE_ALARM("알림 생성 성공"), + SUCCESS_GET_ALARMS("알림 리스트 조회 성공"), + SUCCESS_GET_ALARM("알림 상세 조회 성공"), + SUCCESS_DELETE_ALARM("알림 삭제 성공") + ; private final String message; } diff --git a/src/main/java/org/sopt/makers/operation/controller/web/AlarmController.java b/src/main/java/org/sopt/makers/operation/controller/web/AlarmController.java new file mode 100644 index 00000000..b731ae1d --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/controller/web/AlarmController.java @@ -0,0 +1,76 @@ +package org.sopt.makers.operation.controller.web; + +import static org.sopt.makers.operation.common.ApiResponse.*; +import static org.sopt.makers.operation.common.ResponseMessage.*; + +import java.net.URI; + +import org.sopt.makers.operation.common.ApiResponse; +import org.sopt.makers.operation.dto.alarm.AlarmRequestDTO; +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Status; +import org.sopt.makers.operation.service.AlarmService; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import io.swagger.annotations.ApiOperation; +import lombok.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/alarms") +public class AlarmController { + private final AlarmService alarmService; + + @ApiOperation("알림 생성") + @PostMapping + public ResponseEntity createAlarm(@RequestBody AlarmRequestDTO requestDTO) { + val alarmId = alarmService.createAlarm(requestDTO); + return ResponseEntity + .created(getURI(alarmId)) + .body(success(SUCCESS_CREATE_ALARM.getMessage(), alarmId)); + } + + @ApiOperation("알림 리스트 조회") + @GetMapping + public ResponseEntity getAlarms( + @RequestParam(required = false) Integer generation, + @RequestParam(required = false) Part part, + @RequestParam(required = false) Status status, + Pageable pageable + ) { + val response = alarmService.getAlarms(generation, part, status, pageable); + return ResponseEntity.ok(success(SUCCESS_GET_ALARMS.getMessage(), response)); + } + + @ApiOperation("알림 상세 조회") + @GetMapping("/{alarmId}") + public ResponseEntity getAlarm(@PathVariable Long alarmId) { + val response = alarmService.getAlarm(alarmId); + return ResponseEntity.ok(success(SUCCESS_GET_ALARM.getMessage(), response)); + } + + @ApiOperation("알림 삭제") + @DeleteMapping("/{alarmId}") + public ResponseEntity deleteAlarm(@PathVariable Long alarmId) { + alarmService.deleteAlarm(alarmId); + return ResponseEntity.ok(success(SUCCESS_DELETE_ALARM.getMessage())); + } + + private URI getURI(Long alarmId) { + return ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{alarmId}") + .buildAndExpand(alarmId) + .toUri(); + } +} diff --git a/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmRequestDTO.java b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmRequestDTO.java index b0ce3e9a..281b2f01 100644 --- a/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmRequestDTO.java +++ b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmRequestDTO.java @@ -3,6 +3,7 @@ import java.util.List; import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Alarm; import org.sopt.makers.operation.entity.alarm.Attribute; import org.sopt.makers.operation.entity.alarm.Status; @@ -12,9 +13,11 @@ public record AlarmRequestDTO( String title, String content, String link, - boolean isActive, + Boolean isActive, Part part, - List targetList, - Status status + List targetList ) { + public Alarm toEntity() { + return new Alarm(this); + } } diff --git a/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmResponseDTO.java new file mode 100644 index 00000000..c7149f41 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmResponseDTO.java @@ -0,0 +1,32 @@ +package org.sopt.makers.operation.dto.alarm; + +import static java.util.Objects.*; + +import org.sopt.makers.operation.entity.alarm.Alarm; + +import lombok.Builder; + +@Builder +public record AlarmResponseDTO( + String attribute, + String part, + Boolean isActive, + String title, + String content, + String link, + String createdAt, + String sendAt +) { + public static AlarmResponseDTO of(Alarm alarm) { + return AlarmResponseDTO.builder() + .attribute(alarm.getAttribute().getName()) + .part(alarm.getPart().getName()) + .isActive(alarm.getIsActive()) + .title(alarm.getTitle()) + .content(alarm.getContent()) + .link(alarm.getLink()) + .createdAt(alarm.getCreatedDate().toString()) + .sendAt(nonNull(alarm.getSendAt()) ? alarm.getSendAt().toString() : null) + .build(); + } +} diff --git a/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmsResponseDTO.java b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmsResponseDTO.java new file mode 100644 index 00000000..31dd3110 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/dto/alarm/AlarmsResponseDTO.java @@ -0,0 +1,40 @@ +package org.sopt.makers.operation.dto.alarm; + +import static java.util.Objects.*; + +import java.util.List; + +import org.sopt.makers.operation.entity.alarm.Alarm; + +import lombok.Builder; + +public record AlarmsResponseDTO( + List alarms +) { + public static AlarmsResponseDTO of(List alarms) { + return new AlarmsResponseDTO(alarms.stream().map(AlarmVO::of).toList()); + } + + @Builder + record AlarmVO( + Long alarmId, + String part, + String attribute, + String title, + String content, + String sendAt, + String status + ) { + static AlarmVO of(Alarm alarm) { + return AlarmVO.builder() + .alarmId(alarm.getId()) + .part(nonNull(alarm.getPart()) ? alarm.getPart().getName() : null) + .attribute(alarm.getAttribute().getName()) + .title(alarm.getTitle()) + .content(alarm.getContent()) + .sendAt(nonNull(alarm.getSendAt()) ? alarm.getSendAt().toString() : null) + .status(alarm.getStatus().getName()) + .build(); + } + } +} diff --git a/src/main/java/org/sopt/makers/operation/entity/alarm/Alarm.java b/src/main/java/org/sopt/makers/operation/entity/alarm/Alarm.java index 7777c18d..813a952c 100644 --- a/src/main/java/org/sopt/makers/operation/entity/alarm/Alarm.java +++ b/src/main/java/org/sopt/makers/operation/entity/alarm/Alarm.java @@ -1,5 +1,6 @@ package org.sopt.makers.operation.entity.alarm; +import static java.util.Objects.*; import static javax.persistence.EnumType.*; import static javax.persistence.GenerationType.*; @@ -43,14 +44,14 @@ public class Alarm extends BaseEntity { private String link; - private boolean isActive; + private Boolean isActive; @Enumerated(value = STRING) private Part part; @Column(columnDefinition = "TEXT") @Convert(converter = LongListConverter.class) - private List targetList; + private List targetList; @Column(nullable = false) @Enumerated(value = STRING) @@ -64,8 +65,13 @@ public Alarm(AlarmRequestDTO requestDTO) { this.title = requestDTO.title(); this.content = requestDTO.content(); this.link = requestDTO.link(); - this.isActive = requestDTO.isActive(); - this.part = requestDTO.part(); - this.targetList = requestDTO.targetList(); + if (nonNull(requestDTO.isActive()) && nonNull(requestDTO.part())) { + this.isActive = requestDTO.isActive(); + this.part = requestDTO.part(); + } + if (nonNull(requestDTO.targetList())) { + this.targetList = requestDTO.targetList(); + } + this.status = Status.BEFORE; } } diff --git a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmCustomRepository.java b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmCustomRepository.java new file mode 100644 index 00000000..0ecd4e53 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmCustomRepository.java @@ -0,0 +1,12 @@ +package org.sopt.makers.operation.repository.alarm; + +import java.util.List; + +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Alarm; +import org.sopt.makers.operation.entity.alarm.Status; +import org.springframework.data.domain.Pageable; + +public interface AlarmCustomRepository { + List getAlarms(Integer generation, Part part, Status status, Pageable pageable); +} diff --git a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java new file mode 100644 index 00000000..9ca5e9d4 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java @@ -0,0 +1,7 @@ +package org.sopt.makers.operation.repository.alarm; + +import org.sopt.makers.operation.entity.alarm.Alarm; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AlarmRepository extends JpaRepository, AlarmCustomRepository { +} diff --git a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java new file mode 100644 index 00000000..966b4b5f --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java @@ -0,0 +1,51 @@ +package org.sopt.makers.operation.repository.alarm; + +import static java.util.Objects.*; +import static org.sopt.makers.operation.entity.Part.*; +import static org.sopt.makers.operation.entity.alarm.QAlarm.*; + +import java.util.List; + +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Alarm; +import org.sopt.makers.operation.entity.alarm.Status; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AlarmRepositoryImpl implements AlarmCustomRepository { + private final JPAQueryFactory queryFactory; + + @Override + public List getAlarms(Integer generation, Part part, Status status, Pageable pageable) { + return queryFactory + .selectFrom(alarm) + .where( + generationEq(generation), + partEq(part), + statusEq(status) + ) + .orderBy(alarm.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + private BooleanExpression generationEq(Integer generation) { + return nonNull(generation) ? alarm.generation.eq(generation) : null; + } + + private BooleanExpression partEq(Part part) { + return (nonNull(part) && !part.equals(ALL)) ? alarm.part.eq(part) : null; + } + + private BooleanExpression statusEq(Status status) { + return nonNull(status) ? alarm.status.eq(status) : null; + } +} diff --git a/src/main/java/org/sopt/makers/operation/service/AlarmService.java b/src/main/java/org/sopt/makers/operation/service/AlarmService.java new file mode 100644 index 00000000..9574b811 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/service/AlarmService.java @@ -0,0 +1,15 @@ +package org.sopt.makers.operation.service; + +import org.sopt.makers.operation.dto.alarm.AlarmRequestDTO; +import org.sopt.makers.operation.dto.alarm.AlarmResponseDTO; +import org.sopt.makers.operation.dto.alarm.AlarmsResponseDTO; +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Status; +import org.springframework.data.domain.Pageable; + +public interface AlarmService { + Long createAlarm(AlarmRequestDTO requestDTO); + AlarmsResponseDTO getAlarms(Integer generation, Part part, Status status, Pageable pageable); + AlarmResponseDTO getAlarm(Long alarmId); + void deleteAlarm(Long alarmId); +} diff --git a/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java b/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java new file mode 100644 index 00000000..c87ccbd5 --- /dev/null +++ b/src/main/java/org/sopt/makers/operation/service/AlarmServiceImpl.java @@ -0,0 +1,56 @@ +package org.sopt.makers.operation.service; + +import static org.sopt.makers.operation.common.ExceptionMessage.*; + +import javax.persistence.EntityNotFoundException; + +import org.sopt.makers.operation.dto.alarm.AlarmRequestDTO; +import org.sopt.makers.operation.dto.alarm.AlarmResponseDTO; +import org.sopt.makers.operation.dto.alarm.AlarmsResponseDTO; +import org.sopt.makers.operation.entity.Part; +import org.sopt.makers.operation.entity.alarm.Alarm; +import org.sopt.makers.operation.entity.alarm.Status; +import org.sopt.makers.operation.repository.alarm.AlarmRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.*; + +@Service +@RequiredArgsConstructor +public class AlarmServiceImpl implements AlarmService { + private final AlarmRepository alarmRepository; + + @Override + @Transactional + public Long createAlarm(AlarmRequestDTO requestDTO) { + val alarmEntity = requestDTO.toEntity(); + val savedAlarm = alarmRepository.save(alarmEntity); + return savedAlarm.getId(); + } + + @Override + public AlarmsResponseDTO getAlarms(Integer generation, Part part, Status status, Pageable pageable) { + val alarms = alarmRepository.getAlarms(generation, part, status, pageable); + return AlarmsResponseDTO.of(alarms); + } + + @Override + public AlarmResponseDTO getAlarm(Long alarmId) { + val alarm = findAlarm(alarmId); + return AlarmResponseDTO.of(alarm); + } + + @Override + @Transactional + public void deleteAlarm(Long alarmId) { + val alarm = findAlarm(alarmId); + alarmRepository.delete(alarm); + } + + private Alarm findAlarm(Long alarmId) { + return alarmRepository.findById(alarmId) + .orElseThrow(() -> new EntityNotFoundException(INVALID_ALARM.getName())); + } +}