diff --git a/chat/build.gradle.kts b/chat/build.gradle.kts index 9c1b312d..659fc8e4 100644 --- a/chat/build.gradle.kts +++ b/chat/build.gradle.kts @@ -54,6 +54,9 @@ dependencies { // Redis implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // Slack + implementation("com.slack.api:slack-api-client:1.45.3") } tasks.bootJar { diff --git a/chat/src/main/java/org/example/soundlinkchat_java/global/auth/JwtProvider.java b/chat/src/main/java/org/example/soundlinkchat_java/global/auth/JwtProvider.java index 4a95b18a..a9fc29cc 100644 --- a/chat/src/main/java/org/example/soundlinkchat_java/global/auth/JwtProvider.java +++ b/chat/src/main/java/org/example/soundlinkchat_java/global/auth/JwtProvider.java @@ -1,21 +1,21 @@ package org.example.soundlinkchat_java.global.auth; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; +import io.jsonwebtoken.*; import javax.crypto.SecretKey; -import java.util.Date; +import java.util.*; import java.util.concurrent.TimeUnit; +@Slf4j @Component @RequiredArgsConstructor public class JwtProvider { @@ -31,18 +31,25 @@ public class JwtProvider { private long REFRESH_EXPIRATION_TIME; // 시크릿 키 - private final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("ee7d4dcf88086125155386d999b3a2258d5c55671a390e608f49a2db31efc6e0".getBytes()); + @Value("${jwt.secret}") + private String SECRET_KEY_STRING; + private SecretKey SECRET_KEY; + + @PostConstruct + public void init() { + this.SECRET_KEY = Keys.hmacShaKeyFor(SECRET_KEY_STRING.getBytes()); + } // Access 토큰 public String createAccessToken(long userId) { Claims claims = Jwts.claims().setSubject(String.valueOf(userId)); Date now = new Date(); - return Jwts.builder() .setClaims(claims) .setIssuedAt(now) - .setExpiration(new Date(now.getTime()+ACCESS_EXPIRATION_TIME)) - .signWith(SECRET_KEY, SignatureAlgorithm.HS256) + .setExpiration(new Date(now.getTime() + ACCESS_EXPIRATION_TIME)) + .setHeaderParam("typ", "JWT") + .signWith(SECRET_KEY,SignatureAlgorithm.HS256) .compact(); } @@ -54,6 +61,7 @@ public String createRefreshToken(long userId) { .setClaims(claims) .setIssuedAt(now) .setExpiration(new Date(now.getTime()+REFRESH_EXPIRATION_TIME)) + .setHeaderParam("typ", "JWT") .signWith(SECRET_KEY, SignatureAlgorithm.HS256) .compact(); try { @@ -66,30 +74,23 @@ public String createRefreshToken(long userId) { } //토큰 검증(변조, 만료, 올바른 형식) - public boolean validateToken(String token){ + public boolean validateToken(String token) { try { Jwts.parserBuilder() - .setSigningKey(SECRET_KEY) //서명 검증 + .setSigningKey(SECRET_KEY) // 서명 검증 .build() - .parseClaimsJws(token); //토큰 유효한지 확인. + .parseClaimsJws(token); // 토큰 유효한지 확인 (여기서 만료 시간도 체크) + + // 토큰이 유효한 경우 return true; + }catch (ExpiredJwtException e) { + log.warn("[ERROR] Token is expired."); + throw e; + } catch (JwtException e) { + log.warn("[ERROR] Token validation failed: {}", e.getMessage()); + throw e; } catch (Exception e) { - System.out.println("[ERROR] Token validation failed: "); - return false; - } - } - - public boolean isTokenExpired(String token) { - try { - Jwts.parserBuilder() - .setSigningKey(SECRET_KEY) - .build() - .parseClaimsJws(token); // 만료된 토큰을 처리하려면 ExpiredJwtException이 발생함 - return false; // 만료되지 않으면 false - } catch (ExpiredJwtException ex) { - return true; // 만료된 경우 true - } catch (Exception ex) { - return false; // 다른 예외는 false + throw e; } } @@ -123,5 +124,4 @@ public Long getUserId(String token){ .getBody() .getSubject()); } - } \ No newline at end of file diff --git a/chat/src/main/java/org/example/soundlinkchat_java/global/config/RedisConfig.java b/chat/src/main/java/org/example/soundlinkchat_java/global/config/RedisConfig.java index c4ae218c..d1342d6b 100644 --- a/chat/src/main/java/org/example/soundlinkchat_java/global/config/RedisConfig.java +++ b/chat/src/main/java/org/example/soundlinkchat_java/global/config/RedisConfig.java @@ -7,6 +7,7 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -21,22 +22,27 @@ public class RedisConfig { @Value("${spring.data.redis.host}") private String host; + @Value("${spring.data.redis.port}") - private int port; + private Integer port; + + @Value("${spring.data.redis.password}") + private String password; @Bean public RedisConnectionFactory redisConnectionFactory(){ - return new LettuceConnectionFactory(host, port); + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port); + config.setPassword(password); + + return new LettuceConnectionFactory(config); } @Bean public RedisTemplate redisTemplate(){ RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); - //key,value를 email,authCode로 구현. String으로 직렬화 - redisTemplate.setKeySerializer(new StringRedisSerializer()); - //redisTemplate.setValueSerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setKeySerializer(keySerializer()); + redisTemplate.setValueSerializer(valueSerializer()); return redisTemplate; } @@ -44,11 +50,19 @@ public RedisTemplate redisTemplate(){ @Bean public CacheManager contentCacheManager(RedisConnectionFactory cf) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경 - .entryTtl(Duration.ofMinutes(60L)); // 캐시 수명 10분 + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())) + .entryTtl(Duration.ofMinutes(30L)); return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); } + + private GenericJackson2JsonRedisSerializer valueSerializer() { + return new GenericJackson2JsonRedisSerializer(); + } + + private StringRedisSerializer keySerializer() { + return new StringRedisSerializer(); + } } diff --git a/default/build.gradle.kts b/default/build.gradle.kts index 80656a55..7aaaa02c 100644 --- a/default/build.gradle.kts +++ b/default/build.gradle.kts @@ -88,6 +88,9 @@ dependencies { implementation ("org.apache.kafka:kafka-streams") // Kafka의 스트림 API를 사용할 때 필요 implementation ("org.apache.kafka:kafka-clients") // Kafka 브로커와 직접 통신하는 기본 클라이언트 라이브러리 + // Slack + implementation("com.slack.api:slack-api-client:1.45.3") + //QueryDSL 추가 implementation ("com.querydsl:querydsl-apt:5.0.0") implementation ("com.querydsl:querydsl-jpa:5.0.0:jakarta") diff --git a/default/src/main/java/org/dfbf/soundlink/domain/alert/service/AlertService.java b/default/src/main/java/org/dfbf/soundlink/domain/alert/service/AlertService.java index 6e4c3d8b..87864626 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/alert/service/AlertService.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/alert/service/AlertService.java @@ -6,6 +6,7 @@ import org.dfbf.soundlink.domain.alert.repository.AlertRepository; import org.dfbf.soundlink.global.exception.ErrorCode; import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -21,6 +22,7 @@ public class AlertService { private final AlertRepository alertRepository; + private final SlackService slackService; // 60 * 1000 * 60 = 3,600,000{ms} = 1시간 private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; @@ -54,6 +56,7 @@ public SseEmitter connectAlarm(Long id, String lastEventId) { .data("connect completed!!") ); } catch (IOException e) { + slackService.sendMsg(id, e.getMessage()); log.error("Error sending ping", e); } @@ -98,6 +101,7 @@ public ResponseResult send(Long userId, String alertName, Object data) { return new ResponseResult(ErrorCode.SUCCESS); } catch (IOException e) { + slackService.sendMsg(data, e.getMessage()); alertRepository.delete(userId, emitterId); return new ResponseResult(ErrorCode.BAD_REQUEST_STATUS, e.getMessage()); } diff --git a/default/src/main/java/org/dfbf/soundlink/domain/blocklist/service/BlockListService.java b/default/src/main/java/org/dfbf/soundlink/domain/blocklist/service/BlockListService.java index a85fb51c..ff355902 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/blocklist/service/BlockListService.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/blocklist/service/BlockListService.java @@ -14,6 +14,7 @@ import org.dfbf.soundlink.domain.user.repository.UserRepository; import org.dfbf.soundlink.global.exception.ErrorCode; import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.stereotype.Service; import java.util.List; @@ -25,6 +26,7 @@ public class BlockListService { private final BlockListQueryRepository blockListQueryRepository; private final UserRepository userRepository; private final BlockListRepository blockListRepository; + private final SlackService slackService; @Transactional public ResponseResult blockUser(Long userId, BlockReq req) { @@ -54,16 +56,19 @@ public ResponseResult blockUser(Long userId, BlockReq req) { ErrorCode.SUCCESS ); } catch (BlockedUserNotFound e) { + slackService.sendMsg(req, e.getMessage()); return new ResponseResult( ErrorCode.BLOCKED_USER_NOT_FOUND, e.getMessage() ); } catch (BlockingUserNotFound e) { + slackService.sendMsg(req, e.getMessage()); return new ResponseResult( ErrorCode.BLOCKING_USER_NOT_FOUND, e.getMessage() ); } catch (AlreadyBlockedUser e) { + slackService.sendMsg(req, e.getMessage()); return new ResponseResult( ErrorCode.ALREADY_BLOCKED_USER, e.getMessage() @@ -84,6 +89,7 @@ public ResponseResult unblockUser(Long userId, Long blocklistId) { ErrorCode.SUCCESS ); } catch (BlockingUserNotFound e) { + slackService.sendMsg(blocklistId, e.getMessage()); return new ResponseResult( ErrorCode.BLOCKING_USER_NOT_FOUND, e.getMessage() diff --git a/default/src/main/java/org/dfbf/soundlink/domain/chat/service/ChatRoomService.java b/default/src/main/java/org/dfbf/soundlink/domain/chat/service/ChatRoomService.java index 9bd5d3be..4f40daca 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/chat/service/ChatRoomService.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/chat/service/ChatRoomService.java @@ -27,6 +27,7 @@ import org.dfbf.soundlink.global.exception.ResponseResult; import org.dfbf.soundlink.global.feign.chat.DevChatClient; import org.dfbf.soundlink.global.kafka.KafkaProducer; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Service; @@ -51,6 +52,7 @@ public class ChatRoomService { private final DevChatClient devChatClient; private final KafkaProducer kafkaProducer; private final UserStatusService userStatusService; + private final SlackService slackService; private static final String CHAT_REQUEST_KEY = "chatRequest"; private static final String TOPIC = "alert-topic"; @@ -111,13 +113,17 @@ public ResponseResult saveRequestToRedis(Long requestUserId, Long emotionRecordI return new ResponseResult(ErrorCode.SUCCESS); } catch (IllegalArgumentException e) { + slackService.sendMsg(requestUserId, e.getMessage()); log.info(e.getMessage()); return new ResponseResult(ErrorCode.CHAT_REQUEST_SSE_FAILED); } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(requestUserId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD, e.getMessage()); } catch (UserNotFoundException e) { + slackService.sendMsg(requestUserId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(requestUserId, e.getMessage()); return new ResponseResult(ErrorCode.CHAT_REQUEST_FAILED, e.getMessage()); } } @@ -144,11 +150,14 @@ public ResponseResult deleteRequestFromRedis(Long userId, Long emotionRecordId) } } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD); } catch (UserNotFoundException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER); } catch (Exception e) { log.error(e.getMessage()); + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(400, "Failed to delete the chat request."); } } @@ -180,10 +189,13 @@ public ResponseResult requestRejected(Long responseUserId, ChatRejectDto chatRej } } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(chatRejectDto, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD); } catch (UserNotFoundException e) { + slackService.sendMsg(chatRejectDto, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER); } catch (Exception e) { + slackService.sendMsg(chatRejectDto, e.getMessage()); log.error(e.getMessage()); return new ResponseResult(400, "Failed to reject the chat request."); } @@ -266,10 +278,13 @@ public ResponseResult createChatRoom(Long userId, Long recordId, String requestN return new ResponseResult(400, "ChatRequest not found or expired."); } } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD, e.getMessage()); } catch (NoUserDataException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -298,6 +313,7 @@ public ResponseResult closeChatRoom(@AuthenticationPrincipal Long userId, Long c return new ResponseResult(ErrorCode.SUCCESS); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -324,6 +340,7 @@ public ResponseResult getChatRoomList(@AuthenticationPrincipal Long userId) { .toList(); return new ResponseResult(ErrorCode.SUCCESS, chatRoomList); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -351,10 +368,13 @@ public ResponseResult getChatRoomInfo(Long chatRoomId, Long userId) { ); return new ResponseResult(ErrorCode.SUCCESS, infoDto); } catch (ChatRoomNotFoundException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.CHATROOM_NOT_FOUND, "채팅방을 찾을 수 없습니다."); }catch (UnauthorizedAccessException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.CHAT_UNAUTHORIZED,"권한이 없습니다."); }catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, "채팅방 세부 정보를 가져오는 데 실패했습니다."); } } diff --git a/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/mock/TestDataInitializer.java b/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/mock/TestDataInitializer.java index 3d117c26..0f558e35 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/mock/TestDataInitializer.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/mock/TestDataInitializer.java @@ -2,6 +2,8 @@ import lombok.RequiredArgsConstructor; import org.dfbf.soundlink.domain.user.repository.UserRepository; +import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @@ -11,18 +13,11 @@ public class TestDataInitializer implements CommandLineRunner { private final UserRepository userRepository; + private final SlackService slackService; + @Override public void run(String... args) { -// User testUser = User.builder() -// .nickName("테스트유저") -// .socialId(1212L) -// .socialType(SocialType.NONE) -// .loginId("user1") -// .password("1212") -// .email("test@example.com") -// .build(); -// -// userRepository.save(testUser); - System.out.println("Hello, World!"); + // slackService.sendMsg(new ResponseResult(200, "OK"), "흥칫뿡"); + System.out.println("Server ON"); } } diff --git a/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/service/EmotionRecordService.java b/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/service/EmotionRecordService.java index 939fea4b..9d04383a 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/service/EmotionRecordService.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/emotionRecord/service/EmotionRecordService.java @@ -20,6 +20,7 @@ import org.dfbf.soundlink.global.comm.enums.Emotions; import org.dfbf.soundlink.global.exception.ErrorCode; import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -41,6 +42,7 @@ public class EmotionRecordService { private final SpotifyMusicRepository spotifyMusicRepository; private final EmotionRecordRepository emotionRecordRepository; private final UserRepository userRepository; + private final SlackService slackService; // private final EmotionRecordCacheService emotionRecordCacheService; private final ChatRoomRepository chatRoomRepository; @@ -81,10 +83,13 @@ public ResponseResult saveEmotionRecordWithMusic(Long userId, EmotionRecordReque return new ResponseResult(ErrorCode.SUCCESS); } catch (UserNotFoundException e) { + slackService.sendMsg(request, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (DataAccessException e) { + slackService.sendMsg(request, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(request, e.getMessage()); log.error("감정기록 저장 서버 에러 {}", e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } @@ -109,8 +114,10 @@ public ResponseResult getEmotionRecordsByUserId(Long userId, int page, int size) return new ResponseResult(ErrorCode.SUCCESS, EmotionRecordPageResponseDTO.fromPage(recordsPage, dtoList)); } catch (DataAccessException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -135,8 +142,10 @@ public ResponseResult getEmotionRecordsByLoginId(String userTag, int page, int s return new ResponseResult(ErrorCode.SUCCESS, EmotionRecordPageResponseDTO.fromPage(recordsPage, dtoList)); } catch (DataAccessException e) { + slackService.sendMsg(userTag, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userTag, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -156,6 +165,7 @@ public ResponseResult getEmotionRecordsExcludingUserIdByFilters(Long userId, Lis .map(e -> Emotions.valueOf(e.toUpperCase())) .toList(); } catch (IllegalArgumentException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION, "잘못된 감정 값이 포함되어 있습니다."); } } @@ -169,8 +179,10 @@ public ResponseResult getEmotionRecordsExcludingUserIdByFilters(Long userId, Lis return new ResponseResult(ErrorCode.SUCCESS, EmotionRecordPageResponseDTO.fromPage(recordsPage, dtoList)); } catch (DataAccessException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -184,10 +196,13 @@ public ResponseResult getEmotionRecord(Long userId, Long recordId) { return new ResponseResult(ErrorCode.SUCCESS, EmotionRecordResponseWithOwnerDTO.fromEntity(records, userId)); } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD, e.getMessage()); } catch (DataAccessException e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -202,10 +217,13 @@ public ResponseResult getVideoIdBySpotifyId(String spotifyId) { String videoId = music.getVideoId(); return new ResponseResult(ErrorCode.SUCCESS, videoId); } catch (SpotifyMusicNotFoundException e) { + slackService.sendMsg(spotifyId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_SPOTIFY_MUSIC, e.getMessage()); } catch (DataAccessException e) { + slackService.sendMsg(spotifyId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(spotifyId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -250,10 +268,13 @@ public ResponseResult updateEmotionRecord(Long recordId, EmotionRecordUpdateRequ );*/ return new ResponseResult(ErrorCode.SUCCESS, responseDTO); } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD, e.getMessage()); } catch (DataAccessException e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -299,10 +320,13 @@ public ResponseResult deleteEmotionRecord(Long recordId) { } return new ResponseResult(ErrorCode.SUCCESS, "감정 기록이 성공적으로 삭제되었습니다."); } catch (EmotionRecordNotFoundException e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_EMOTION_RECORD, e.getMessage()); } catch (DataAccessException e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(recordId, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -316,6 +340,7 @@ private ResponseResult validateAndCreatePageable(int page, int size) { Pageable pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").descending()); return new ResponseResult(ErrorCode.SUCCESS, pageable); } catch (IllegalArgumentException e) { + slackService.sendMsg(page, e.getMessage()); return new ResponseResult(ErrorCode.INVALID_PAGE_REQUEST, "페이지 요청 값이 잘못되었습니다."); } } diff --git a/default/src/main/java/org/dfbf/soundlink/domain/user/service/UserService.java b/default/src/main/java/org/dfbf/soundlink/domain/user/service/UserService.java index c49a6dd9..bc560848 100644 --- a/default/src/main/java/org/dfbf/soundlink/domain/user/service/UserService.java +++ b/default/src/main/java/org/dfbf/soundlink/domain/user/service/UserService.java @@ -28,6 +28,7 @@ import org.dfbf.soundlink.global.auth.TokenProperties; import org.dfbf.soundlink.global.exception.ErrorCode; import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseCookie; @@ -57,6 +58,7 @@ public class UserService { private final MailService mailService; private final RedisService redisService; private final UserStatusService userStatusService; + private final SlackService slackService; private final JwtProvider jwtProvider; private final TokenProperties tokenProperties; @@ -80,6 +82,7 @@ public ResponseResult signUp(UserSignUpDto userSignUpDto) { userRepository.save(userSignUpDto.toEntity(passwordEncoder)); return new ResponseResult(ErrorCode.SUCCESS); } catch (Exception e) { + slackService.sendMsg(userSignUpDto, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } } @@ -95,6 +98,7 @@ public ResponseResult getUser(Long userId) { return new ResponseResult(ErrorCode.SUCCESS, result); } catch (NoUserDataException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } } @@ -141,8 +145,10 @@ public ResponseResult updateUser(Long userId, UserUpdateDto userUpdateDto) { return new ResponseResult(ErrorCode.SUCCESS); } catch (NoUserDataException e) { + slackService.sendMsg(userUpdateDto, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(userUpdateDto, e.getMessage()); return new ResponseResult(ErrorCode.BAD_REQUEST, e.getMessage()); } } @@ -169,8 +175,10 @@ public ResponseResult deleteUser(Long userId) { return new ResponseResult(ErrorCode.SUCCESS); } catch (NoUserDataException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } } @@ -204,8 +212,10 @@ public ResponseResult getMyPage(Long userId) { return new ResponseResult(ErrorCode.SUCCESS, result); } catch (NoUserDataException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } } @@ -218,6 +228,7 @@ public ResponseResult sendAuthCode(String email) { redisService.setCode(email, authCode); return new ResponseResult(ErrorCode.SUCCESS, email); } catch (MessagingException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.EMAIL_SEND_ERROR, e.getMessage()); } } @@ -233,6 +244,7 @@ public ResponseResult validateAuthCode(String email, String authCode){ return new ResponseResult(ErrorCode.BAD_REQUEST, "이메일 전송 실패: 잘못된 요청입니다."); } } catch (AuthenticationException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.EMAIL_SEND_ERROR, e.getMessage()); } } @@ -318,6 +330,7 @@ public ResponseResult login(LoginReqDto loginReqDto, HttpServletResponse respons return new ResponseResult(responseBody); } catch (Exception e) { log.info("[ERROR] " + e.getMessage()); + slackService.sendMsg(loginReqDto, e.getMessage()); return new ResponseResult(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); } } @@ -350,6 +363,7 @@ public ResponseResult logout(HttpServletResponse response, HttpServletRequest re } catch (Exception e) { log.info("[ERROR] " + e.getMessage()); + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode. INTERNAL_SERVER_ERROR,"로그아웃 중 오류가 발생했습니다."); } } @@ -363,6 +377,7 @@ public ResponseResult checkLoginiId(String loginId) { return new ResponseResult(ErrorCode.NOT_DUPLICATE_LOGINID); } } catch (Exception e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } } @@ -378,8 +393,10 @@ public ResponseResult getProfile(String tag) { return new ResponseResult(ErrorCode.SUCCESS, result); } catch (NoUserDataException e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.FAIL_TO_FIND_USER, e.getMessage()); } catch (Exception e) { + slackService.sendMsg(null, e.getMessage()); return new ResponseResult(ErrorCode.DB_ERROR, e.getMessage()); } } diff --git a/default/src/main/java/org/dfbf/soundlink/global/auth/JwtProvider.java b/default/src/main/java/org/dfbf/soundlink/global/auth/JwtProvider.java index 29207e19..95b0dba0 100644 --- a/default/src/main/java/org/dfbf/soundlink/global/auth/JwtProvider.java +++ b/default/src/main/java/org/dfbf/soundlink/global/auth/JwtProvider.java @@ -1,44 +1,53 @@ package org.dfbf.soundlink.global.auth; import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.dfbf.soundlink.domain.user.exception.CustomJwtException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import io.jsonwebtoken.*; import javax.crypto.SecretKey; -import java.util.Date; +import java.util.*; import java.util.concurrent.TimeUnit; @Slf4j @Component +@RequiredArgsConstructor public class JwtProvider { - // 토큰(Access,Refresh) 만료시간(ms) + private final RedisTemplate redisTemplate; + + // 토큰(Access) 만료시간(ms) @Value("${ACCESS_TOKEN_EXPIRATION_TIME}") private long ACCESS_EXPIRATION_TIME; + // 토큰(Refresh) 만료시간(ms) @Value("${REFRESH_TOKEN_EXPIRATION_TIME}") private long REFRESH_EXPIRATION_TIME; - //시크릿 키 - SecretKey SECRET_KEY = Keys.hmacShaKeyFor("ee7d4dcf88086125155386d999b3a2258d5c55671a390e608f49a2db31efc6e0".getBytes()); + // 시크릿 키 + @Value("${jwt.secret}") + private String SECRET_KEY_STRING; + private SecretKey SECRET_KEY; + + @PostConstruct + public void init() { + this.SECRET_KEY = Keys.hmacShaKeyFor(SECRET_KEY_STRING.getBytes()); + } - @Autowired - private RedisTemplate redisTemplate; - //Access 토큰 + // Access 토큰 public String createAccessToken(long userId) { Claims claims = Jwts.claims().setSubject(String.valueOf(userId)); Date now = new Date(); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) - .setExpiration(new Date(now.getTime()+ACCESS_EXPIRATION_TIME)) + .setExpiration(new Date(now.getTime() + ACCESS_EXPIRATION_TIME)) .setHeaderParam("typ", "JWT") .signWith(SECRET_KEY,SignatureAlgorithm.HS256) .compact(); diff --git a/default/src/main/java/org/dfbf/soundlink/global/config/RedisConfig.java b/default/src/main/java/org/dfbf/soundlink/global/config/RedisConfig.java index 6a197b60..c0b0008c 100644 --- a/default/src/main/java/org/dfbf/soundlink/global/config/RedisConfig.java +++ b/default/src/main/java/org/dfbf/soundlink/global/config/RedisConfig.java @@ -7,6 +7,7 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -21,20 +22,27 @@ public class RedisConfig { @Value("${spring.data.redis.host}") private String host; + @Value("${spring.data.redis.port}") - private int port; + private Integer port; + + @Value("${spring.data.redis.password}") + private String password; @Bean public RedisConnectionFactory redisConnectionFactory(){ - return new LettuceConnectionFactory(host, port); + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port); + config.setPassword(password); + + return new LettuceConnectionFactory(config); } @Bean public RedisTemplate redisTemplate(){ RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); - redisTemplate.setKeySerializer(new StringRedisSerializer()); //key,value를 email,authCode로 구현. String으로 직렬화 - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setKeySerializer(keySerializer()); + redisTemplate.setValueSerializer(valueSerializer()); return redisTemplate; } @@ -42,10 +50,18 @@ public RedisTemplate redisTemplate(){ @Bean public CacheManager contentCacheManager(RedisConnectionFactory cf) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경 - .entryTtl(Duration.ofMinutes(30L)); // 캐시 수명 30분 + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())) + .entryTtl(Duration.ofMinutes(30L)); return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build(); } + + private GenericJackson2JsonRedisSerializer valueSerializer() { + return new GenericJackson2JsonRedisSerializer(); + } + + private StringRedisSerializer keySerializer() { + return new StringRedisSerializer(); + } } diff --git a/default/src/main/java/org/dfbf/soundlink/global/slack/service/SlackService.java b/default/src/main/java/org/dfbf/soundlink/global/slack/service/SlackService.java new file mode 100644 index 00000000..3511d4a8 --- /dev/null +++ b/default/src/main/java/org/dfbf/soundlink/global/slack/service/SlackService.java @@ -0,0 +1,82 @@ +package org.dfbf.soundlink.global.slack.service; + +import com.slack.api.Slack; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.slack.api.webhook.WebhookResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Slf4j +@Service +public class SlackService { + + @Value("${SLACK.WEBHOOK.URL}") + private String webhookUrl; + + public void sendMsg(Object object, String errorMessage){ + ObjectMapper objectMapper = new ObjectMapper(); + String objectJson = ""; + try { + objectJson = objectMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(object) + .replace("\"", ""); + } catch (Exception ex) { + log.error("[SLACK ERROR] Failed to convert object to JSON", ex); + } + + String message = """ + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*❗서버 오류*" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Response Data\n```%s```" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Exeception\n`%s`" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "발생시간\n`%s`" + } + }, + { + "type": "divider" + } + ] + } + """.formatted(objectJson, errorMessage, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + + try { + WebhookResponse response = Slack.getInstance().send(webhookUrl, message); + + if (!response.getCode().equals(200)) { + log.error("[SLACK RESPONSE ERROR] {}", response.getBody()); + } + } catch (IOException e) { + log.error("[SLACK ERROR] {}", e.toString()); + throw new RuntimeException(e); + } + } +} diff --git a/default/src/test/java/org/dfbf/soundlink/domain/blocklist/BlocklistServiceTest.java b/default/src/test/java/org/dfbf/soundlink/domain/blocklist/BlocklistServiceTest.java index 764dc0ea..0d62f3e8 100644 --- a/default/src/test/java/org/dfbf/soundlink/domain/blocklist/BlocklistServiceTest.java +++ b/default/src/test/java/org/dfbf/soundlink/domain/blocklist/BlocklistServiceTest.java @@ -10,6 +10,7 @@ import org.dfbf.soundlink.domain.user.repository.UserRepository; import org.dfbf.soundlink.global.exception.ErrorCode; import org.dfbf.soundlink.global.exception.ResponseResult; +import org.dfbf.soundlink.global.slack.service.SlackService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -32,6 +33,9 @@ class BlocklistServiceTest { @Mock private BlockListQueryRepository blockListQueryRepository; + @Mock + private SlackService slackService; + @Mock private UserRepository userRepository;