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
63 changes: 34 additions & 29 deletions src/main/java/com/team4/giftidea/configuration/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,34 @@

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;

/**
* 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() // 나머지 요청은 인증 없이 허용
Expand All @@ -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("*")); // 모든 헤더 허용

Expand All @@ -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> corsFilter() {
FilterRegistrationBean<CorsFilter> 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();
}
}
144 changes: 90 additions & 54 deletions src/main/java/com/team4/giftidea/service/CoupangApiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,62 +42,97 @@ public CoupangApiService(ProductService productService) {
}

/**
* 주어진 키워드에 대해 쿠팡에서 상품을 검색하고 리스트로 반환합니다.
*
* @param query 검색 키워드
* @return 크롤링된 상품 리스트
*/
public List<Product> searchItems(String query) {
List<Product> 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<Product> searchItems(String query) {
List<Product> 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<WebElement> 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<WebElement> 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;
}

/**
Expand Down
12 changes: 3 additions & 9 deletions src/main/java/com/team4/giftidea/service/KreamApiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,13 @@ public List<Product> 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);

Expand Down
Loading