diff --git a/src/main/java/com/team4/giftidea/configuration/SecurityConfig.java b/src/main/java/com/team4/giftidea/configuration/SecurityConfig.java index 0662e75..993d918 100644 --- a/src/main/java/com/team4/giftidea/configuration/SecurityConfig.java +++ b/src/main/java/com/team4/giftidea/configuration/SecurityConfig.java @@ -2,11 +2,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.ResponseEntity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.bind.annotation.*; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import java.util.List; @@ -14,29 +18,18 @@ * Spring Security 및 CORS 설정을 담당하는 설정 클래스입니다. */ @Configuration +@RestController +@RequestMapping("/api") // 모든 API 요청 처리 public class SecurityConfig { /** * HTTP 보안 설정을 구성하는 Bean입니다. - * - * - CORS 설정 적용 - * - CSRF 보호 비활성화 (JWT 사용 시 필요) - * - 특정 경로 보호 및 기본 요청 허용 설정 - * - * @param http Spring Security의 HTTP 보안 설정 객체 - * @return SecurityFilterChain 보안 필터 체인 - * @throws Exception 설정 과정에서 발생할 수 있는 예외 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // CORS 설정 적용 - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - - // CSRF 보호 비활성화 (JWT 인증을 사용하는 경우 필요) - .csrf(csrf -> csrf.disable()) - - // 접근 제어 설정 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 적용 + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").authenticated() // "/admin/**" 경로는 인증 필요 .anyRequest().permitAll() // 나머지 요청은 인증 없이 허용 @@ -47,28 +40,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti /** * CORS 설정을 구성하는 Bean입니다. - * - * - 허용할 도메인(origin) 설정 - * - 허용할 HTTP 메서드(GET, POST 등) 지정 - * - 허용할 헤더 설정 - * - 쿠키 포함 요청 허용 - * - * @return CorsConfigurationSource CORS 설정 객체 */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - - // 허용할 출처(Origin) 설정 + + // 허용할 출처 설정 configuration.setAllowedOrigins(List.of( - "http://localhost:3000", // 로컬 개발 환경 - "https://presentalk.store", // 프론트엔드 배포 주소 - "https://app.presentalk.store" // 백엔드 API 주소 + "http://localhost:5174", + "http://localhost:3000", + "https://presentalk.store", + "https://app.presentalk.store" )); // 허용할 HTTP 메서드 설정 configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - + // 허용할 요청 헤더 설정 configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용 @@ -77,8 +64,26 @@ public CorsConfigurationSource corsConfigurationSource() { // CORS 설정을 특정 경로에 적용 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용 + source.registerCorsConfiguration("/**", configuration); return source; } + + /** + * CORS 필터가 Spring Security보다 먼저 실행되도록 설정 + */ + @Bean + public FilterRegistrationBean corsFilter() { + FilterRegistrationBean filterBean = new FilterRegistrationBean<>(new CorsFilter(corsConfigurationSource())); + filterBean.setOrder(0); // 가장 먼저 실행되도록 설정 + return filterBean; + } + + /** + * OPTIONS 요청을 수동으로 처리 (CORS 문제 해결) + */ + @RequestMapping(value = "/**", method = RequestMethod.OPTIONS) + public ResponseEntity handleOptions() { + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/team4/giftidea/service/CoupangApiService.java b/src/main/java/com/team4/giftidea/service/CoupangApiService.java index 6cf5afd..baba671 100644 --- a/src/main/java/com/team4/giftidea/service/CoupangApiService.java +++ b/src/main/java/com/team4/giftidea/service/CoupangApiService.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.Collections; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -41,62 +42,97 @@ public CoupangApiService(ProductService productService) { } /** - * 주어진 키워드에 대해 쿠팡에서 상품을 검색하고 리스트로 반환합니다. - * - * @param query 검색 키워드 - * @return 크롤링된 상품 리스트 - */ - public List searchItems(String query) { - List productList = new ArrayList<>(); - System.setProperty("webdriver.chrome.driver", chromeDriverPath); - - ChromeOptions options = new ChromeOptions(); - options.setBinary("/opt/google/chrome/chrome"); // 크롬 바이너리 직접 지정 (AWS 환경) - options.addArguments("--disable-gpu"); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - options.addArguments("--remote-debugging-port=9222"); - options.addArguments("--window-size=1920,1080"); - options.addArguments("--disable-software-rasterizer"); - options.addArguments("--disable-crash-reporter"); - options.addArguments("--disable-extensions"); - options.addArguments("--disable-hang-monitor"); + * 주어진 키워드에 대해 쿠팡에서 상품을 검색하고 리스트로 반환합니다. + * + * @param query 검색 키워드 + * @return 크롤링된 상품 리스트 + */ + public List searchItems(String query) { + List productList = new ArrayList<>(); + System.setProperty("webdriver.chrome.driver", chromeDriverPath); + ChromeOptions options = new ChromeOptions(); + options.setBinary("/opt/google/chrome/chrome"); // 크롬 바이너리 직접 지정 (AWS 환경) + options.addArguments("--headless"); // 기본적으로 headless 모드 유지 + options.addArguments("--disable-gpu"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + options.addArguments("--remote-debugging-port=9222"); + options.addArguments("--window-size=1920,1080"); + options.addArguments("--disable-software-rasterizer"); + options.addArguments("--disable-extensions"); + options.addArguments("--disable-popup-blocking"); - // 최신 User-Agent 추가 - options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.63 Safari/537.36"); + // 최신 User-Agent 추가 (봇 탐지 우회) + options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.63 Safari/537.36"); + + WebDriver driver = new ChromeDriver(options); - WebDriver driver = new ChromeDriver(options); - - try { - String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8); - String searchUrl = String.format(COUPANG_SEARCH_URL, encodedQuery); - - driver.get(searchUrl); - WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(90)); - wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".search-product"))); - - List products = driver.findElements(By.cssSelector(".search-product")); - - for (WebElement productElement : products) { - try { - Product productEntity = extractProductInfo(productElement, query); - if (productEntity != null) { - productList.add(productEntity); - productService.saveItems(List.of(productEntity), query); - } - } catch (NoSuchElementException e) { - log.warn("요소를 찾을 수 없음: {}", e.getMessage()); - } - } - } catch (TimeoutException e) { - log.error("페이지 로딩 시간 초과: {}", e.getMessage()); - } catch (Exception e) { - log.error("크롤링 중 오류 발생: {}", e.getMessage()); - } finally { - driver.quit(); // 크롤링 후 브라우저 종료 - } - - return productList; + try { + log.info("검색어: {}", query); + + String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8); + String searchUrl = String.format("https://www.coupang.com/np/search?q=%s&channel=user", encodedQuery); + + log.info("쿠팡 검색 URL: {}", searchUrl); + driver.get(searchUrl); + + log.info("쿠팡 페이지 접속 완료. 5초 대기 중..."); + Thread.sleep(5000); // 페이지 로딩 대기 + + // navigator.webdriver 감추기 + JavascriptExecutor js = (JavascriptExecutor) driver; + js.executeScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"); + + log.info("navigator.webdriver 속성 제거 완료."); + + // 현재 페이지의 HTML 확인 + String pageSource = driver.getPageSource(); + log.debug("현재 페이지 HTML (앞부분): {}", pageSource.substring(0, Math.min(1000, pageSource.length()))); + + // 페이지가 제대로 로드되었는지 확인 + if (pageSource.contains("captcha") || pageSource.contains("robot check")) { + log.error("쿠팡에서 봇 탐지를 수행함. 크롤링 차단됨."); + return Collections.emptyList(); + } + + log.info("상품 리스트 로딩 시작..."); + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(90)); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".search-product"))); + + List products = driver.findElements(By.cssSelector(".search-product")); + + if (products.isEmpty()) { + log.warn("검색 결과가 없음. 크롤링할 제품 없음."); + return Collections.emptyList(); + } + + log.info("총 {}개의 상품 발견", products.size()); + + for (WebElement productElement : products) { + try { + Product productEntity = extractProductInfo(productElement, query); + if (productEntity != null) { + productList.add(productEntity); + productService.saveItems(List.of(productEntity), query); + log.info("상품 저장 완료: {}", productEntity.getTitle()); + } + } catch (NoSuchElementException e) { + log.warn("요소를 찾을 수 없음: {}", e.getMessage()); + } + } + + } catch (TimeoutException e) { + log.error("페이지 로딩 시간 초과: {}", e.getMessage()); + } catch (WebDriverException e) { + log.error("ChromeDriver 관련 오류 발생: {}", e.getMessage()); + } catch (Exception e) { + log.error("크롤링 중 알 수 없는 오류 발생: {}", e.getMessage()); + } finally { + log.info("ChromeDriver 종료..."); + driver.quit(); + } + + return productList; } /** diff --git a/src/main/java/com/team4/giftidea/service/KreamApiService.java b/src/main/java/com/team4/giftidea/service/KreamApiService.java index 051318f..5cc3519 100644 --- a/src/main/java/com/team4/giftidea/service/KreamApiService.java +++ b/src/main/java/com/team4/giftidea/service/KreamApiService.java @@ -48,19 +48,13 @@ public List searchItems(String query) { System.setProperty("webdriver.chrome.driver", chromeDriverPath); ChromeOptions options = new ChromeOptions(); - options.setBinary("/opt/google/chrome/chrome"); // 크롬 바이너리 직접 지정 (AWS 환경) + options.setBinary("/opt/google/chrome/chrome"); + options.addArguments("--headless"); // Headless 모드 유지 options.addArguments("--disable-gpu"); options.addArguments("--no-sandbox"); options.addArguments("--disable-dev-shm-usage"); options.addArguments("--remote-debugging-port=9222"); - options.addArguments("--window-size=1920,1080"); // 창 크기 설정 - options.addArguments("--disable-software-rasterizer"); - options.addArguments("--disable-crash-reporter"); - options.addArguments("--disable-extensions"); - options.addArguments("--disable-hang-monitor"); - - // 최신 User-Agent 추가 - options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.63 Safari/537.36"); + options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.90 Safari/537.36"); // 브라우저 속이기 WebDriver driver = new ChromeDriver(options);