diff --git a/analytics-service/src/main/java/kt/aivle/analytics/adapter/in/websocket/ReportWebSocketHandler.java b/analytics-service/src/main/java/kt/aivle/analytics/adapter/in/websocket/ReportWebSocketHandler.java index 076eca8..9010ef6 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/adapter/in/websocket/ReportWebSocketHandler.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/adapter/in/websocket/ReportWebSocketHandler.java @@ -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() diff --git a/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/SnsAccountRepository.java b/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/SnsAccountRepository.java index 2849e87..4efe0aa 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/SnsAccountRepository.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/SnsAccountRepository.java @@ -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 @@ -39,7 +38,10 @@ public List findByUserId(Long userId) { return snsAccountJpaRepository.findByUserId(userId); } - + @Override + public Optional findUserIdByAccountId(Long accountId) { + return snsAccountJpaRepository.findUserIdByAccountId(accountId); + } @Override public void deleteById(Long id) { diff --git a/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/repository/SnsAccountJpaRepository.java b/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/repository/SnsAccountJpaRepository.java index 2274bd0..fd98b10 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/repository/SnsAccountJpaRepository.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/adapter/out/persistence/repository/SnsAccountJpaRepository.java @@ -12,4 +12,7 @@ public interface SnsAccountJpaRepository extends BaseJpaRepository { List findByUserId(Long userId); List findByUserIdAndType(Long userId, SnsType type); + + // accountId로 userId 조회 + Optional findUserIdByAccountId(Long accountId); } diff --git a/analytics-service/src/main/java/kt/aivle/analytics/application/port/in/AnalyticsQueryUseCase.java b/analytics-service/src/main/java/kt/aivle/analytics/application/port/in/AnalyticsQueryUseCase.java index 93f2219..68c8d8a 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/application/port/in/AnalyticsQueryUseCase.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/application/port/in/AnalyticsQueryUseCase.java @@ -32,5 +32,5 @@ public interface AnalyticsQueryUseCase { ReportResponse generateReport(Long userId, Long accountId, Long postId, Long storeId); // 통합된 비동기 AI 보고서 생성 (WebSocket용) - 캐시 확인 포함 - CompletableFuture> generateReportAsync(Long userId, Long accountId, Long postId, Long storeId); + CompletableFuture> generateReportAsync(Long accountId, Long postId, Long storeId); } diff --git a/analytics-service/src/main/java/kt/aivle/analytics/application/port/out/repository/SnsAccountRepositoryPort.java b/analytics-service/src/main/java/kt/aivle/analytics/application/port/out/repository/SnsAccountRepositoryPort.java index 6642b1b..8c57c72 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/application/port/out/repository/SnsAccountRepositoryPort.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/application/port/out/repository/SnsAccountRepositoryPort.java @@ -14,7 +14,9 @@ public interface SnsAccountRepositoryPort { long countAll(); List findByUserId(Long userId); - + + // accountId로 userId 조회 + Optional findUserIdByAccountId(Long accountId); void deleteById(Long id); List findAllWithPagination(int page, int size); diff --git a/analytics-service/src/main/java/kt/aivle/analytics/application/service/AnalyticsQueryService.java b/analytics-service/src/main/java/kt/aivle/analytics/application/service/AnalyticsQueryService.java index afbce7d..60efe79 100644 --- a/analytics-service/src/main/java/kt/aivle/analytics/application/service/AnalyticsQueryService.java +++ b/analytics-service/src/main/java/kt/aivle/analytics/application/service/AnalyticsQueryService.java @@ -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; @@ -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; @@ -197,9 +199,13 @@ public ReportResponse generateReport(Long userId, Long accountId, Long postId, L // 통합된 비동기 AI 보고서 생성 (WebSocket용) - 캐시 확인 포함 @Override - public CompletableFuture> generateReportAsync(Long userId, Long accountId, Long postId, Long storeId) { + public CompletableFuture> 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); @@ -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 검증 */ diff --git a/gateway/src/main/java/kt/aivle/gateway/config/ExcludePaths.java b/gateway/src/main/java/kt/aivle/gateway/config/ExcludePaths.java index bf9f951..6e5a614 100644 --- a/gateway/src/main/java/kt/aivle/gateway/config/ExcludePaths.java +++ b/gateway/src/main/java/kt/aivle/gateway/config/ExcludePaths.java @@ -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/**", diff --git a/gateway/src/main/java/kt/aivle/gateway/filter/JwtAuthenticationFilter.java b/gateway/src/main/java/kt/aivle/gateway/filter/JwtAuthenticationFilter.java index bf4e3d0..36c5dec 100644 --- a/gateway/src/main/java/kt/aivle/gateway/filter/JwtAuthenticationFilter.java +++ b/gateway/src/main/java/kt/aivle/gateway/filter/JwtAuthenticationFilter.java @@ -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 @@ -41,7 +38,7 @@ public Mono 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); @@ -90,44 +87,14 @@ public Mono 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);