Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 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
383 changes: 184 additions & 199 deletions .github/workflows/deploy-prod.yml

Large diffs are not rendered by default.

164 changes: 139 additions & 25 deletions .github/workflows/dev-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 12 # 테스트 시간 제한

services:
mysql:
Expand All @@ -18,7 +19,11 @@ jobs:
MYSQL_DATABASE: backendDB
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
options: >-
--health-cmd="mysqladmin ping"
--health-interval=5s
--health-timeout=3s
--health-retries=6

redis:
image: redis:7.2
Expand All @@ -32,6 +37,7 @@ jobs:
env:
discovery.type: single-node
xpack.security.enabled: false
ES_JAVA_OPTS: -Xms256m -Xmx256m # 메모리 사용량 감소
ports:
- 9200:9200

Expand All @@ -44,41 +50,149 @@ jobs:
java-version: '17'
distribution: 'corretto'

- name: Gradle 패키지 캐시
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

- name: gradlew 권한 설정
run: chmod +x SpringBoot/gradlew

- name: 서비스 대기
- name: 서비스 준비 대기 (최적화)
run: |
echo "⏳ Dev 테스트 환경 준비 중..."

# 병렬로 서비스 상태 확인
wait_for_service() {
local service_name=$1
local check_cmd=$2
local max_attempts=$3

for i in $(seq 1 $max_attempts); do
if eval "$check_cmd" >/dev/null 2>&1; then
echo "✅ $service_name 준비 완료"
return 0
fi
sleep 1
done
echo "❌ $service_name 준비 실패"
return 1
}

# 병렬 서비스 체크 (시간 단축)
wait_for_service "MySQL" "mysqladmin ping -h127.0.0.1 -P3306 -uroot -ptestpass --silent" 10 &
wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 8 &
wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 15 &

# 모든 백그라운드 작업 완료 대기
wait
echo "✅ 모든 서비스 준비 완료"

- name: 테스트용 환경설정 파일 생성
working-directory: ./SpringBoot
run: |
sleep 30
echo "서비스 준비 완료"
cat > src/main/resources/application-test.properties << 'EOF'
spring.application.name=News-Deliver
spring.main.allow-bean-definition-overriding=true

# DB Connection
spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=testpass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false

# Redis Configuration
redis.cache.host=localhost
redis.cache.port=6379
redis.session1.host=localhost
redis.session1.port=6380
redis.session2.host=localhost
redis.session2.port=6381

# Elasticsearch
spring.elasticsearch.uris=http://localhost:9200

# JWT
jwt.secretKey=test-secret-key-that-is-long-enough-for-jwt-hmac-sha256-algorithm-minimum-32-bytes
jwt.accessTokenExpirationTime=1800000
jwt.refreshTokenExpirationTime=86400000

# OAuth2
spring.security.oauth2.client.registration.kakao.client-id=test-id
spring.security.oauth2.client.registration.kakao.client-secret=test-secret
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id

# Spring AI
spring.ai.openai.api-key=test-key
spring.ai.openai.chat.options.model=gpt-3.5-turbo

# DeepSearch API
deepsearch.api.key=test-key

# Batch Job 비활성화 (테스트 시)
spring.batch.job.enabled=false

# Logging Configuration
logging.level.org.springframework.web=INFO
logging.level.org.hibernate.SQL=ERROR
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR
logging.level.com.zaxxer.hikari=ERROR

# Time Zone
spring.jackson.time-zone=Asia/Seoul

# Forward headers
server.forward-headers-strategy=framework
EOF

- name: 테스트 실행
- name: 테스트 실행 (최적화)
working-directory: ./SpringBoot
env:
DB_SERVER: localhost
DB_PORT: 3306
DB_USER: root
DB_PASS: testpass
REDIS_CACHE_HOST: localhost
REDIS_CACHE_PORT: 6379
REDIS_SESSION1_HOST: localhost
REDIS_SESSION1_PORT: 6380
REDIS_SESSION2_HOST: localhost
REDIS_SESSION2_PORT: 6381
JWT_SECRET_KEY: test-secret-key-that-is-long-enough-for-jwt-hmac-sha256-algorithm-minimum-32-bytes
ELASTICSEARCH_SERVER: localhost
ELASTICSEARCH_PORT: 9200
OPENAI_API_KEY: test-key
KAKAO_CLIENT_ID: test-id
KAKAO_CLIENT_SECRET: test-secret
DEEPSEARCH_API_KEY: test-key
run: ./gradlew build --no-daemon
SPRING_PROFILES_ACTIVE: test
run: |
echo "🚀 Dev 브랜치 테스트 시작"
./gradlew clean build --no-daemon --parallel --build-cache

- name: 테스트 결과 업로드
uses: actions/upload-artifact@v4
if: failure()
with:
name: dev-test-reports-${{ github.run_number }}
path: |
SpringBoot/build/reports/
SpringBoot/build/test-results/
retention-days: 3


- name: 결과 요약
if: always()
run: |
echo "## 📋 Dev 테스트 결과" >> $GITHUB_STEP_SUMMARY
echo "- **테스트 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- **브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- **실행 시간**: $(date)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [ "${{ job.status }}" == "success" ]; then
echo "✅ Dev 테스트 성공!"
echo "### ✅ Dev 테스트 성공!" >> $GITHUB_STEP_SUMMARY
echo "모든 테스트가 통과했습니다. 프로덕션 배포 준비 완료!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ Dev 테스트 실패!"
echo "### ❌ Dev 테스트 실패!" >> $GITHUB_STEP_SUMMARY
echo "테스트 실패 원인을 확인하고 수정해주세요." >> $GITHUB_STEP_SUMMARY
fi
Binary file added SpringBoot/.DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions SpringBoot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ RUN gradle clean build -x test

# --- 2단계: 실행 ---
FROM amazoncorretto:17-alpine

# 타임존 추가
RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && echo "Asia/Seoul" > /etc/timezone && apk del tzdata
ENV TZ=Asia/Seoul

WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
# 환경 변수는 docker-compose에서 주입
Expand Down
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
Loading