Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
766 changes: 766 additions & 0 deletions hs_err_pid21744.log

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,22 @@ public ResponseEntity<ApiResponse<List<FollowResponseDTO>>> getFollowers(@Authen
return ApiResponse.success(SuccessStatus.GET_FOLLOWER_SUCCESS, response);
}

@Operation(
summary = "팔로우 승인/삭제 API",
description = "팔로우를 승인 또는 삭제하는 API 입니다." +
"<br>status - 승인: ACCEPT / 삭제: DELETE 로 보내주시기 바랍니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "팔로우 승인/삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다.")
})
@PostMapping("/follow/accept")
public ResponseEntity<ApiResponse<Void>> acceptFollow(@AuthenticationPrincipal UserDetails userDetails,
@RequestBody AcceptFollowRequestDTO acceptFollowRequestDTO){
followService.acceptFollow(acceptFollowRequestDTO, userDetails.getUsername());
return ApiResponse.success_only(SuccessStatus.FOLLOW_PROCESS_SUCCESS);
}

/*
*
* 닉네임 API
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.moongeul.backend.api.member.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AcceptFollowRequestDTO {

private Long followerId; // 승인/삭제할 팔로워의 ID
private String status; // 처리 상태 (승인: ACCEPT / 삭제: DELETE)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.moongeul.backend.api.member.service;


import com.moongeul.backend.api.member.dto.AcceptFollowRequestDTO;
import com.moongeul.backend.api.member.dto.FollowResponseDTO;
import com.moongeul.backend.api.member.entity.Follow;
import com.moongeul.backend.api.member.entity.FollowStatus;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.PrivacyLevel;
import com.moongeul.backend.api.member.repository.FollowRepository;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.api.notification.entity.NotificationType;
import com.moongeul.backend.api.notification.entity.Notifications;
import com.moongeul.backend.api.notification.repository.NotificationRepository;
import com.moongeul.backend.api.notification.service.NotificationTriggerService;
import com.moongeul.backend.common.exception.BadRequestException;
import com.moongeul.backend.common.exception.NotFoundException;
Expand All @@ -28,13 +32,14 @@ public class FollowService {

private final FollowRepository followRepository;
private final MemberRepository memberRepository;
private final NotificationRepository notificationRepository;

private final NotificationTriggerService notificationTriggerService;

/* 팔로우 API */
@Transactional
public void follow(Long following_id, String email){
Member following = getById(following_id); // 팔로우 대상
Member following = getMemberById(following_id); // 팔로우 대상
Member follower = getMemberByEmail(email); // 나

// 에러처리: 자기자신을 팔로우하는 경우 (불가)
Expand Down Expand Up @@ -137,7 +142,33 @@ public List<FollowResponseDTO> getFollower(String email){
.toList();
}

private Member getById(Long id) {
/* 팔로우 승인 API */
@Transactional
public void acceptFollow(AcceptFollowRequestDTO acceptFollowRequestDTO, String email){

Member member = getMemberByEmail(email);

Follow follow = followRepository.findByFollowingIdAndFollowerId(member.getId(), acceptFollowRequestDTO.getFollowerId())
.orElseThrow(() -> new BadRequestException(ErrorStatus.NO_FOLLOW_RELATIONSHIP.getMessage()));

// 알림 상태를 바꿔주기 위해
Notifications notifications = notificationRepository.findByReceiverIdAndActorIdAndType(member.getId(), acceptFollowRequestDTO.getFollowerId(), NotificationType.FOLLOW_PRIVATE)
.orElseThrow(() -> new BadRequestException(ErrorStatus.NOTIFICATION_NOTFOUND_EXCEPTION.getMessage()));

if(acceptFollowRequestDTO.getStatus().equals("ACCEPT")){
// '승인'한 경우 -> ACCEPTED 변경 + 알림 타입 변경
follow.accept();
notifications.switchNotificationType();
} else if(acceptFollowRequestDTO.getStatus().equals("DELETE")){
// '삭제'한 경우 -> 팔로우/알림 삭제
followRepository.delete(follow);
notificationRepository.delete(notifications);
} else{
throw new BadRequestException(ErrorStatus.BAD_FOLLOW_PROCESS_REQUEST.getMessage());
}
}

private Member getMemberById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package com.moongeul.backend.api.notification.controller;

import com.moongeul.backend.api.notification.dto.DeviceTokenRequestDTO;
import com.moongeul.backend.api.notification.dto.NotificationsResponseDTO;
import com.moongeul.backend.api.notification.dto.checkNotificationsResponseDTO;
import com.moongeul.backend.api.notification.service.NotificationService;
import com.moongeul.backend.api.notification.service.PushNotificationService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Notification", description = "Notification(푸시알람) 관련 API 입니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v2/notification")
public class NotificationController {

private final PushNotificationService pushNotificationService;
private final NotificationService notificationService;

@Operation(
Expand All @@ -36,8 +43,50 @@ public class NotificationController {
@PostMapping("/device-token")
public ResponseEntity<ApiResponse<Void>> registerToken(@AuthenticationPrincipal UserDetails userDetails,
@RequestBody DeviceTokenRequestDTO requestDTO) {
notificationService.registerOrUpdateToken(userDetails.getUsername(), requestDTO);
pushNotificationService.registerOrUpdateToken(userDetails.getUsername(), requestDTO);

return ApiResponse.success_only(SuccessStatus.REGISTER_DEVICE_TOKEN_SUCCESS);
}

@Operation(
summary = "미확인 알림 존재 여부 조회 API",
description = "미확인 알림이 존재하는지 여부(true/false)를 알 수 있습니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "미확인 알림 존재 여부 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다."),
})
@GetMapping("/unread")
public ResponseEntity<ApiResponse<checkNotificationsResponseDTO>> checkUnReadNotifications(@AuthenticationPrincipal UserDetails userDetails) {

checkNotificationsResponseDTO response = notificationService.checkUnReadNotifications(userDetails.getUsername());
return ApiResponse.success(SuccessStatus.GET_UNREAD_NOTIFICATIONS_SUCCESS, response);
}

@Operation(
summary = "알림 내역 전체 조회 API",
description = "알림 페이지에 띄울 정보들을 전체 조회 합니다." +
"<br>- profileImage 값이 null인 것은 '서버공지'인 경우 입니다." +
"<br>- 알림 유형이 'FOLLOW_PRIVATE'일 경우, 승인/삭제 버튼 필요" +
"<br><br>[enum] 알림 유형 ->" +
"<br>- NOTICE: 서버공지" +
"<br>- LIKE: 공감 알림" +
"<br>- COMMENT: 댓글 알림" +
"<br>- FOLLOW_OPEN: 팔로우(공개 계정)" +
"<br>- FOLLOW_PRIVATE: 팔로우(비공개 계정)" +
"<br>- FOLLOW_PRIVATE_ACCEPTED: 팔로우(비공개 계정) - 승인됨"
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "알림 내역 전체 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다."),
})
@GetMapping
public ResponseEntity<ApiResponse<List<NotificationsResponseDTO>>> getNotifications(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam(required = false, defaultValue = "1") @Min(value = 1, message = "페이지는 1 이상이어야 합니다.(1부터 시작)") Integer page,
@RequestParam(required = false, defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) {

return ApiResponse.success_only(SuccessStatus.REGISTER_DEVICE_TOKEN);
List<NotificationsResponseDTO> response = notificationService.getNotifications(page, size, userDetails.getUsername());
return ApiResponse.success(SuccessStatus.GET_NOTIFICATIONS_SUCCESS, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moongeul.backend.api.notification.dto;

import com.moongeul.backend.api.notification.entity.NotificationType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NotificationsResponseDTO {

private Long id; // 알림 ID

private Long relatedId; // 연관된 ID값 (게시글ID, 회원ID 등)
private NotificationType notificationType; // 알림 타입 (LIKE, FOLLOW_OPEN ... 등)

private String profileImage;
private String content;
private LocalDateTime created_at;

private boolean isRead; // 읽음 처리
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.moongeul.backend.api.notification.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class checkNotificationsResponseDTO {

private boolean exist; // 읽지 않은 알림 존재 여부(true/false)
private Long count; // 읽지 않은 알림 개수
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public enum NotificationType {
LIKE("공감 알림"),
COMMENT("댓글 알림"),
FOLLOW_OPEN("팔로우(공개 계정)"),
FOLLOW_PRIVATE("팔로우(비공개 계정)");
FOLLOW_PRIVATE("팔로우(비공개 계정)"),
FOLLOW_PRIVATE_ACCEPTED("팔로우(비공개 계정) - 승인됨");

private final String key;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ public class Notifications extends BaseTimeEntity {
private Long relatedId; // 클릭 시 이동할 게시글 ID or 유저 ID or 공지사항 ID

private boolean isRead; // 읽음 처리 여부

public void switchNotificationType(){
this.type = NotificationType.FOLLOW_PRIVATE_ACCEPTED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.moongeul.backend.api.notification.event.AnswerNotificationEvent;
import com.moongeul.backend.api.notification.event.FollowNotificationEvent;
import com.moongeul.backend.api.notification.event.LikeNotificationEvent;
import com.moongeul.backend.api.notification.service.NotificationService;
import com.moongeul.backend.api.notification.service.PushNotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
Expand All @@ -15,7 +15,7 @@
@RequiredArgsConstructor
public class NotificationEventListener { // 발행된 이벤트를 받아서 별도의 스레드에서 비동기적으로 알림을 저장하고 전송하는 클래스

private final NotificationService notificationService;
private final PushNotificationService pushNotificationService;

/* 공감 알림 */
@Async // 별도의 스레드에서 실행되도록 설정
Expand All @@ -24,7 +24,7 @@ public void handleLikeNotification(LikeNotificationEvent event) {
String message = event.actor().getNickname() + "님이 회원님의 기록에 공감했습니다.";

// 실제 DB 저장 및 Expo 푸시 알림 발송 로직 실행
notificationService.send(
pushNotificationService.send(
event.receiver(),
event.actor(),
NotificationType.LIKE,
Expand All @@ -40,7 +40,7 @@ public void handleCommentNotification(AnswerNotificationEvent event) {
String message = event.actor().getNickname() + "님이 회원님의 질문에 댓글을 달았습니다.";

// 실제 DB 저장 및 Expo 푸시 알림 발송 로직 실행
notificationService.send(
pushNotificationService.send(
event.receiver(),
event.actor(),
NotificationType.LIKE,
Expand All @@ -63,7 +63,7 @@ public void handleFollowNotification(FollowNotificationEvent event) {
}

// 실제 DB 저장 및 Expo 푸시 알림 발송 로직 실행
notificationService.send(
pushNotificationService.send(
event.receiver(),
event.actor(),
notificationType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
package com.moongeul.backend.api.notification.repository;

import com.moongeul.backend.api.notification.entity.NotificationType;
import com.moongeul.backend.api.notification.entity.Notifications;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface NotificationRepository extends JpaRepository<Notifications, Long> {

Slice<Notifications> findByReceiverIdOrderByCreatedAtDesc(Long id, Pageable pageable);

@Modifying(clearAutomatically = true)
@Query("UPDATE Notifications n SET n.isRead = true WHERE n.receiver.id = :receiverId AND n.isRead = false")
void updateIsReadByReceiverId(@Param("receiverId") Long receiverId);

// 읽지 않은 알림이 있는지 여부 확인 (EXISTS 쿼리 사용으로 빠름)
boolean existsByReceiverIdAndIsReadFalse(Long receiverId);

// 읽지 않은 알림의 개수 확인
long countByReceiverIdAndIsReadFalse(Long receiverId);

// receiverId와 actorId로 FOLLOW_PRIVATE 알림 가져오기
Optional<Notifications> findByReceiverIdAndActorIdAndType(Long receiverId, Long actorId, NotificationType notificationType);

}
Loading