Skip to content

Commit 66629ba

Browse files
trz-21claude
andcommitted
feat: 내 제품 vs 경쟁사 비교 분석으로 전환 — 내 제품 개선점 도출
링크 입력을 평면 urls(전부 경쟁사)에서 내 제품 URL(필수) + 경쟁 상품 URL(1~3개)로 분리해 받고, 두 제품군 리뷰를 비교하여 내 제품에서 개선할 점을 도출하도록 변경. BE: ProductReviews/ProductSummary.is_mine, Analysis.my_url(+마이그레이션), Insights.comparison_summary(additive), create_analysis(my_url,competitor_urls) 및 파이프라인 역할 구분 크롤링, Mock/Gemini 분석기 비교 관점 재작성, 핸들러 요청/뷰 갱신, 통합 테스트 보강. FE: 입력 분리(UrlInput), 비교 총평/내제품 배지/평균평점 비교 헤더, AnalysisCard·types·dashboard 갱신, 테스트 갱신. Refs #34 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e419a4b commit 66629ba

28 files changed

Lines changed: 652 additions & 183 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE analyses ADD COLUMN my_url TEXT;

backend/src/adapters/coupang/crawler.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ impl CoupangCrawler for MockCoupangCrawler {
6767
url: url.to_string(),
6868
product_name: Self::product_name_for(url),
6969
reviews,
70+
is_mine: false,
7071
})
7172
}
7273
}
@@ -101,9 +102,7 @@ impl HttpCoupangCrawler {
101102
/// 없으면 타겟 URL 을 그대로 반환(직접 호출).
102103
pub fn resolve_request_url(&self, target: &str) -> String {
103104
match &self.proxy_url {
104-
Some(tmpl) if tmpl.contains("{url}") => {
105-
tmpl.replace("{url}", &percent_encode(target))
106-
}
105+
Some(tmpl) if tmpl.contains("{url}") => tmpl.replace("{url}", &percent_encode(target)),
107106
// 템플릿에 {url} 가 없으면 쿼리로 덧붙인다(?url= 또는 &url=).
108107
Some(tmpl) => {
109108
let sep = if tmpl.contains('?') { '&' } else { '?' };
@@ -142,7 +141,14 @@ impl HttpCoupangCrawler {
142141
return r;
143142
}
144143
}
145-
for k in ["reviews", "data", "content", "reviewList", "items", "contents"] {
144+
for k in [
145+
"reviews",
146+
"data",
147+
"content",
148+
"reviewList",
149+
"items",
150+
"contents",
151+
] {
146152
if let Some(arr) = json.get(k).and_then(|v| v.as_array()) {
147153
let r: Vec<Review> = arr.iter().filter_map(Self::parse_one_review).collect();
148154
if !r.is_empty() {
@@ -262,7 +268,10 @@ impl CoupangCrawler for HttpCoupangCrawler {
262268
.client
263269
.get(&request_url)
264270
.header("User-Agent", USER_AGENT)
265-
.header("Referer", format!("https://www.coupang.com/vp/products/{product_id}"))
271+
.header(
272+
"Referer",
273+
format!("https://www.coupang.com/vp/products/{product_id}"),
274+
)
266275
.header("Accept", "application/json, text/plain, */*")
267276
.header("Accept-Language", "ko-KR,ko;q=0.9,en;q=0.8")
268277
.header("sec-fetch-dest", "empty")
@@ -295,13 +304,14 @@ impl CoupangCrawler for HttpCoupangCrawler {
295304
let mut reviews = Self::parse_reviews(&json);
296305
reviews.truncate(limit as usize);
297306

298-
let product_name = Self::find_product_name(&json)
299-
.unwrap_or_else(|| format!("쿠팡 상품 {product_id}"));
307+
let product_name =
308+
Self::find_product_name(&json).unwrap_or_else(|| format!("쿠팡 상품 {product_id}"));
300309

301310
Ok(ProductReviews {
302311
url: url.to_string(),
303312
product_name,
304313
reviews,
314+
is_mine: false,
305315
})
306316
}
307317
}
@@ -412,7 +422,8 @@ mod tests {
412422
reqwest::Client::new(),
413423
Some("https://api.scraperapi.com/?api_key=KEY&url={url}".into()),
414424
);
415-
let out = c.resolve_request_url("https://www.coupang.com/next-api/review?productId=1&size=2");
425+
let out =
426+
c.resolve_request_url("https://www.coupang.com/next-api/review?productId=1&size=2");
416427
assert!(out.starts_with("https://api.scraperapi.com/?api_key=KEY&url="));
417428
// 타겟 URL 은 percent-encode 되어야 한다(콜론/슬래시/물음표/앰퍼샌드).
418429
assert!(out.contains("https%3A%2F%2Fwww.coupang.com"));

0 commit comments

Comments
 (0)