Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7d300c8
✨ [기능추가] 동적인 스케쥴러를 구현을 위해 배치와 설정을 테스크스케쥴러로 등록
daumi125 Jul 17, 2025
f2e97ed
♻️ 깃 액션 : master 코드로 변경
moonjun1 Jul 17, 2025
94cd449
♻️ 깃 액션 : master 코드로 변경
moonjun1 Jul 17, 2025
787f673
♻️ 깃 액션 : master 코드로 변경
moonjun1 Jul 17, 2025
e0416a7
♻️ 깃 액션 : master 코드로 변경
moonjun1 Jul 17, 2025
07e8fd4
♻️ 깃 액션 : master 코드로 변경
moonjun1 Jul 17, 2025
c2b71d0
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
31af03a
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
8a67085
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
9b43ee1
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
de75eea
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
b5e69ec
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
b09a65f
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
48a5f5a
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
0c34044
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
7792ffe
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
5a750a4
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
be58a3d
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
22c2eab
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
c887c02
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
34f6db5
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
ec08bc2
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
4ec1243
♻️ 깃 액션 : 프로덕션 코드로 변경
moonjun1 Jul 17, 2025
f3f9bfd
🔧 Docker 컨테이너 한국 시간대 설정 추가
moonjun1 Jul 18, 2025
7638956
✨ [기능추가] 배치, 엘라스틱 서치 테스크 스케쥴러 추가 구현완료
daumi125 Jul 18, 2025
e909b82
[feat] merge해결중
daumi125 Jul 18, 2025
a56b251
✨ [기능추가] 테스크 스케쥴러에 배치, 핫토핏, 엘라스틱 서치 스케쥴링 완료
daumi125 Jul 18, 2025
66b7201
✨ [기능 추가] 사용자 셋팅값을 받아 메세지 전송 완료
daumi125 Jul 19, 2025
9f3fe51
✨ [기능추가] 문서화 주석 달아둠
daumi125 Jul 20, 2025
fd749fa
Merge branch 'dev' into feat/kakao
moonjun1 Jul 21, 2025
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
Binary file added SpringBoot/.DS_Store
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import Baemin.News_Deliver.Global.Exception.ErrorCode;
import Baemin.News_Deliver.Global.News.ElasticSearch.dto.NewsEsDocument;
import Baemin.News_Deliver.Global.ResponseObject.ApiResponseWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
Expand All @@ -17,45 +20,60 @@
import java.util.ArrayList;
import java.util.List;

/**
* 카카오 메시지 및 뉴스 관련 API 컨트롤러
* <p>사용자로부터 키워드를 제공받아, 사용자별 맞춤 뉴스 데이터를 카카오톡으로 전달합니다.</p>
*/
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/kakao")
@Tag(name = "Kakao API", description = "카카오톡 뉴스 메시지 API")
public class KakaoController {

private final KakaoMessageService kakaoMessageService;
private final KakaoNewsService newsSearchService;
private final KakaoSchedulerService kakaoSchedulerService;
private final AuthRepository authRepository;


//스케쥴러를 통한 메세지 전송코드
//@GetMapping

/**
* 카카오톡 나에게 보내기 메시지 전송 메서드
* 모든 유저에게 카카오톡 메시지를 전송하는 API
*
* 코드 호출 순서
* <ul>
* <li>카카오톡으로 회원 가입한 전체 유저의 정보를 받아, 전체 유저의 RefreshToken 가져옵니다.</li>
* <li>유저의 RefreshToken을 기준으로 각 유저의 정보를 받아옵니다.</li>
* <li>각 유저별로 kakaoMessageService 의 sendKakaoMessage를 호출합니다.</li>
* </ul>
*
* @return 메시지 전송 결과 응답
*/
@Operation(
summary = "카카오톡 메시지 전송",
description = "전체 유저의 카카오 리프레시 토큰을 기준으로 카카오톡 메시지를 전송합니다. 실패한 토큰은 두 번까지 재시도합니다.",
responses = {
@ApiResponse(responseCode = "200", description = "모든 유저에게 메시지 전송 성공"),
@ApiResponse(responseCode = "500", description = "카카오 메시지 전송 실패 (최종 실패 토큰 존재)")
}
)
@GetMapping("/send-message")
public ResponseEntity<ApiResponseWrapper<String>> sendMessage() {

//전체 유저의 정보를 받아, RefreshToken 가져옴.
// 전체 유저의 정보를 받아, RefreshToken 가져옴.
List<Auth> allUsers = authRepository.findAll();
List<String> allUsersRefreshToken = allUsers.stream()
.map(Auth::getKakaoRefreshToken)
.toList();

log.info("유저 RefreshToken 확인" + allUsersRefreshToken);

log.info("유저 RefreshToken 확인: {}", allUsersRefreshToken);

// 1차 전송 실패시 저장되는 토큰
List<String> failedTokens = new ArrayList<>();
for (String token : allUsersRefreshToken) {
try {
Auth auth = authRepository.findByKakaoRefreshToken(token);
Long userId = auth.getUser().getId();

log.info("유저 정보 확인 코드 {}", userId);

log.info("유저 정보 확인: {}", userId);
kakaoMessageService.sendKakaoMessage(token, userId);
} catch (KakaoException e) {
failedTokens.add(token);
Expand All @@ -75,26 +93,27 @@ public ResponseEntity<ApiResponseWrapper<String>> sendMessage() {
}

if (!finalFailedTokens.isEmpty()) {
log.error("카카오 메시지 전송 실패 토큰 : {}", finalFailedTokens);
log.error("카카오 메시지 전송 실패 토큰: {}", finalFailedTokens);
throw new KakaoException(ErrorCode.MESSAGE_SEND_FAILED);
}

return ResponseEntity.ok(new ApiResponseWrapper<>("SUCCESS", "모든 유저에게 메시지 전송 성공"));
}


//테스트용 코드
@GetMapping("/search-news")
public ResponseEntity<List<NewsEsDocument>> searchNews() {
/**
* 키워드 기반 뉴스 검색 테스트 API
*
* @return 뉴스 검색 결과 리스트
*/
@Operation(
summary = "뉴스 검색 테스트",
description = "키워드와 차단 키워드를 기반으로 어제 날짜의 뉴스를 검색합니다. (현재는 테스트용으로 키워드는 null 전달)"
)
@GetMapping("/search-news-test")
public ResponseEntity<List<NewsEsDocument>> searchNewsTest() {
List<String> keyword = null;
List<String> blockKeyword = null;

return ResponseEntity.ok(newsSearchService.searchNews(keyword, blockKeyword));
}

@GetMapping("/getcron")
public ResponseEntity getcron() {
Long userId = 1L;
return ResponseEntity.ok(kakaoSchedulerService.getCron(userId));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
import java.util.List;
import java.util.Optional;


/**
* 유저에게 카카오톡으로 뉴스를 전달하면 기록되는
* 뉴스 전송 기록(History) 엔티티에 대한 JPA 리포지토리 인터페이스입니다.
*/
public interface HistoryRepository extends JpaRepository<History, Long> {
boolean existsBySettingAndNews(Setting setting, News news);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import Baemin.News_Deliver.Domain.Auth.Entity.User;
import Baemin.News_Deliver.Domain.Auth.Repository.UserRepository;
import Baemin.News_Deliver.Domain.Kakao.Exception.KakaoException;
import Baemin.News_Deliver.Domain.Kakao.entity.History;
import Baemin.News_Deliver.Domain.Kakao.repository.HistoryRepository;
import Baemin.News_Deliver.Domain.Kakao.repository.NewsRepository;
import Baemin.News_Deliver.Domain.Mypage.DTO.SettingDTO;
import Baemin.News_Deliver.Domain.Mypage.Entity.Setting;
import Baemin.News_Deliver.Domain.Mypage.Repository.SettingRepository;
import Baemin.News_Deliver.Domain.Mypage.service.SettingService;
import Baemin.News_Deliver.Global.Exception.ErrorCode;
import Baemin.News_Deliver.Global.Kakao.KakaoTokenProvider;
import Baemin.News_Deliver.Global.News.Batch.entity.News;
import Baemin.News_Deliver.Global.News.ElasticSearch.dto.NewsEsDocument;
Expand All @@ -28,6 +30,20 @@
import java.util.List;
import java.util.Map;


/**
* KakaoMessageService는 사용자의 키워드 기반으로 뉴스를 조회하고,
* 해당 뉴스를 카카오톡 메시지로 전송하는 기능을 담당하는 서비스입니다.
*
* 주요 기능:
* <ul>
* <li>유저의 AccessToken 갱신 및 조회</li>
* <li>뉴스 검색 및 사용자 맞춤 필터링</li>
* <li>카카오톡 템플릿 메시지 전송</li>
* <li>전송된 뉴스에 대한 히스토리 저장</li>
* </ul>
*/

@Service
@RequiredArgsConstructor
@Slf4j
Expand All @@ -50,29 +66,40 @@ public class KakaoMessageService {


/**
* 현재 로그인한 카카오 유저의 Access Token을 가져옴
* 카카오 사용자 Access Token을 Refresh Token을 이용해 갱신 후 반환합니다.
*
* @param refreshAccessToken 사용자의 카카오 Refresh Token
* @param userId 사용자 고유 ID
* @return 갱신된 Access Token 문자열
* @throws RuntimeException Access Token을 가져오지 못했을 경우
*/
public String getKakaoUserAccessToken(String refreshAccessToken, Long userId) {

//유저의 accesstoken을 가져 올 것
String accessToken = provider.refreshAccessToken(refreshAccessToken);
if (accessToken == null || accessToken.isEmpty()) {
throw new RuntimeException("Access Token을 가져올 수 없습니다.");
throw new KakaoException(ErrorCode.KAKAO_TOKEN_ACCESS_FAILED);
}

getNewsEsDocumentList(userId);
return accessToken;
}

/**
* 메시지 전송 메서드
* 사용자의 키워드에 맞는 뉴스를 검색한 후, 카카오 메시지를 전송합니다.
*
* @param refreshAccessToken 사용자 카카오 Refresh Token
* @param userId 사용자 고유 ID
* @return 메시지 전송 성공 여부 (true: 성공, false: 실패)
*/
public boolean sendKakaoMessage(String refreshAccessToken, Long userId) {
try {
String accessToken = getKakaoUserAccessToken(refreshAccessToken, userId);

List<NewsEsDocument> newsList = getNewsEsDocumentList(userId);
if (newsList == null) return false;

//뉴스가 없을 때 반환할 값을 고민 해볼 것.
if (newsList == null) new KakaoException(ErrorCode.NO_NEWS_DATA);;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

문법 오류: 예외가 throw되지 않음

예외 객체를 생성하지만 throw하지 않고 있으며, 세미콜론이 중복되어 있습니다:

-            if (newsList == null) new KakaoException(ErrorCode.NO_NEWS_DATA);;
+            if (newsList == null) throw new KakaoException(ErrorCode.NO_NEWS_DATA);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (newsList == null) new KakaoException(ErrorCode.NO_NEWS_DATA);;
if (newsList == null) throw new KakaoException(ErrorCode.NO_NEWS_DATA);
🤖 Prompt for AI Agents
In
SpringBoot/src/main/java/Baemin/News_Deliver/Domain/Kakao/service/KakaoMessageService.java
at line 102, the code creates a new KakaoException but does not throw it, and
there is an extra semicolon. Fix this by adding the 'throw' keyword before the
new KakaoException to properly throw the exception, and remove the redundant
semicolon.


// 헤더 설정
HttpHeaders headers = new HttpHeaders();
Expand All @@ -99,14 +126,16 @@ public boolean sendKakaoMessage(String refreshAccessToken, Long userId) {

} catch (Exception e) {
log.error("카카오 메시지 전송 실패: ", e);
return false;
throw new KakaoException(ErrorCode.MESSAGE_SEND_FAILED);
}
}

/**
* 사용자의 키워드를 바탕으로 뉴스를 검색하여 리스트로 반환하는 메서드입니다.
* 사용자의 Setting 정보를 기반으로 키워드에 해당하는 뉴스를 검색하여 반환합니다.
* 뉴스는 히스토리에 저장되며, 최대 5개까지 템플릿으로 전송됩니다.
*
* @return NewsEsDocument의 리스트
* @param userId 사용자 고유 ID
* @return 뉴스 리스트 {@code List<NewsEsDocument>}, 키워드가 없거나 오류 시 {@code null}
*/
private List<NewsEsDocument> getNewsEsDocumentList(Long userId) {

Expand All @@ -130,7 +159,7 @@ private List<NewsEsDocument> getNewsEsDocumentList(Long userId) {

if (keywords.isEmpty()) {
log.error("설정된 키워드가 없습니다.");
return null;
throw new KakaoException(ErrorCode.SETTING_NOT_FOUND);
}

//키워드별 뉴스 검색
Expand All @@ -144,9 +173,16 @@ private List<NewsEsDocument> getNewsEsDocumentList(Long userId) {
return newsList;
}

/**
* 뉴스 리스트를 템플릿 전송용 파라미터(Map)로 변환합니다.
* 최대 5개의 뉴스만 포함됩니다.
*
* @param newsList 뉴스 리스트
* @return 템플릿에 들어갈 파라미터 Map
*/
private static Map<String, String> createTemplateData(List<NewsEsDocument> newsList) {

log.info("뉴스 전체 리스트 확인용:" + newsList);
log.info("뉴스 전체 리스트 확인:" + newsList);
Map<String, String> templateArgs = new HashMap<>();

//메세지 5개 고정
Expand All @@ -159,21 +195,37 @@ private static Map<String, String> createTemplateData(List<NewsEsDocument> newsL

}

/**
* 전송된 뉴스 정보를 히스토리로 저장합니다. 중복 뉴스는 저장하지 않습니다.
*
* @param newsList 뉴스 리스트
* @param settings 해당 뉴스에 적용된 사용자 설정들
* @return 저장이 이루어진 경우 true, 아무 것도 저장되지 않았으면 false
*/
private boolean saveHistory(List<NewsEsDocument> newsList, List<SettingDTO> settings) {
if (newsList == null || newsList.isEmpty()) {
log.warn("해당 키워드로 검색된 뉴스가 없습니다.");
return false;
throw new KakaoException(ErrorCode.NO_NEWS_DATA);
}

//중복 저장 방지용 플래그
boolean saved = false;

for (NewsEsDocument newsDoc : newsList) {
// News newsitem = newsRepository.findById(Long.parseLong(newsDoc.getId()))
// .orElseThrow(() -> new RuntimeException("뉴스가 존재하지 않습니다: " + newsDoc.getId()));

/* DB와 ES 동기화 되어있지 않을 시, 예외 */
News newsitem = newsRepository.findById(Long.parseLong(newsDoc.getId()))
.orElseThrow(() -> new RuntimeException("뉴스가 존재하지 않습니다: " + newsDoc.getId()));
.orElse(null);
if (newsitem == null) {
log.warn("DB에 존재하지 않는 뉴스입니다. id={}", newsDoc.getId());
continue; // 이 뉴스는 히스토리 저장 생략
}

for (SettingDTO settingDTO : settings) {
Setting setting = settingRepository.findById(settingDTO.getId())
.orElseThrow(() -> new RuntimeException("설정이 존재하지 않습니다: " + settingDTO.getId()));
.orElseThrow(() -> new KakaoException(ErrorCode.SETTING_NOT_FOUND));

// 중복 저장 방지용 코드
boolean exists = historyRepository.existsBySettingAndNews(setting, newsitem);
Expand Down
Loading