Compare commits
10 Commits
feature/ai
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ce62738a1 | |||
| 06ea838547 | |||
| efcec065ec | |||
| 262a5fea33 | |||
| 6e7a9386f6 | |||
| 047703fb89 | |||
| 17278ad045 | |||
| 4bc7f87663 | |||
| ae8f540d46 | |||
| be59934f78 |
@@ -23,6 +23,11 @@
|
|||||||
<env name="KAFKA_CONSUMER_GROUP" value="distribution-service" />
|
<env name="KAFKA_CONSUMER_GROUP" value="distribution-service" />
|
||||||
<env name="JPA_DDL_AUTO" value="update" />
|
<env name="JPA_DDL_AUTO" value="update" />
|
||||||
<env name="JPA_SHOW_SQL" value="false" />
|
<env name="JPA_SHOW_SQL" value="false" />
|
||||||
|
<env name="NAVER_BLOG_USERNAME" value="" />
|
||||||
|
<env name="NAVER_BLOG_PASSWORD" value="" />
|
||||||
|
<env name="NAVER_BLOG_BLOG_ID" value="" />
|
||||||
|
<env name="NAVER_BLOG_HEADLESS" value="false" />
|
||||||
|
<env name="NAVER_BLOG_SESSION_PATH" value="playwright-sessions" />
|
||||||
</envs>
|
</envs>
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
private void publishEventCreatedEvents() throws Exception {
|
private void publishEventCreatedEvents() throws Exception {
|
||||||
// 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과 - ROI 200%)
|
// 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과 - ROI 200%)
|
||||||
EventCreatedEvent event1 = EventCreatedEvent.builder()
|
EventCreatedEvent event1 = EventCreatedEvent.builder()
|
||||||
.eventId("1")
|
.eventId("evt_2025012301")
|
||||||
.eventTitle("신년맞이 20% 할인 이벤트")
|
.eventTitle("신년맞이 20% 할인 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("5000000"))
|
.totalInvestment(new BigDecimal("5000000"))
|
||||||
@@ -238,7 +238,7 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
// 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과 - ROI 100%)
|
// 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과 - ROI 100%)
|
||||||
EventCreatedEvent event2 = EventCreatedEvent.builder()
|
EventCreatedEvent event2 = EventCreatedEvent.builder()
|
||||||
.eventId("2")
|
.eventId("evt_2025012302")
|
||||||
.eventTitle("설날 특가 선물세트 이벤트")
|
.eventTitle("설날 특가 선물세트 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("3500000"))
|
.totalInvestment(new BigDecimal("3500000"))
|
||||||
@@ -251,7 +251,7 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
// 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과 - ROI 50%)
|
// 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과 - ROI 50%)
|
||||||
EventCreatedEvent event3 = EventCreatedEvent.builder()
|
EventCreatedEvent event3 = EventCreatedEvent.builder()
|
||||||
.eventId("3")
|
.eventId("evt_2025012303")
|
||||||
.eventTitle("겨울 신메뉴 런칭 이벤트")
|
.eventTitle("겨울 신메뉴 런칭 이벤트")
|
||||||
.storeId("store_001")
|
.storeId("store_001")
|
||||||
.totalInvestment(new BigDecimal("2000000"))
|
.totalInvestment(new BigDecimal("2000000"))
|
||||||
@@ -269,7 +269,7 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
* DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
|
* DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
|
||||||
*/
|
*/
|
||||||
private void publishDistributionCompletedEvents() throws Exception {
|
private void publishDistributionCompletedEvents() throws Exception {
|
||||||
String[] eventIds = {"1", "2", "3"};
|
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
|
||||||
int[][] expectedViews = {
|
int[][] expectedViews = {
|
||||||
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
|
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
|
||||||
{3500, 7000, 2000, 1500}, // 이벤트2
|
{3500, 7000, 2000, 1500}, // 이벤트2
|
||||||
@@ -359,7 +359,7 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
* - 이벤트3: 30명 (user071~user100) → 30명이 이전 이벤트들과 중복
|
* - 이벤트3: 30명 (user071~user100) → 30명이 이전 이벤트들과 중복
|
||||||
*/
|
*/
|
||||||
private void publishParticipantRegisteredEvents() throws Exception {
|
private void publishParticipantRegisteredEvents() throws Exception {
|
||||||
String[] eventIds = {"1", "2", "3"};
|
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
|
||||||
String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
|
String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
|
||||||
|
|
||||||
// 이벤트별 참여자 범위 (중복 참여 반영)
|
// 이벤트별 참여자 범위 (중복 참여 반영)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kt.event.analytics.config;
|
|||||||
import com.kt.event.common.security.JwtAuthenticationFilter;
|
import com.kt.event.common.security.JwtAuthenticationFilter;
|
||||||
import com.kt.event.common.security.JwtTokenProvider;
|
import com.kt.event.common.security.JwtTokenProvider;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
@@ -12,15 +11,12 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spring Security 설정
|
* Spring Security 설정
|
||||||
* JWT 기반 인증 및 API 보안 설정
|
* JWT 기반 인증 및 API 보안 설정
|
||||||
|
*
|
||||||
|
* ⚠️ CORS 설정은 WebConfig에서 관리합니다.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@@ -29,14 +25,11 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
@Value("${cors.allowed-origins:http://localhost:*}")
|
|
||||||
private String allowedOrigins;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(AbstractHttpConfigurer::disable) // CORS는 WebConfig에서 관리
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
@@ -46,25 +39,5 @@ public class SecurityConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
// CORS 설정은 WebConfig에서 관리 (모든 origin 허용)
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
|
||||||
|
|
||||||
String[] origins = allowedOrigins.split(",");
|
|
||||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
|
||||||
|
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
|
||||||
|
|
||||||
configuration.setAllowedHeaders(Arrays.asList(
|
|
||||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
|
||||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
|
||||||
));
|
|
||||||
|
|
||||||
configuration.setAllowCredentials(true);
|
|
||||||
configuration.setMaxAge(3600L);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/**", configuration);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
<entry key="KAKAO_API_URL" value="http://localhost:9006/api/kakao" />
|
<entry key="KAKAO_API_URL" value="http://localhost:9006/api/kakao" />
|
||||||
<entry key="LOG_FILE" value="logs/distribution-service.log" />
|
<entry key="LOG_FILE" value="logs/distribution-service.log" />
|
||||||
<entry key="NAVER_API_URL" value="http://localhost:9005/api/naver" />
|
<entry key="NAVER_API_URL" value="http://localhost:9005/api/naver" />
|
||||||
|
<entry key="NAVER_BLOG_BLOG_ID" value="bokchi_13" />
|
||||||
|
<entry key="NAVER_BLOG_HEADLESS" value="false" />
|
||||||
|
<entry key="NAVER_BLOG_PASSWORD" value="" />
|
||||||
|
<entry key="NAVER_BLOG_SESSION_PATH" value="playwright-sessions" />
|
||||||
|
<entry key="NAVER_BLOG_USERNAME" value="" />
|
||||||
<entry key="RINGOBIZ_API_URL" value="http://localhost:9002/api/ringobiz" />
|
<entry key="RINGOBIZ_API_URL" value="http://localhost:9002/api/ringobiz" />
|
||||||
<entry key="SERVER_PORT" value="8085" />
|
<entry key="SERVER_PORT" value="8085" />
|
||||||
<entry key="URIDONGNETV_API_URL" value="http://localhost:9001/api/uridongnetv" />
|
<entry key="URIDONGNETV_API_URL" value="http://localhost:9001/api/uridongnetv" />
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
# Multi-stage build for Spring Boot application
|
# Multi-stage build for Spring Boot application
|
||||||
FROM eclipse-temurin:21-jre-alpine AS builder
|
FROM eclipse-temurin:21-jre AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
RUN java -Djarmode=layertools -jar app.jar extract
|
RUN java -Djarmode=layertools -jar app.jar extract
|
||||||
|
|
||||||
FROM eclipse-temurin:21-jre-alpine
|
FROM eclipse-temurin:21-jre
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Create non-root user
|
# Install Playwright essential dependencies only
|
||||||
RUN addgroup -S spring && adduser -S spring -G spring
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
USER spring:spring
|
wget \
|
||||||
|
libnss3 \
|
||||||
|
libnspr4 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libdrm2 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2t64 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
libatspi2.0-0 \
|
||||||
|
libxshmfence1 \
|
||||||
|
fonts-liberation \
|
||||||
|
libappindicator3-1 \
|
||||||
|
xdg-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create browser installation directory with proper permissions
|
||||||
|
RUN mkdir -p /app/playwright && chmod 777 /app/playwright
|
||||||
|
|
||||||
# Copy layers from builder
|
# Copy layers from builder
|
||||||
COPY --from=builder /app/dependencies/ ./
|
COPY --from=builder /app/dependencies/ ./
|
||||||
@@ -17,6 +42,17 @@ COPY --from=builder /app/spring-boot-loader/ ./
|
|||||||
COPY --from=builder /app/snapshot-dependencies/ ./
|
COPY --from=builder /app/snapshot-dependencies/ ./
|
||||||
COPY --from=builder /app/application/ ./
|
COPY --from=builder /app/application/ ./
|
||||||
|
|
||||||
|
# Set Playwright browsers path
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/app/playwright
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN groupadd -r spring && useradd -r -g spring spring
|
||||||
|
|
||||||
|
# Change ownership to spring user
|
||||||
|
RUN chown -R spring:spring /app
|
||||||
|
|
||||||
|
USER spring:spring
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8085/distribution/actuator/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8085/distribution/actuator/health || exit 1
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# 네이버 블로그 포스팅 설정 가이드
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
Distribution Service는 Playwright를 사용하여 네이버 블로그에 자동으로 포스팅합니다.
|
||||||
|
|
||||||
|
## 사전 준비
|
||||||
|
|
||||||
|
### 1. Playwright 설치
|
||||||
|
처음 실행 시 Playwright 브라우저가 자동으로 다운로드됩니다. 수동으로 설치하려면:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 네이버 계정 준비
|
||||||
|
- 네이버 계정 (아이디/비밀번호)
|
||||||
|
- 네이버 블로그 개설 (blog.naver.com에서 블로그 만들기)
|
||||||
|
- 블로그 ID 확인 (예: blog.naver.com/YOUR_BLOG_ID)
|
||||||
|
|
||||||
|
## 환경 변수 설정
|
||||||
|
|
||||||
|
### IntelliJ 실행 프로파일 설정
|
||||||
|
`.run/DistributionServiceApplication.run.xml` 파일에서 다음 환경 변수를 설정:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<env name="NAVER_BLOG_USERNAME" value="your_naver_id" />
|
||||||
|
<env name="NAVER_BLOG_PASSWORD" value="your_password" />
|
||||||
|
<env name="NAVER_BLOG_BLOG_ID" value="your_blog_id" />
|
||||||
|
<env name="NAVER_BLOG_HEADLESS" value="false" /> <!-- 브라우저 표시 여부 -->
|
||||||
|
<env name="NAVER_BLOG_SESSION_PATH" value="playwright-sessions" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 환경 변수 설명
|
||||||
|
|
||||||
|
| 환경 변수 | 설명 | 기본값 | 필수 |
|
||||||
|
|----------|------|--------|------|
|
||||||
|
| `NAVER_BLOG_USERNAME` | 네이버 아이디 | - | ✅ |
|
||||||
|
| `NAVER_BLOG_PASSWORD` | 네이버 비밀번호 | - | ✅ |
|
||||||
|
| `NAVER_BLOG_BLOG_ID` | 네이버 블로그 ID | - | ✅ |
|
||||||
|
| `NAVER_BLOG_HEADLESS` | Headless 모드 (true/false) | true | ❌ |
|
||||||
|
| `NAVER_BLOG_SESSION_PATH` | 세션 저장 경로 | playwright-sessions | ❌ |
|
||||||
|
|
||||||
|
### Headless 모드
|
||||||
|
- **false**: 브라우저 창이 표시되어 디버깅에 유용 (개발 환경 권장)
|
||||||
|
- **true**: 백그라운드 실행, 서버 환경에 적합 (운영 환경 권장)
|
||||||
|
|
||||||
|
## 사용 방법
|
||||||
|
|
||||||
|
### API 호출 예시
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 배포 요청
|
||||||
|
curl -X POST http://localhost:8085/distribution/api/v1/distributions \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"eventId": "EVT001",
|
||||||
|
"title": "신규 이벤트 안내",
|
||||||
|
"content": "이벤트 상세 내용입니다.",
|
||||||
|
"imageUrl": "https://example.com/event.jpg",
|
||||||
|
"channels": ["NAVER"]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 응답 예시
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventId": "EVT001",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"totalChannels": 1,
|
||||||
|
"successCount": 1,
|
||||||
|
"failureCount": 0,
|
||||||
|
"channels": [
|
||||||
|
{
|
||||||
|
"channel": "NAVER",
|
||||||
|
"success": true,
|
||||||
|
"distributionId": "NAVER-abc123",
|
||||||
|
"distributionUrl": "https://blog.naver.com/your_blog_id/222999999999",
|
||||||
|
"estimatedReach": 2000,
|
||||||
|
"executionTimeMs": 5234
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"distributedAt": "2025-10-29T10:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 세션 관리
|
||||||
|
|
||||||
|
### 자동 로그인
|
||||||
|
- 최초 실행 시 네이버에 로그인하고 세션이 저장됩니다
|
||||||
|
- 이후 요청은 저장된 세션을 사용하여 로그인 없이 진행됩니다
|
||||||
|
- 세션 파일 위치: `playwright-sessions/naver-blog-session.json`
|
||||||
|
|
||||||
|
### 세션 만료 시
|
||||||
|
세션이 만료되면 자동으로 재로그인을 시도합니다.
|
||||||
|
|
||||||
|
### 수동 세션 초기화
|
||||||
|
```bash
|
||||||
|
# 세션 파일 삭제
|
||||||
|
rm -rf playwright-sessions/naver-blog-session.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 문제 해결
|
||||||
|
|
||||||
|
### 1. 로그인 실패
|
||||||
|
**증상**: "Login failed" 에러 발생
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
- 네이버 아이디/비밀번호 확인
|
||||||
|
- 네이버 로그인 보안 설정 확인 (캡차, 2단계 인증 등)
|
||||||
|
- Headless 모드를 false로 설정하여 브라우저 동작 확인
|
||||||
|
- 세션 파일 삭제 후 재시도
|
||||||
|
|
||||||
|
### 2. 브라우저 실행 실패
|
||||||
|
**증상**: "Failed to initialize Playwright" 에러
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
```bash
|
||||||
|
# Playwright 브라우저 재설치
|
||||||
|
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 포스팅 실패
|
||||||
|
**증상**: 포스팅 URL이 반환되지 않음
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
- Headless 모드를 false로 설정하여 UI 확인
|
||||||
|
- 네이버 블로그 에디터 구조 변경 여부 확인
|
||||||
|
- 로그 확인: `logs/distribution-service.log`
|
||||||
|
|
||||||
|
### 4. 성능 이슈
|
||||||
|
브라우저 자동화는 리소스를 많이 사용하므로:
|
||||||
|
- Resilience4j Bulkhead 설정으로 동시 실행 제한 (현재 10개)
|
||||||
|
- Circuit Breaker로 반복 실패 방지
|
||||||
|
- 실패 시 자동 재시도 (최대 3회)
|
||||||
|
|
||||||
|
## 보안 고려사항
|
||||||
|
|
||||||
|
### 1. 비밀번호 관리
|
||||||
|
- **절대로** 소스 코드에 비밀번호를 하드코딩하지 마세요
|
||||||
|
- 환경 변수 또는 시크릿 관리 서비스 사용
|
||||||
|
- Git에 `.run/*.xml` 파일을 커밋하지 마세요 (`.gitignore` 추가)
|
||||||
|
|
||||||
|
### 2. 세션 파일 보안
|
||||||
|
- `playwright-sessions/` 디렉토리를 `.gitignore`에 추가
|
||||||
|
- 서버 환경에서 파일 권한 설정 (chmod 600)
|
||||||
|
|
||||||
|
### 3. 네트워크 보안
|
||||||
|
- HTTPS만 사용
|
||||||
|
- 프록시 사용 시 안전한 프록시 설정
|
||||||
|
|
||||||
|
## 운영 환경 배포
|
||||||
|
|
||||||
|
### Docker 환경
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile에 Playwright 설치 추가
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libnss3 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libdrm2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2
|
||||||
|
|
||||||
|
# Playwright 브라우저 설치
|
||||||
|
RUN mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes 환경
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: naver-blog-credentials
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
username: your_naver_id
|
||||||
|
password: your_password
|
||||||
|
blog-id: your_blog_id
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: distribution-service
|
||||||
|
env:
|
||||||
|
- name: NAVER_BLOG_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: naver-blog-credentials
|
||||||
|
key: username
|
||||||
|
- name: NAVER_BLOG_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: naver-blog-credentials
|
||||||
|
key: password
|
||||||
|
- name: NAVER_BLOG_BLOG_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: naver-blog-credentials
|
||||||
|
key: blog-id
|
||||||
|
- name: NAVER_BLOG_HEADLESS
|
||||||
|
value: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 제약사항
|
||||||
|
|
||||||
|
1. **동시 실행 제한**: Bulkhead 설정으로 최대 10개 동시 실행
|
||||||
|
2. **실행 시간**: 브라우저 자동화는 API 호출보다 느림 (평균 5-10초)
|
||||||
|
3. **네이버 정책**: 네이버 블로그 정책 변경 시 업데이트 필요
|
||||||
|
4. **UI 변경**: 네이버 블로그 UI 변경 시 코드 수정 필요
|
||||||
|
|
||||||
|
## 모니터링
|
||||||
|
|
||||||
|
### 로그 확인
|
||||||
|
```bash
|
||||||
|
# 실시간 로그
|
||||||
|
tail -f logs/distribution-service.log
|
||||||
|
|
||||||
|
# 에러만 필터
|
||||||
|
grep ERROR logs/distribution-service.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 주요 로그 메시지
|
||||||
|
- `Initializing Playwright for Naver Blog`: Playwright 초기화
|
||||||
|
- `Starting Naver login process`: 로그인 시작
|
||||||
|
- `Naver login successful`: 로그인 성공
|
||||||
|
- `Post published successfully`: 포스팅 성공
|
||||||
|
- `Failed to post to Naver blog`: 포스팅 실패
|
||||||
|
|
||||||
|
## 참고 자료
|
||||||
|
|
||||||
|
- [Playwright for Java](https://playwright.dev/java/)
|
||||||
|
- [네이버 블로그 고객센터](https://help.naver.com/service/5614/)
|
||||||
|
- [Resilience4j 문서](https://resilience4j.readme.io/)
|
||||||
|
|
||||||
|
## 지원
|
||||||
|
|
||||||
|
문제 발생 시:
|
||||||
|
1. 로그 파일 확인: `logs/distribution-service.log`
|
||||||
|
2. Headless 모드를 false로 설정하여 브라우저 동작 확인
|
||||||
|
3. GitHub Issue 등록 (로그 첨부)
|
||||||
@@ -15,6 +15,9 @@ dependencies {
|
|||||||
implementation "io.github.resilience4j:resilience4j-retry:${resilience4jVersion}"
|
implementation "io.github.resilience4j:resilience4j-retry:${resilience4jVersion}"
|
||||||
implementation "io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}"
|
implementation "io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}"
|
||||||
|
|
||||||
|
// Playwright for browser automation
|
||||||
|
implementation 'com.microsoft.playwright:playwright:1.41.0'
|
||||||
|
|
||||||
// Jackson for JSON
|
// Jackson for JSON
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
package com.kt.distribution.adapter;
|
package com.kt.distribution.adapter;
|
||||||
|
|
||||||
|
import com.kt.distribution.client.NaverBlogClient;
|
||||||
import com.kt.distribution.dto.ChannelDistributionResult;
|
import com.kt.distribution.dto.ChannelDistributionResult;
|
||||||
import com.kt.distribution.dto.ChannelType;
|
import com.kt.distribution.dto.ChannelType;
|
||||||
import com.kt.distribution.dto.DistributionRequest;
|
import com.kt.distribution.dto.DistributionRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Naver Blog Adapter
|
* Naver Blog Adapter
|
||||||
* Naver Blog 포스팅 API 호출
|
* Naver Blog 포스팅 (Playwright 기반)
|
||||||
*
|
*
|
||||||
* @author System Architect
|
* @author Backend Developer
|
||||||
* @since 2025-10-23
|
* @since 2025-10-29
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "naver.blog.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
public class NaverAdapter extends AbstractChannelAdapter {
|
public class NaverAdapter extends AbstractChannelAdapter {
|
||||||
|
|
||||||
@Value("${channel.apis.naver.url}")
|
private final NaverBlogClient naverBlogClient;
|
||||||
private String apiUrl;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelType getChannelType() {
|
public ChannelType getChannelType() {
|
||||||
@@ -30,16 +33,35 @@ public class NaverAdapter extends AbstractChannelAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ChannelDistributionResult executeDistribution(DistributionRequest request) {
|
protected ChannelDistributionResult executeDistribution(DistributionRequest request) {
|
||||||
log.debug("Calling Naver API: url={}, eventId={}", apiUrl, request.getEventId());
|
log.debug("Posting to Naver Blog: eventId={}, title={}",
|
||||||
|
request.getEventId(), request.getTitle());
|
||||||
|
|
||||||
// TODO: 실제 API 호출 (현재는 Mock)
|
try {
|
||||||
String distributionId = "NAVER-" + UUID.randomUUID().toString();
|
// 네이버 블로그에 포스팅
|
||||||
|
String postUrl = naverBlogClient.postToBlog(request);
|
||||||
|
String distributionId = "NAVER-" + UUID.randomUUID().toString();
|
||||||
|
|
||||||
return ChannelDistributionResult.builder()
|
log.info("Naver blog post created successfully: eventId={}, postUrl={}",
|
||||||
.channel(ChannelType.NAVER)
|
request.getEventId(), postUrl);
|
||||||
.success(true)
|
|
||||||
.distributionId(distributionId)
|
return ChannelDistributionResult.builder()
|
||||||
.estimatedReach(2000) // 블로그 방문자 수 기반
|
.channel(ChannelType.NAVER)
|
||||||
.build();
|
.success(true)
|
||||||
|
.distributionId(distributionId)
|
||||||
|
.postUrl(postUrl)
|
||||||
|
.estimatedReach(2000) // 블로그 방문자 수 기반
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to post to Naver blog: eventId={}, error={}",
|
||||||
|
request.getEventId(), e.getMessage(), e);
|
||||||
|
|
||||||
|
return ChannelDistributionResult.builder()
|
||||||
|
.channel(ChannelType.NAVER)
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("Naver blog posting failed: " + e.getMessage())
|
||||||
|
.estimatedReach(0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package com.kt.distribution.client;
|
||||||
|
|
||||||
|
import com.kt.distribution.dto.DistributionRequest;
|
||||||
|
import com.microsoft.playwright.*;
|
||||||
|
import com.microsoft.playwright.options.LoadState;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Naver Blog Client using Playwright
|
||||||
|
* 네이버 블로그 포스팅 자동화 클라이언트
|
||||||
|
*
|
||||||
|
* @author Backend Developer
|
||||||
|
* @since 2025-10-29
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "naver.blog.enabled", havingValue = "true", matchIfMissing = false)
|
||||||
|
public class NaverBlogClient {
|
||||||
|
|
||||||
|
@Value("${naver.blog.username:}")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Value("${naver.blog.password:}")
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Value("${naver.blog.blog-id:}")
|
||||||
|
private String blogId;
|
||||||
|
|
||||||
|
@Value("${naver.blog.headless:false}")
|
||||||
|
private boolean headless;
|
||||||
|
|
||||||
|
@Value("${naver.blog.session-path:playwright-sessions}")
|
||||||
|
private String sessionPath;
|
||||||
|
|
||||||
|
private Playwright playwright;
|
||||||
|
private Browser browser;
|
||||||
|
private BrowserContext context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright 초기화
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
try {
|
||||||
|
log.info("Initializing Playwright for Naver Blog");
|
||||||
|
playwright = Playwright.create();
|
||||||
|
|
||||||
|
browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
|
||||||
|
.setHeadless(headless)
|
||||||
|
.setSlowMo(100)); // 안정성을 위한 느린 실행
|
||||||
|
|
||||||
|
// 세션 디렉토리 생성
|
||||||
|
File sessionDir = new File(sessionPath);
|
||||||
|
if (!sessionDir.exists()) {
|
||||||
|
sessionDir.mkdirs();
|
||||||
|
log.info("Created session directory: {}", sessionPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 세션 파일 경로
|
||||||
|
Path sessionFilePath = Paths.get(sessionPath, "naver-blog-session.json");
|
||||||
|
|
||||||
|
// 세션 파일이 있으면 로드, 없으면 새로운 컨텍스트 생성
|
||||||
|
if (Files.exists(sessionFilePath)) {
|
||||||
|
log.info("Loading existing session from: {}", sessionFilePath);
|
||||||
|
context = browser.newContext(new Browser.NewContextOptions()
|
||||||
|
.setStorageStatePath(sessionFilePath));
|
||||||
|
} else {
|
||||||
|
log.info("No existing session found, creating new context");
|
||||||
|
context = browser.newContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Playwright initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to initialize Playwright", e);
|
||||||
|
throw new RuntimeException("Playwright initialization failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네이버 블로그에 포스팅
|
||||||
|
*
|
||||||
|
* @param request DistributionRequest
|
||||||
|
* @return 포스팅 URL
|
||||||
|
* @throws Exception 포스팅 실패 시
|
||||||
|
*/
|
||||||
|
public String postToBlog(DistributionRequest request) throws Exception {
|
||||||
|
Page page = null;
|
||||||
|
try {
|
||||||
|
page = context.newPage();
|
||||||
|
// 타임아웃을 5분(300000ms)으로 설정
|
||||||
|
page.setDefaultTimeout(300000);
|
||||||
|
|
||||||
|
// 로그인 확인 및 처리
|
||||||
|
if (!isLoggedIn(page)) {
|
||||||
|
login(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 블로그 글쓰기 페이지로 이동
|
||||||
|
String writeUrl = String.format("https://blog.naver.com/%s/postwrite", blogId);
|
||||||
|
page.navigate(writeUrl);
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
|
||||||
|
|
||||||
|
// 도움말 팝업이 있으면 닫기
|
||||||
|
try {
|
||||||
|
page.waitForTimeout(5000); // 충분히 대기 필요
|
||||||
|
|
||||||
|
Locator helpPanel = page.locator("[class*='help-panel']");
|
||||||
|
|
||||||
|
if (helpPanel.isVisible(new Locator.IsVisibleOptions().setTimeout(2000))) {
|
||||||
|
log.debug("Help dialog detected, closing it");
|
||||||
|
|
||||||
|
// 팝업 안의 닫기 버튼 찾기
|
||||||
|
Locator closeBtn = page.locator("button[class*='se-help-panel-close-button']");
|
||||||
|
closeBtn.click();
|
||||||
|
Thread.sleep(500);
|
||||||
|
log.debug("Help dialog closed");
|
||||||
|
} else{
|
||||||
|
log.debug("--------------------- 도움말 없음");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("No help dialog found or already closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 제목 입력
|
||||||
|
Locator titleInput = page.locator(".se-text-paragraph").first();
|
||||||
|
titleInput.click();
|
||||||
|
titleInput.pressSequentially(request.getTitle(), new Locator.PressSequentiallyOptions().setDelay(50));
|
||||||
|
log.debug("Title entered: {}", request.getTitle());
|
||||||
|
|
||||||
|
// 본문 입력
|
||||||
|
Locator editorInput = page.locator(".se-text-paragraph").nth(1);
|
||||||
|
editorInput.click();
|
||||||
|
titleInput.pressSequentially(request.getDescription(), new Locator.PressSequentiallyOptions().setDelay(50));
|
||||||
|
log.debug("Content entered");
|
||||||
|
|
||||||
|
// 이미지가 있으면 업로드
|
||||||
|
if (request.getImageUrl() != null && !request.getImageUrl().isEmpty()) {
|
||||||
|
uploadImage(page, request.getImageUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 발행 버튼 클릭
|
||||||
|
page.locator("button[class*='publish_btn']").click();
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
page.locator("button[class*='confirm_btn']").click();
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
|
||||||
|
page.waitForTimeout(5000); // 충분히 대기 필요
|
||||||
|
|
||||||
|
// 포스팅 URL 가져오기
|
||||||
|
String postUrl = page.url();
|
||||||
|
log.info("Post published successfully: {}", postUrl);
|
||||||
|
|
||||||
|
return postUrl;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to post to Naver blog: eventId={}, error={}",
|
||||||
|
request.getEventId(), e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (page != null) {
|
||||||
|
page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 상태 확인
|
||||||
|
*
|
||||||
|
* @param page Page
|
||||||
|
* @return 로그인 여부
|
||||||
|
*/
|
||||||
|
private boolean isLoggedIn(Page page) {
|
||||||
|
try {
|
||||||
|
page.navigate("https://blog.naver.com");
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
|
||||||
|
// 로그인 버튼이 보이지 않으면 로그인된 상태
|
||||||
|
// ID 기반 선택자 사용으로 strict mode violation 방지
|
||||||
|
return !page.locator("#gnb_login_button").isVisible();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to check login status", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네이버 로그인 (수동 로그인 대기 방식)
|
||||||
|
*
|
||||||
|
* @param page Page
|
||||||
|
* @throws Exception 로그인 실패 시
|
||||||
|
*/
|
||||||
|
private void login(Page page) throws Exception {
|
||||||
|
try {
|
||||||
|
log.info("Starting Naver manual login process");
|
||||||
|
log.info("=================================================");
|
||||||
|
log.info("Please login manually in the browser window");
|
||||||
|
log.info("브라우저 창에서 수동으로 로그인해주세요");
|
||||||
|
log.info("=================================================");
|
||||||
|
|
||||||
|
// 네이버 로그인 페이지로 이동
|
||||||
|
page.navigate("https://nid.naver.com/nidlogin.login");
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
|
||||||
|
// 사용자가 수동으로 로그인할 때까지 대기 (URL이 변경될 때까지)
|
||||||
|
// 로그인 성공 시 URL이 nid.naver.com에서 벗어남
|
||||||
|
log.info("Waiting for manual login... (Timeout: 30 seconds)");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 30초 동안 URL이 nid.naver.com을 벗어날 때까지 대기
|
||||||
|
page.waitForURL(url -> !url.contains("nid.naver.com"),
|
||||||
|
new Page.WaitForURLOptions().setTimeout(30000));
|
||||||
|
|
||||||
|
log.info("Login URL changed, assuming login successful");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Login timeout or failed", e);
|
||||||
|
throw new Exception("Manual login timeout or failed after 30 seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 추가 안정화 대기
|
||||||
|
page.waitForLoadState(LoadState.NETWORKIDLE);
|
||||||
|
Thread.sleep(2000); // 2초 추가 대기
|
||||||
|
|
||||||
|
// 세션 저장
|
||||||
|
context.storageState(new BrowserContext.StorageStateOptions()
|
||||||
|
.setPath(Paths.get(sessionPath, "naver-blog-session.json")));
|
||||||
|
|
||||||
|
log.info("Naver manual login successful, session saved");
|
||||||
|
log.info("Current URL: {}", page.url());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Naver manual login process failed", e);
|
||||||
|
throw new Exception("Naver manual login failed: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 업로드
|
||||||
|
*
|
||||||
|
* @param page Page
|
||||||
|
* @param imageUrl 이미지 URL
|
||||||
|
*/
|
||||||
|
private void uploadImage(Page page, String imageUrl) {
|
||||||
|
try {
|
||||||
|
log.debug("Uploading image: {}", imageUrl);
|
||||||
|
|
||||||
|
// 이미지 업로드 버튼 클릭
|
||||||
|
page.locator("button[aria-label='사진']").click();
|
||||||
|
|
||||||
|
// URL로 이미지 추가 (실제 구현은 네이버 블로그 UI에 따라 조정 필요)
|
||||||
|
// 여기서는 간단히 로그만 남김
|
||||||
|
log.info("Image upload placeholder - URL: {}", imageUrl);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to upload image: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright 리소스 정리
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
try {
|
||||||
|
if (context != null) {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
if (browser != null) {
|
||||||
|
browser.close();
|
||||||
|
}
|
||||||
|
if (playwright != null) {
|
||||||
|
playwright.close();
|
||||||
|
}
|
||||||
|
log.info("Playwright resources cleaned up");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to cleanup Playwright resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동으로 브라우저 컨텍스트 새로고침
|
||||||
|
* 장시간 사용 시 세션 만료 방지용
|
||||||
|
*/
|
||||||
|
public void refreshContext() {
|
||||||
|
try {
|
||||||
|
if (context != null) {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 세션 파일 경로
|
||||||
|
Path sessionFilePath = Paths.get(sessionPath, "naver-blog-session.json");
|
||||||
|
|
||||||
|
// 세션 파일이 있으면 로드, 없으면 새로운 컨텍스트 생성
|
||||||
|
if (Files.exists(sessionFilePath)) {
|
||||||
|
log.info("Refreshing context with existing session");
|
||||||
|
context = browser.newContext(new Browser.NewContextOptions()
|
||||||
|
.setStorageStatePath(sessionFilePath));
|
||||||
|
} else {
|
||||||
|
log.info("Refreshing context without session");
|
||||||
|
context = browser.newContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Browser context refreshed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to refresh context", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -32,6 +32,11 @@ public class ChannelDistributionResult {
|
|||||||
*/
|
*/
|
||||||
private String distributionId;
|
private String distributionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배포 URL (성공 시) - 실제 포스팅된 URL
|
||||||
|
*/
|
||||||
|
private String postUrl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 예상 노출 수 (성공 시)
|
* 예상 노출 수 (성공 시)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
@@ -225,6 +225,7 @@ public class DistributionService {
|
|||||||
.channel(result.getChannel())
|
.channel(result.getChannel())
|
||||||
.status(result.isSuccess() ? "COMPLETED" : "FAILED")
|
.status(result.isSuccess() ? "COMPLETED" : "FAILED")
|
||||||
.distributionId(result.getDistributionId())
|
.distributionId(result.getDistributionId())
|
||||||
|
.postUrl(result.getPostUrl())
|
||||||
.estimatedViews(result.getEstimatedReach())
|
.estimatedViews(result.getEstimatedReach())
|
||||||
.eventId(eventId)
|
.eventId(eventId)
|
||||||
.completedAt(completedAt)
|
.completedAt(completedAt)
|
||||||
|
|||||||
@@ -126,10 +126,11 @@ channel:
|
|||||||
# Naver Blog Configuration (Playwright 기반)
|
# Naver Blog Configuration (Playwright 기반)
|
||||||
naver:
|
naver:
|
||||||
blog:
|
blog:
|
||||||
|
enabled: ${NAVER_BLOG_ENABLED:false}
|
||||||
username: ${NAVER_BLOG_USERNAME:}
|
username: ${NAVER_BLOG_USERNAME:}
|
||||||
password: ${NAVER_BLOG_PASSWORD:}
|
password: ${NAVER_BLOG_PASSWORD:}
|
||||||
blog-id: ${NAVER_BLOG_ID:}
|
blog-id: ${NAVER_BLOG_ID:}
|
||||||
headless: ${NAVER_BLOG_HEADLESS:true}
|
headless: ${NAVER_BLOG_HEADLESS:false}
|
||||||
session-path: ${NAVER_BLOG_SESSION_PATH:playwright-sessions}
|
session-path: ${NAVER_BLOG_SESSION_PATH:playwright-sessions}
|
||||||
|
|
||||||
# Springdoc OpenAPI (Swagger)
|
# Springdoc OpenAPI (Swagger)
|
||||||
|
|||||||
Reference in New Issue
Block a user