Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -76,30 +76,15 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
private void processReportAsync(WebSocketSession session, ReportRequestMessage request) {
String taskId = "task-" + taskCounter.incrementAndGet();


Long userId;
try {
// Gateway에서 제공하는 X-USER-ID 헤더에서 userId 추출
String userIdHeader = session.getHandshakeHeaders().getFirst("X-USER-ID");
if (userIdHeader == null) {
sendMessage(session, WebSocketResponseMessage.error("사용자 인증 정보가 없습니다."));
return;
}
userId = Long.parseLong(userIdHeader);
} catch (NumberFormatException e) {
sendMessage(session, WebSocketResponseMessage.error("잘못된 사용자 ID 형식입니다."));
return;
}

log.info("[WebSocket] AI 분석 보고서 생성 요청 - userId: {}, postId: {}, accountId: {}, storeId: {}",
userId, request.getPostId(), request.getAccountId(), request.getStoreId());
log.info("[WebSocket] AI 분석 보고서 생성 요청 - postId: {}, accountId: {}, storeId: {}",
request.getPostId(), request.getAccountId(), request.getStoreId());

// 진행률 전송 시작
sendMessage(session, WebSocketResponseMessage.progress(0, "AI 분석 보고서 생성을 시작합니다..."));

// 통합된 비동기 메서드 호출 (캐시 확인 포함)
// accountId로 userId 조회 후 AI 분석 진행
// userId는 sns_account 테이블에서 조회하여 사용
analyticsQueryUseCase.generateReportAsync(
userId, // 헤더에서 추출한 userId 사용
request.getAccountId(),
request.getPostId(),
request.getStoreId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import kt.aivle.analytics.adapter.out.persistence.repository.SnsAccountJpaRepository;
import kt.aivle.analytics.application.port.out.repository.SnsAccountRepositoryPort;
import kt.aivle.analytics.domain.entity.SnsAccount;

import lombok.RequiredArgsConstructor;

@Repository
Expand Down Expand Up @@ -39,7 +38,10 @@ public List<SnsAccount> findByUserId(Long userId) {
return snsAccountJpaRepository.findByUserId(userId);
}


@Override
public Optional<Long> findUserIdByAccountId(Long accountId) {
return snsAccountJpaRepository.findUserIdByAccountId(accountId);
}

@Override
public void deleteById(Long id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
public interface SnsAccountJpaRepository extends BaseJpaRepository<SnsAccount, Long> {
List<SnsAccount> findByUserId(Long userId);
List<SnsAccount> findByUserIdAndType(Long userId, SnsType type);

// accountId로 userId 조회
Optional<Long> findUserIdByAccountId(Long accountId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ public interface AnalyticsQueryUseCase {
ReportResponse generateReport(Long userId, Long accountId, Long postId, Long storeId);

// 통합된 비동기 AI 보고서 생성 (WebSocket용) - 캐시 확인 포함
CompletableFuture<WebSocketResponseMessage<ReportResponse>> generateReportAsync(Long userId, Long accountId, Long postId, Long storeId);
CompletableFuture<WebSocketResponseMessage<ReportResponse>> generateReportAsync(Long accountId, Long postId, Long storeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public interface SnsAccountRepositoryPort {

long countAll();
List<SnsAccount> findByUserId(Long userId);


// accountId로 userId 조회
Optional<Long> findUserIdByAccountId(Long accountId);

void deleteById(Long id);
List<SnsAccount> findAllWithPagination(int page, int size);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import kt.aivle.analytics.application.port.out.infrastructure.ValidationPort;
import kt.aivle.analytics.application.port.out.repository.PostCommentKeywordRepositoryPort;
import kt.aivle.analytics.application.port.out.repository.SnsAccountMetricRepositoryPort;
import kt.aivle.analytics.application.port.out.repository.SnsAccountRepositoryPort;
import kt.aivle.analytics.application.port.out.repository.SnsPostCommentMetricRepositoryPort;
import kt.aivle.analytics.application.port.out.repository.SnsPostMetricRepositoryPort;
import kt.aivle.analytics.application.port.out.repository.SnsPostRepositoryPort;
Expand All @@ -55,6 +56,7 @@ public class AnalyticsQueryService implements AnalyticsQueryUseCase {
private final SnsAccountMetricRepositoryPort snsAccountMetricRepositoryPort;
private final SnsPostCommentMetricRepositoryPort snsPostCommentMetricRepositoryPort;
private final SnsPostRepositoryPort snsPostRepositoryPort;
private final SnsAccountRepositoryPort snsAccountRepositoryPort;
private final PostCommentKeywordRepositoryPort postCommentKeywordRepository;
private final ExternalApiPort externalApiPort;
private final ValidationPort validationPort;
Expand Down Expand Up @@ -197,9 +199,13 @@ public ReportResponse generateReport(Long userId, Long accountId, Long postId, L

// 통합된 비동기 AI 보고서 생성 (WebSocket용) - 캐시 확인 포함
@Override
public CompletableFuture<WebSocketResponseMessage<ReportResponse>> generateReportAsync(Long userId, Long accountId, Long postId, Long storeId) {
public CompletableFuture<WebSocketResponseMessage<ReportResponse>> generateReportAsync(Long accountId, Long postId, Long storeId) {
log.info("[WebSocket] 비동기 AI 보고서 생성 시작 - postId: {}", postId);

// accountId로 userId 조회
Long userId = getUserIdByAccountId(accountId);
log.info("[WebSocket] accountId {}로 userId {} 조회", accountId, userId);

// 1. 캐시 확인
return CompletableFuture.supplyAsync(() -> {
log.info("[WebSocket] 캐시 확인 중 - postId: {}", postId);
Expand Down Expand Up @@ -456,6 +462,14 @@ private void validatePostExists(Long postId) {
.orElseThrow(() -> new BusinessException(AnalyticsErrorCode.POST_NOT_FOUND));
}

/**
* 계정 ID로 사용자 ID 조회
*/
private Long getUserIdByAccountId(Long accountId) {
return snsAccountRepositoryPort.findUserIdByAccountId(accountId)
.orElseThrow(() -> new BusinessException(AnalyticsErrorCode.ACCOUNT_NOT_FOUND));
}

/**
* 게시물의 계정 ID 검증
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class ExcludePaths {
"/api/auth/oauth2/code/**",
"/api/sns/oauth/**/callback/**",

// WebSocket
"/api/analytics/report",

// swagger
"/swagger-ui.html",
"/swagger-ui/**",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;

import static kt.aivle.gateway.exception.GatewayErrorCode.*;

@Slf4j
Expand All @@ -41,7 +38,7 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}

// 1. 토큰 추출 (WebSocket이면 쿼리 파라미터에서, HTTP면 헤더에서)
// 1. 토큰 추출 (HTTP 헤더에서)
String token = extractToken(exchange);
if (token == null) {
throw new BusinessException(INVALID_TOKEN);
Expand Down Expand Up @@ -90,44 +87,14 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

private String extractToken(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();

log.info("[JWT Filter] URI: {}, Scheme: {}, Query: {}", uri, uri.getScheme(), uri.getQuery());
log.info("[JWT Filter] Headers - Upgrade: {}, Connection: {}",
request.getHeaders().getFirst("Upgrade"),
request.getHeaders().getFirst("Connection"));

// WebSocket 요청인지 확인 (HTTP 헤더 기반)
boolean isWebSocket = isWebSocketRequest(request);
log.info("[JWT Filter] Is WebSocket: {}", isWebSocket);

if (isWebSocket) {
// WebSocket: 쿼리 파라미터에서 token 추출
String token = extractTokenFromQuery(uri);
log.info("[JWT Filter] WebSocket token extracted: {}", token != null ? "SUCCESS" : "FAILED");
return token;
} else {
// HTTP: Authorization 헤더에서 token 추출
String token = extractTokenFromHeader(request);
log.info("[JWT Filter] HTTP token extracted: {}", token != null ? "SUCCESS" : "FAILED");
return token;
}
// HTTP: Authorization 헤더에서 token 추출
String token = extractTokenFromHeader(request);
log.info("[JWT Filter] HTTP token extracted: {}", token != null ? "SUCCESS" : "FAILED");
return token;
}

private boolean isWebSocketRequest(ServerHttpRequest request) {
String upgrade = request.getHeaders().getFirst("Upgrade");
String connection = request.getHeaders().getFirst("Connection");

return "websocket".equalsIgnoreCase(upgrade) &&
"upgrade".equalsIgnoreCase(connection);
}

private String extractTokenFromQuery(URI uri) {
return UriComponentsBuilder.fromUri(uri)
.build()
.getQueryParams()
.getFirst("token");
}

private String extractTokenFromHeader(ServerHttpRequest request) {
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
Expand Down
Loading