diff --git a/backend-env b/backend-env index b539bae..55174d7 160000 --- a/backend-env +++ b/backend-env @@ -1 +1 @@ -Subproject commit b539bae500fb9045dcbdfa2fcf2f781bb9730c61 +Subproject commit 55174d72d65e0629abdaab1b279dac74a46059ae diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMController.java b/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMController.java index 2c32a14..ca4cdbf 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMController.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMController.java @@ -2,10 +2,9 @@ import org.example.skuhomepage.domain.firebase.dto.TokenRequestDTO; import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; -import org.example.skuhomepage.domain.firebase.service.FCMService; -import org.example.skuhomepage.domain.firebase.service.FCMTopicService; +import org.example.skuhomepage.domain.firebase.service.DeviceTokenService; +import org.example.skuhomepage.domain.firebase.service.KeywordService; import org.example.skuhomepage.global.apiPayload.ApiResponse; -import org.example.skuhomepage.global.enums.TopicGroup; import org.example.skuhomepage.global.security.CustomUserDetails; import org.springframework.web.bind.annotation.RestController; @@ -15,44 +14,47 @@ @RequiredArgsConstructor public class FCMController implements FCMControllerSpec { - private final FCMService fcmService; - private final FCMTopicService fcmTopicService; + // private final FCMService fcmService; + private final DeviceTokenService deviceTokenService; + private final KeywordService keywordService; + + // private final FCMTopicService fcmTopicService; @Override public ApiResponse registerToken(TokenRequestDTO tokenReq, CustomUserDetails userDetails) { - fcmService.registerToken(tokenReq, userDetails.getUserId()); - return ApiResponse.onSuccess(null); - } - - @Override - public ApiResponse sendTestMessage( - TokenRequestDTO tokenReq, CustomUserDetails userDetails) { - fcmService.sendTestMessage(tokenReq); + deviceTokenService.registerToken(tokenReq, userDetails); return ApiResponse.onSuccess(null); } - @Override - public ApiResponse sendTestTopicMessage( - TopicRequestDTO topicReq, CustomUserDetails userDetails) { - fcmService.sendTestTopicMessage(topicReq); - return ApiResponse.onSuccess(null); - } + // @Override + // public ApiResponse sendTestMessage( + // TokenRequestDTO tokenReq, CustomUserDetails userDetails) { + // fcmService.sendTestMessage(tokenReq); + // return ApiResponse.onSuccess(null); + // } + // + // @Override + // public ApiResponse sendTestTopicMessage( + // TopicRequestDTO topicReq, CustomUserDetails userDetails) { + // fcmService.sendTestTopicMessage(topicReq); + // return ApiResponse.onSuccess(null); + // } @Override public ApiResponse topicRegister(TopicRequestDTO topicReq, CustomUserDetails userDetails) { - fcmTopicService.registerTopic(topicReq, userDetails.getUserId()); - return ApiResponse.onSuccess(null); - } - - @Override - public ApiResponse topicDelete(TopicRequestDTO topicReq, CustomUserDetails userDetails) { - fcmTopicService.deleteTopic(topicReq, userDetails.getUserId()); + keywordService.registerKeyword(topicReq, userDetails); return ApiResponse.onSuccess(null); } @Override - public ApiResponse topicList(TopicGroup topicGroup, CustomUserDetails userDetails) { + public ApiResponse topicDelete(String keyword, CustomUserDetails userDetails) { + keywordService.deleteKeyword(keyword, userDetails); return ApiResponse.onSuccess(null); } + // + // @Override + // public ApiResponse topicList(TopicGroup topicGroup, CustomUserDetails userDetails) { + // return ApiResponse.onSuccess(null); + // } } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMControllerSpec.java b/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMControllerSpec.java index 4418ce6..2d25ce6 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMControllerSpec.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/controller/FCMControllerSpec.java @@ -3,13 +3,11 @@ import org.example.skuhomepage.domain.firebase.dto.TokenRequestDTO; import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; import org.example.skuhomepage.global.apiPayload.ApiResponse; -import org.example.skuhomepage.global.enums.TopicGroup; import org.example.skuhomepage.global.security.CustomUserDetails; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "알림", description = "FCM 알림 관련 API") @@ -21,15 +19,15 @@ public interface FCMControllerSpec { ApiResponse registerToken( @RequestBody TokenRequestDTO req, @AuthenticationPrincipal CustomUserDetails userDetails); - @PostMapping("/test/send-token") - @Operation(summary = "테스트 알림 전송", description = "테스트 알림을 전송하는 API") - ApiResponse sendTestMessage( - @RequestBody TokenRequestDTO req, @AuthenticationPrincipal CustomUserDetails userDetails); - - @PostMapping("/test/send-topic") - @Operation(summary = "테스트 토픽 알림 전송", description = "테스트 토픽 알림을 전송하는 API") - ApiResponse sendTestTopicMessage( - @RequestBody TopicRequestDTO req, @AuthenticationPrincipal CustomUserDetails userDetails); + // @PostMapping("/test/send-token") + // @Operation(summary = "테스트 알림 전송", description = "테스트 알림을 전송하는 API") + // ApiResponse sendTestMessage( + // @RequestBody TokenRequestDTO req, @AuthenticationPrincipal CustomUserDetails userDetails); + // + // @PostMapping("/test/send-topic") + // @Operation(summary = "테스트 토픽 알림 전송", description = "테스트 토픽 알림을 전송하는 API") + // ApiResponse sendTestTopicMessage( + // @RequestBody TopicRequestDTO req, @AuthenticationPrincipal CustomUserDetails userDetails); @PostMapping("/keyword") @Operation(summary = "키워드 등록", description = "키워드를 등록하는 API") @@ -40,14 +38,13 @@ ApiResponse topicRegister( @DeleteMapping("/keyword") @Operation(summary = "키워드 삭제", description = "키워드를 삭제하는 API") ApiResponse topicDelete( - @RequestBody TopicRequestDTO keywordReq, - @AuthenticationPrincipal CustomUserDetails userDetails); - - @GetMapping("/keyword") - @Operation(summary = "키워드 조회", description = "키워드를 조회하는 API") - ApiResponse topicList( - @Parameter(description = "주제", example = "SKU_NOTICE", required = true) @RequestParam - TopicGroup topicGroup, - @Parameter(description = "사용자 정보", hidden = true) @AuthenticationPrincipal - CustomUserDetails userDetails); + @RequestBody String keyword, @AuthenticationPrincipal CustomUserDetails userDetails); + + // @GetMapping("/keyword") + // @Operation(summary = "키워드 조회", description = "키워드를 조회하는 API") + // ApiResponse topicList( + // @Parameter(description = "주제", example = "SKU_NOTICE", required = true) @RequestParam + // TopicGroup topicGroup, + // @Parameter(description = "사용자 정보", hidden = true) @AuthenticationPrincipal + // CustomUserDetails userDetails); } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/dto/TokenRequestDTO.java b/src/main/java/org/example/skuhomepage/domain/firebase/dto/TokenRequestDTO.java index fdf1c33..05ec367 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/dto/TokenRequestDTO.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/dto/TokenRequestDTO.java @@ -7,5 +7,5 @@ @Schema(description = "토큰 요청 DTO") public class TokenRequestDTO { @Schema(description = "토큰", example = "fcm_token") - private String token; + private String fcmToken; } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/dto/TopicRequestDTO.java b/src/main/java/org/example/skuhomepage/domain/firebase/dto/TopicRequestDTO.java index a16c58d..3e7581c 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/dto/TopicRequestDTO.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/dto/TopicRequestDTO.java @@ -1,18 +1,13 @@ package org.example.skuhomepage.domain.firebase.dto; -import org.example.skuhomepage.global.enums.TopicGroup; +import jakarta.validation.constraints.NotBlank; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter -@Builder -@Schema(description = "키워드 요청 DTO") +@NoArgsConstructor public class TopicRequestDTO { - @Schema(description = "키워드", example = "장학금") - private String keyword; - @Schema(description = "토픽 그룹", example = "SKU_NOTICE") - private TopicGroup topicGroup; + @NotBlank private String keyword; } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserDeviceToken.java b/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserDeviceToken.java index febbe40..487ee89 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserDeviceToken.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserDeviceToken.java @@ -1,4 +1,23 @@ package org.example.skuhomepage.domain.firebase.entity; -public class UserDeviceToken { +import jakarta.persistence.*; + +import org.example.skuhomepage.domain.mypage.entity.User; +import org.example.skuhomepage.global.common.BaseTimeEntity; + +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserDeviceToken extends BaseTimeEntity { + @Id @GeneratedValue private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @Column(nullable = false, unique = true) + private String fcmToken; } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserKeyword.java b/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserKeyword.java index cfcd233..0a2915a 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserKeyword.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/entity/UserKeyword.java @@ -1,4 +1,26 @@ package org.example.skuhomepage.domain.firebase.entity; -public class UserKeyword { +import jakarta.persistence.*; + +import org.example.skuhomepage.domain.mypage.entity.User; +import org.example.skuhomepage.global.common.BaseTimeEntity; + +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserKeyword extends BaseTimeEntity { + @Id @GeneratedValue private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @Column(nullable = false) + private String keyword; + + @Column(nullable = false) + private String topicGroup; } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/exception/FirebaseErrorStatus.java b/src/main/java/org/example/skuhomepage/domain/firebase/exception/FirebaseErrorStatus.java index 76299d8..7504f44 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/exception/FirebaseErrorStatus.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/exception/FirebaseErrorStatus.java @@ -1,4 +1,34 @@ package org.example.skuhomepage.domain.firebase.exception; -public class FirebaseErrorStatus { +import org.example.skuhomepage.global.apiPayload.code.BaseErrorCode; +import org.example.skuhomepage.global.apiPayload.code.ErrorReasonDTO; +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FirebaseErrorStatus implements BaseErrorCode { + KEYWORD_NOT_VALID(HttpStatus.BAD_REQUEST, "KEYWORD404001", "중복된 키워드입니다"), + KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "KEYWORD404002", "존재하지 않는 키워드입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder().message(message).code(code).isSuccess(false).build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build(); + } } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserDeviceTokenRepository.java b/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserDeviceTokenRepository.java index 647e9b4..c9d8455 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserDeviceTokenRepository.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserDeviceTokenRepository.java @@ -1,4 +1,16 @@ package org.example.skuhomepage.domain.firebase.repository; -public interface UserDeviceTokenRepository { +import java.util.List; +import java.util.Optional; + +import org.example.skuhomepage.domain.firebase.entity.UserDeviceToken; +import org.example.skuhomepage.domain.mypage.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserDeviceTokenRepository extends JpaRepository { + boolean existsByFcmToken(String fcmToken); + + Optional findByFcmToken(String fcmToken); + + List findAllByUser(User user); } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserKeywordRepository.java b/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserKeywordRepository.java index cf41a0f..9b9add8 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserKeywordRepository.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/repository/UserKeywordRepository.java @@ -1,4 +1,16 @@ package org.example.skuhomepage.domain.firebase.repository; -public interface UserKeywordRepository { +import java.util.List; +import java.util.Optional; + +import org.example.skuhomepage.domain.firebase.entity.UserKeyword; +import org.example.skuhomepage.domain.mypage.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserKeywordRepository extends JpaRepository { + boolean existsByUserAndKeyword(User user, String keyword); + + List findAllByUser(User user); + + Optional findByUserAndKeyword(User user, String keyword); } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/DeviceTokenService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/DeviceTokenService.java index fc4e3d3..30c6b4e 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/DeviceTokenService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/DeviceTokenService.java @@ -1,4 +1,51 @@ package org.example.skuhomepage.domain.firebase.service; +import org.example.skuhomepage.domain.firebase.dto.TokenRequestDTO; +import org.example.skuhomepage.domain.firebase.entity.UserDeviceToken; +import org.example.skuhomepage.domain.firebase.repository.UserDeviceTokenRepository; +import org.example.skuhomepage.domain.mypage.entity.User; +import org.example.skuhomepage.domain.mypage.exception.MyPageErrorStatus; +import org.example.skuhomepage.domain.mypage.repository.UserRepository; +import org.example.skuhomepage.global.exception.GeneralException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor public class DeviceTokenService { + + private final UserRepository userRepository; + private final UserDeviceTokenRepository tokenRepository; + + public void registerToken(TokenRequestDTO request, UserDetails userDetails) { + User user = + userRepository + .findByAccount(userDetails.getUsername()) + .orElseThrow(() -> new GeneralException(MyPageErrorStatus.USER_NOT_FOUND)); + + tokenRepository + .findByFcmToken(request.getFcmToken()) + .ifPresentOrElse( + existingToken -> { + // 이미 등록된 경우 + if (!existingToken.getUser().getId().equals(user.getId())) { + existingToken = + UserDeviceToken.builder() + .id(existingToken.getId()) + .fcmToken(existingToken.getFcmToken()) + .user(user) + .build(); + tokenRepository.save(existingToken); + } + }, + () -> { + // 새로 등록 + UserDeviceToken token = + UserDeviceToken.builder().user(user).fcmToken(request.getFcmToken()).build(); + + tokenRepository.save(token); + }); + } } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMService.java index e1228b9..9b750b1 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMService.java @@ -1,74 +1,63 @@ package org.example.skuhomepage.domain.firebase.service; -import java.time.LocalDateTime; - -import org.example.skuhomepage.domain.firebase.dto.TokenRequestDTO; -import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; -import org.example.skuhomepage.domain.firebase.repository.NotificationSubscribeRepository; -import org.example.skuhomepage.global.dto.MessageRequest; -import org.example.skuhomepage.global.utils.FirebaseUtils; -import org.example.skuhomepage.global.utils.RedisUtils; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@RequiredArgsConstructor -@Slf4j -public class FCMService { - - private final FirebaseUtils firebaseUtils; - private final RedisUtils redisUtils; - private final FCMTopicService fcmTopicService; - private final NotificationSubscribeRepository notificationSubscribeRepository; - - public void registerToken(TokenRequestDTO tokenReq, Long userId) { - redisUtils.saveTokenWithExpiry(userId, tokenReq.getToken(), 60 * 60); - - notificationSubscribeRepository - .findByUserId(userId) - .forEach( - notificationSubscribe -> - firebaseUtils.topicSubscribe( - notificationSubscribe.getTopic(), tokenReq.getToken())); - } - - public void sendTestMessage(TokenRequestDTO tokenReq) { - firebaseUtils.sendMessage( - MessageRequest.builder() - .title("테스트 알림입니다.") - .content("테스트 알림 내용입니다.") - .contentUrl("https://www.skuniv.ac.kr/index.php?mid=notice&page=1&document_srl=266092") - .inAppLink("/notice/266092") - .sendTime(LocalDateTime.now()) - .token(tokenReq.getToken()) - .build()); - } - - public void sendTestTopicMessage(TopicRequestDTO topicReq) { - firebaseUtils.sendTopicMessage( - MessageRequest.builder() - .title("테스트 알림입니다.") - .content("테스트 알림 내용입니다.") - .contentUrl("https://www.skuniv.ac.kr/index.php?mid=notice&page=1&document_srl=266092") - .inAppLink("/notice/266092") - .sendTime(LocalDateTime.now()) - .topic(fcmTopicService.topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword())) - .build()); - } - - public void sendTopicMessage(MessageRequest msgRequest, TopicRequestDTO topicReq) { - firebaseUtils.sendTopicMessage( - MessageRequest.builder() - .title(msgRequest.getTitle()) - .content(msgRequest.getContent()) - .contentUrl(msgRequest.getContentUrl()) - .inAppLink(msgRequest.getInAppLink()) - .sendTime(LocalDateTime.now()) - .topic(fcmTopicService.topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword())) - .build()); - - log.info("토픽 메시지 전송 완료: {}", msgRequest.getTopic()); - } -} +// @Service +// @RequiredArgsConstructor +// @Slf4j +// public class FCMService { +// +// private final FirebaseUtils firebaseUtils; +// private final RedisUtils redisUtils; +// private final FCMTopicService fcmTopicService; +// private final NotificationSubscribeRepository notificationSubscribeRepository; +// +// public void registerToken(TokenRequestDTO tokenReq, Long userId) { +// redisUtils.saveTokenWithExpiry(userId, tokenReq.getToken(), 60 * 60); +// +// notificationSubscribeRepository +// .findByUserId(userId) +// .forEach( +// notificationSubscribe -> +// firebaseUtils.topicSubscribe( +// notificationSubscribe.getTopic(), tokenReq.getToken())); +// } +// +// public void sendTestMessage(TokenRequestDTO tokenReq) { +// firebaseUtils.sendMessage( +// MessageRequest.builder() +// .title("테스트 알림입니다.") +// .content("테스트 알림 내용입니다.") +// +// .contentUrl("https://www.skuniv.ac.kr/index.php?mid=notice&page=1&document_srl=266092") +// .inAppLink("/notice/266092") +// .sendTime(LocalDateTime.now()) +// .token(tokenReq.getToken()) +// .build()); +// } +// +// public void sendTestTopicMessage(TopicRequestDTO topicReq) { +// firebaseUtils.sendTopicMessage( +// MessageRequest.builder() +// .title("테스트 알림입니다.") +// .content("테스트 알림 내용입니다.") +// +// .contentUrl("https://www.skuniv.ac.kr/index.php?mid=notice&page=1&document_srl=266092") +// .inAppLink("/notice/266092") +// .sendTime(LocalDateTime.now()) +// .topic(fcmTopicService.topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword())) +// .build()); +// } +// +// public void sendTopicMessage(MessageRequest msgRequest, TopicRequestDTO topicReq) { +// firebaseUtils.sendTopicMessage( +// MessageRequest.builder() +// .title(msgRequest.getTitle()) +// .content(msgRequest.getContent()) +// .contentUrl(msgRequest.getContentUrl()) +// .inAppLink(msgRequest.getInAppLink()) +// .sendTime(LocalDateTime.now()) +// .topic(fcmTopicService.topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword())) +// .build()); +// +// log.info("토픽 메시지 전송 완료: {}", msgRequest.getTopic()); +// } +// } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMTopicService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMTopicService.java index 2e4e180..351ff1e 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMTopicService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/FCMTopicService.java @@ -1,63 +1,51 @@ package org.example.skuhomepage.domain.firebase.service; -import java.util.Set; - -import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; -import org.example.skuhomepage.domain.firebase.entity.Topic; -import org.example.skuhomepage.domain.firebase.repository.TopicRepository; -import org.example.skuhomepage.global.enums.TopicGroup; -import org.example.skuhomepage.global.utils.FirebaseUtils; -import org.example.skuhomepage.global.utils.RedisUtils; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class FCMTopicService { - - private final TopicRepository topicRepository; - private final FirebaseUtils firebaseUtils; - private final RedisUtils redisUtils; - - public Topic saveTopic(String topic, TopicGroup topicGroup) { - - return topicRepository.save( - Topic.builder().topic(topic).topicGroup(topicGroup.getValue()).build()); - } - - public void registerTopic(TopicRequestDTO topicReq, long userId) { - getOrCreateTopicId(topicReq.getKeyword(), topicReq.getTopicGroup()); - Set tokens = redisUtils.getTokens(userId); - - String topicName = topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword()); - - for (String token : tokens) { - firebaseUtils.topicSubscribe(topicName, token); - } - } - - public void deleteTopic(TopicRequestDTO topicReq, long userId) { - - Set tokens = redisUtils.getTokens(userId); - String topicName = topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword()); - firebaseUtils.topicUnsubscribe(topicName, tokens.stream().toList()); - } - - public Long getOrCreateTopicId(String topic, TopicGroup topicGroup) { - - return topicRepository - .findByTopicAndTopicGroup(topic, topicGroup.getValue()) - .map(Topic::getId) - .orElseGet(() -> saveTopic(topic, topicGroup).getId()); - } - - public String topicCreator(TopicGroup topicGroup, String topicContent) { - Long topicId = getOrCreateTopicId(topicContent, topicGroup); - return topicGroup.getValue() + "-" + topicId.toString(); - } - - public String topicCreator(TopicGroup topicGroup) { - return topicGroup.getValue(); - } -} +// @Service +// @RequiredArgsConstructor +// public class FCMTopicService { +// +// private final TopicRepository topicRepository; +// private final FirebaseUtils firebaseUtils; +// private final RedisUtils redisUtils; +// +// public Topic saveTopic(String topic, TopicGroup topicGroup) { +// +// return topicRepository.save( +// Topic.builder().topic(topic).topicGroup(topicGroup.getValue()).build()); +// } +// +// public void registerTopic(TopicRequestDTO topicReq, long userId) { +// getOrCreateTopicId(topicReq.getKeyword(), topicReq.getTopicGroup()); +// Set tokens = redisUtils.getTokens(userId); +// +// String topicName = topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword()); +// +// for (String token : tokens) { +// firebaseUtils.topicSubscribe(topicName, token); +// } +// } +// +// public void deleteTopic(TopicRequestDTO topicReq, long userId) { +// +// Set tokens = redisUtils.getTokens(userId); +// String topicName = topicCreator(topicReq.getTopicGroup(), topicReq.getKeyword()); +// firebaseUtils.topicUnsubscribe(topicName, tokens.stream().toList()); +// } +// +// public Long getOrCreateTopicId(String topic, TopicGroup topicGroup) { +// +// return topicRepository +// .findByTopicAndTopicGroup(topic, topicGroup.getValue()) +// .map(Topic::getId) +// .orElseGet(() -> saveTopic(topic, topicGroup).getId()); +// } +// +// public String topicCreator(TopicGroup topicGroup, String topicContent) { +// Long topicId = getOrCreateTopicId(topicContent, topicGroup); +// return topicGroup.getValue() + "-" + topicId.toString(); +// } +// +// public String topicCreator(TopicGroup topicGroup) { +// return topicGroup.getValue(); +// } +// } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/KeywordService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/KeywordService.java index 7fbf5f3..6d8b8dd 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/KeywordService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/KeywordService.java @@ -1,4 +1,65 @@ package org.example.skuhomepage.domain.firebase.service; +import java.util.List; +import java.util.stream.Collectors; + +import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; +import org.example.skuhomepage.domain.firebase.entity.UserKeyword; +import org.example.skuhomepage.domain.firebase.exception.FirebaseErrorStatus; +import org.example.skuhomepage.domain.firebase.repository.UserKeywordRepository; +import org.example.skuhomepage.domain.mypage.entity.User; +import org.example.skuhomepage.domain.mypage.exception.MyPageErrorStatus; +import org.example.skuhomepage.domain.mypage.repository.UserRepository; +import org.example.skuhomepage.global.exception.GeneralException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor public class KeywordService { + + private final UserRepository userRepository; + private final UserKeywordRepository keywordRepository; + + public void registerKeyword(TopicRequestDTO request, UserDetails userDetails) { + User user = + userRepository + .findByAccount(userDetails.getUsername()) + .orElseThrow(() -> new GeneralException(MyPageErrorStatus.USER_NOT_FOUND)); + + boolean exists = keywordRepository.existsByUserAndKeyword(user, request.getKeyword()); + if (exists) { + throw new GeneralException(FirebaseErrorStatus.KEYWORD_NOT_VALID); + } + + UserKeyword keyword = UserKeyword.builder().user(user).keyword(request.getKeyword()).build(); + + keywordRepository.save(keyword); + } + + public List getUserKeywords(UserDetails userDetails) { + User user = + userRepository + .findByAccount(userDetails.getUsername()) + .orElseThrow(() -> new GeneralException(MyPageErrorStatus.USER_NOT_FOUND)); + + List keywords = keywordRepository.findAllByUser(user); + return keywords.stream().map(UserKeyword::getKeyword).collect(Collectors.toList()); + } + + public void deleteKeyword(String keywordToDelete, UserDetails userDetails) { + User user = + userRepository + .findByAccount(userDetails.getUsername()) + .orElseThrow(() -> new GeneralException(MyPageErrorStatus.USER_NOT_FOUND)); + + UserKeyword keyword = + keywordRepository + .findByUserAndKeyword(user, keywordToDelete) + .orElseThrow(() -> new GeneralException(FirebaseErrorStatus.KEYWORD_NOT_FOUND)); + + keywordRepository.delete(keyword); + } } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationService.java index 0b871d3..5809523 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationService.java @@ -1,4 +1,32 @@ package org.example.skuhomepage.domain.firebase.service; +import org.springframework.stereotype.Service; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor public class NotificationService { + + private final FirebaseMessaging firebaseMessaging; + + public void sendPush(String token, String title, String body) { + Message message = + Message.builder() + .setToken(token) + .setNotification(Notification.builder().setTitle(title).setBody(body).build()) + .build(); + + try { + String response = firebaseMessaging.send(message); + System.out.println("푸시 알림이 전송되었습니다. 토큰: " + token + ": " + response); + } catch (FirebaseMessagingException e) { + System.err.println("푸시 알림 전송 실패: " + token + " - " + e.getMessage()); + } + } } diff --git a/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationSubscribeService.java b/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationSubscribeService.java index 5790f96..8039239 100644 --- a/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationSubscribeService.java +++ b/src/main/java/org/example/skuhomepage/domain/firebase/service/NotificationSubscribeService.java @@ -2,6 +2,7 @@ import java.util.List; +import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; import org.example.skuhomepage.domain.firebase.entity.NotificationSubscribe; import org.example.skuhomepage.domain.firebase.repository.NotificationSubscribeRepository; import org.springframework.stereotype.Service; @@ -15,6 +16,8 @@ public class NotificationSubscribeService { private final NotificationSubscribeRepository notificationSubscribeRepository; + public void registerTopic(TopicRequestDTO topicReq, long userId) {} + public List getTokenListByTopic(String topic) { return notificationSubscribeRepository.findTokenByTopic(topic); } diff --git a/src/main/java/org/example/skuhomepage/domain/skunotice/service/SkuNoticeScrapService.java b/src/main/java/org/example/skuhomepage/domain/skunotice/service/SkuNoticeScrapService.java index f2d3951..bb43db9 100644 --- a/src/main/java/org/example/skuhomepage/domain/skunotice/service/SkuNoticeScrapService.java +++ b/src/main/java/org/example/skuhomepage/domain/skunotice/service/SkuNoticeScrapService.java @@ -5,12 +5,17 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import org.example.skuhomepage.domain.firebase.dto.TopicRequestDTO; -import org.example.skuhomepage.domain.firebase.repository.TopicRepository; -import org.example.skuhomepage.domain.firebase.service.FCMService; +import org.example.skuhomepage.domain.firebase.entity.UserDeviceToken; +import org.example.skuhomepage.domain.firebase.entity.UserKeyword; +import org.example.skuhomepage.domain.firebase.repository.UserDeviceTokenRepository; +import org.example.skuhomepage.domain.firebase.repository.UserKeywordRepository; +import org.example.skuhomepage.domain.firebase.service.NotificationService; +import org.example.skuhomepage.domain.mypage.entity.User; import org.example.skuhomepage.domain.skunotice.dto.SkuNoticeApiResponseDTO; import org.example.skuhomepage.domain.skunotice.dto.SkuNoticeApiResponseDTO.SkuNoticeApiResponse; import org.example.skuhomepage.domain.skunotice.dto.SkuNoticeResponseDTO.SkuNoticeDTO; @@ -19,7 +24,6 @@ import org.example.skuhomepage.domain.skunotice.enums.ECNoticeType; import org.example.skuhomepage.domain.skunotice.exception.SkuEcNoticeErrorStatus; import org.example.skuhomepage.domain.skunotice.repository.SkuNoticeRepository; -import org.example.skuhomepage.global.enums.TopicGroup; import org.example.skuhomepage.global.exception.GeneralException; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; @@ -41,8 +45,9 @@ public class SkuNoticeScrapService { private final RestTemplate restTemplate; private final SkuNoticeRepository skuNoticeRepository; - private final FCMService fcmService; - private final TopicRepository topicRepository; + private final NotificationService notificationService; + private final UserKeywordRepository userKeywordRepository; + private final UserDeviceTokenRepository userDeviceTokenRepository; @Value("${sku.notice.url}") private String skuNoticeApiUrl; @@ -124,41 +129,80 @@ public void saveAll(int startPage, int endPage) { log.info("모든 페이지 데이터 저장 완료"); } - @Scheduled(cron = "0 0/10 * * * ?") // 매 10분마다 실행 + // @Scheduled(cron = "0 0/10 * * * ?") // 매 10분마다 실행 + // public void saveNoticeTask() { + // SkuNoticeListDTO newNotices = save(1); + // + // if (newNotices.getSkuNoticeList().isEmpty()) return; + // + // List ecNoticeTopics = + // topicRepository.findTopicsByTopicGroup(TopicGroup.SKU_EC_NOTICE.getValue()); + // List noticeTopics = + // topicRepository.findTopicsByTopicGroup(TopicGroup.SKU_NOTICE.getValue()); + // + // for (SkuNoticeDTO notice : newNotices.getSkuNoticeList()) { + // TopicGroup topicGroup = TopicGroup.SKU_NOTICE; + // if (notice.getAuthor().equals(ECNoticeType.GYOSU_HAKSEUB.getValue()) + // || notice.getAuthor().equals(ECNoticeType.DAEHAK_HYEOKSIN.getValue()) + // || notice.getAuthor().equals(ECNoticeType.JINLO_CHWIEOB.getValue())) { + // for (String topic : ecNoticeTopics) { + // log.info("토픽 확인: {}, department: {}", topic, notice.getAuthor()); + // if (notice.getTitle().contains(topic)) { + // fcmService.sendTopicMessage( + // notice.toMessageRequest(), + // TopicRequestDTO.builder().topicGroup(topicGroup).keyword(topic).build()); + // } + // } + // } else { + // for (String topic : noticeTopics) { + // log.info("토픽 확인: {}, department: {}", topic, notice.getAuthor()); + // if (notice.getTitle().contains(topic)) { + // fcmService.sendTopicMessage( + // notice.toMessageRequest(), + // TopicRequestDTO.builder().topicGroup(topicGroup).keyword(topic).build()); + // } + // } + // } + // } + // } + @Scheduled(cron = "0 0/10 * * * ?") // 10분마다 실행 public void saveNoticeTask() { + System.out.println("[INFO] 공지사항 스크래핑 작업 시작"); + SkuNoticeListDTO newNotices = save(1); + if (newNotices.getSkuNoticeList().isEmpty()) { + System.out.println("[INFO] 새 공지사항 없음"); + return; + } - if (newNotices.getSkuNoticeList().isEmpty()) return; + System.out.println("[INFO] 새 공지사항 수: " + newNotices.getSkuNoticeList().size()); - List ecNoticeTopics = - topicRepository.findTopicsByTopicGroup(TopicGroup.SKU_EC_NOTICE.getValue()); - List noticeTopics = - topicRepository.findTopicsByTopicGroup(TopicGroup.SKU_NOTICE.getValue()); + List allKeywords = userKeywordRepository.findAll(); for (SkuNoticeDTO notice : newNotices.getSkuNoticeList()) { - TopicGroup topicGroup = TopicGroup.SKU_NOTICE; - if (notice.getAuthor().equals(ECNoticeType.GYOSU_HAKSEUB.getValue()) - || notice.getAuthor().equals(ECNoticeType.DAEHAK_HYEOKSIN.getValue()) - || notice.getAuthor().equals(ECNoticeType.JINLO_CHWIEOB.getValue())) { - for (String topic : ecNoticeTopics) { - log.info("토픽 확인: {}, department: {}", topic, notice.getAuthor()); - if (notice.getTitle().contains(topic)) { - fcmService.sendTopicMessage( - notice.toMessageRequest(), - TopicRequestDTO.builder().topicGroup(topicGroup).keyword(topic).build()); - } + Set matchedUsers = new HashSet<>(); + + for (UserKeyword keyword : allKeywords) { + if (notice.getTitle().contains(keyword.getKeyword())) { + matchedUsers.add(keyword.getUser()); } - } else { - for (String topic : noticeTopics) { - log.info("토픽 확인: {}, department: {}", topic, notice.getAuthor()); - if (notice.getTitle().contains(topic)) { - fcmService.sendTopicMessage( - notice.toMessageRequest(), - TopicRequestDTO.builder().topicGroup(topicGroup).keyword(topic).build()); - } + } + + if (!matchedUsers.isEmpty()) { + System.out.println( + "[INFO] '" + notice.getTitle() + "' 키워드 알림 대상자 수: " + matchedUsers.size()); + } + + for (User user : matchedUsers) { + List tokens = userDeviceTokenRepository.findAllByUser(user); + for (UserDeviceToken token : tokens) { + System.out.println("[INFO] 사용자 " + user.getId() + " 에게 푸시 전송: " + token.getFcmToken()); + notificationService.sendPush(token.getFcmToken(), notice.getTitle(), "등록되었습니다"); } } } + + System.out.println("[INFO] 공지사항 스크래핑 작업 종료"); } private LocalDateTime parseDate(String dateString) { diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableController.java b/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableController.java index 334b6d4..c9af517 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableController.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableController.java @@ -61,4 +61,12 @@ public ApiResponse deleteSubject( timeTableService.deleteSubject(userDetails, subjectId); return ApiResponse.onSuccess(result); } + + @Override + public ApiResponse dailyTimeTable( + UserDetails userDetails, String dayOfWeek) { + TimeTableResponseDTO.MyTimeTableDTO result = + timeTableService.getSubjectsByDay(userDetails, dayOfWeek); + return ApiResponse.onSuccess(result); + } } diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableControllerSpec.java b/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableControllerSpec.java index 9c7b2e4..5d16e38 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableControllerSpec.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/controller/TimeTableControllerSpec.java @@ -73,4 +73,13 @@ ApiResponse deleteSubject( UserDetails userDetails, @Parameter(name = "subjectId", description = "과목 아이디", required = true) @PathVariable Long subjectId); + + @GetMapping("/weekly/{dayOfWeek}") + @Operation(summary = "요일별 시간표 조회하기", description = "요일별 시간표 조회하기") + @ApiErrorCodeExample(TimeTableErrorStatus.class) + ApiResponse dailyTimeTable( + @Parameter(name = "userDetails", description = "인증된 사용자 정보", hidden = true) + @AuthenticationPrincipal + UserDetails userDetails, + @Parameter(description = "조회할 요일", example = "MONDAY") @PathVariable String dayOfWeek); } diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/dto/TimeTableResponseDTO.java b/src/main/java/org/example/skuhomepage/domain/timetable/dto/TimeTableResponseDTO.java index bef8bb4..368a766 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/dto/TimeTableResponseDTO.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/dto/TimeTableResponseDTO.java @@ -41,8 +41,8 @@ public record TimeTableDTO( @Schema(description = "교수 이름", example = "이지영") String professor, @Schema(description = "수업 시간", example = "월 1교시") String time, @Schema(description = "장소", example = "북악관 608호") String classroom, - @Schema(description = "학점", example = "3") int credit, - @Schema(description = "학년", example = "1") int grade, + @Schema(description = "학점", example = "3") String credit, + @Schema(description = "학년", example = "1") String grade, @Schema(description = "수강 대상", example = "컴퓨터공학과") String target, @Schema(description = "구분", example = "전공") String division) { public TimeTableDTO(Subject subject) { diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/entity/Subject.java b/src/main/java/org/example/skuhomepage/domain/timetable/entity/Subject.java index 91359dd..efa9f3c 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/entity/Subject.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/entity/Subject.java @@ -48,17 +48,17 @@ public class Subject extends BaseTimeEntity { @Comment("학점") @Column(nullable = false) - private int credit; + private String credit; @Comment("학년") @Column(nullable = false) - private int grade; + private String grade; @Comment("수강대상") @Column(nullable = false) private String target; - @Comment("구분") + @Comment("이수구분") @Enumerated(EnumType.STRING) private SubjectType division; } diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/entity/SubjectType.java b/src/main/java/org/example/skuhomepage/domain/timetable/entity/SubjectType.java index 705fa4a..7c16f7f 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/entity/SubjectType.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/entity/SubjectType.java @@ -9,5 +9,11 @@ public enum SubjectType { 전핵, 전심, 전공, - 융합 + 융합, + 핵심역량교양필수, + 핵심역량교양선택, + 영어, + 중어, + 일어, + 기초학문교양선택, } diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/repository/TimeTableSubjectRepository.java b/src/main/java/org/example/skuhomepage/domain/timetable/repository/TimeTableSubjectRepository.java index 7d5b33c..543c978 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/repository/TimeTableSubjectRepository.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/repository/TimeTableSubjectRepository.java @@ -1,5 +1,6 @@ package org.example.skuhomepage.domain.timetable.repository; +import java.util.List; import java.util.Optional; import org.example.skuhomepage.domain.timetable.entity.Subject; @@ -17,4 +18,6 @@ public interface TimeTableSubjectRepository extends JpaRepository findByUserAccountBySubjectId( @Param("account") String account, @Param("subjectId") Long subjectId); + + List findAllByTimeTable(TimeTable timeTable); } diff --git a/src/main/java/org/example/skuhomepage/domain/timetable/service/TimeTableService.java b/src/main/java/org/example/skuhomepage/domain/timetable/service/TimeTableService.java index 54aa894..0b25002 100644 --- a/src/main/java/org/example/skuhomepage/domain/timetable/service/TimeTableService.java +++ b/src/main/java/org/example/skuhomepage/domain/timetable/service/TimeTableService.java @@ -139,9 +139,9 @@ public AddSubjectDTO addSelfSubject(UserDetails userDetails, selfSubjectDTO requ .subject(request.getSubject()) .time(request.getTime()) .classroom(request.getClassroom()) - .credit(0) + .credit("") .professor("") - .grade(0) + .grade("") .target("") .division(SubjectType.자유선택) .build()); @@ -177,6 +177,33 @@ public DeleteSubjectDTO deleteSubject(UserDetails userDetails, Long subjectId) { return new DeleteSubjectDTO(subjectId); } + public TimeTableResponseDTO.MyTimeTableDTO getSubjectsByDay( + UserDetails userDetails, String dayOfWeek) { + DayOfWeek targetDay = DayOfWeek.valueOf(dayOfWeek.toUpperCase()); + + TimeTable myTimeTable = + timeTableRepository + .findByUser_Account(userDetails.getUsername()) + .orElseThrow(() -> new GeneralException(TimeTableErrorStatus.TIME_TABLE_NOT_FOUND)); + + List mySubjects = timeTableSubjectRepository.findAllByTimeTable(myTimeTable); + + List subjects = + mySubjects.stream() + .map(TimeTableSubject::getSubject) + .filter(subject -> isSubjectOnToday(subject, targetDay)) + .map( + subject -> + new TimeTableResponseDTO.MySubjectDTO( + subject.getId(), + subject.getSubject(), + subject.getTime(), + subject.getClassroom())) + .collect(Collectors.toList()); + + return new TimeTableResponseDTO.MyTimeTableDTO(subjects); + } + private boolean isSubjectOnToday(Subject subject, DayOfWeek today) { String time = subject.getTime(); diff --git a/src/main/java/org/example/skuhomepage/global/config/FirebaseConfig.java b/src/main/java/org/example/skuhomepage/global/config/FirebaseConfig.java index c580034..53e33d9 100644 --- a/src/main/java/org/example/skuhomepage/global/config/FirebaseConfig.java +++ b/src/main/java/org/example/skuhomepage/global/config/FirebaseConfig.java @@ -7,12 +7,14 @@ import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; import lombok.extern.slf4j.Slf4j; @@ -46,4 +48,9 @@ public void initialize() { log.error("Firebase Admin SDK 초기화 중 오류가 발생했습니다.", e); } } + + @Bean + public FirebaseMessaging firebaseMessaging() { + return FirebaseMessaging.getInstance(); + } }