8000 feat: 올리브영 오특(오늘의 특가) 조회 추가 by chuuminggg · Pull Request #331 · hmmhmmhm/daiso-mcp · GitHub
[go: up one dir, main page]

Skip to content

feat: 올리브영 오특(오늘의 특가) 조회 추가 - #331

Merged
hmmhmmhm merged 13 commits into
hmmhmmhm:mainfrom
chuuminggg:feat/oliveyoung-today-deals
Nov 1, 2026
Merged

hmmhmmhm merged 13 commits into
hmmhmmhm:mainfrom
chuuminggg:feat/oliveyoung-today-deals

Conversation

@chuuminggg
Copy link
Copy Markdown
Contributor

변경 요약

올리브영 오특(오늘의 특가) 화면을 조회하는 기능을 제안하고 구현했습니다. 공개 도구를 늘리지 않고 기존 get_catalog에 작업을 하나 추가했습니다.

get_catalog {"service":"oliveyoung","operation":"today_deals"}

오특 화면은 세 섹션으로 되어 있습니다. 이 PR에서는 가격과 상품번호가 있는 두 섹션을 구현했고, coming soon은 후속으로 제안합니다.

화면 섹션 이 PR 원본 데이터
오늘의 특가 (인기순 전체) ✅ todayDeals GET /store/main/getHotdealPagingListAjax.do?date=YYYYMMDD&pageIdx=N&... (HTML 조각, 10개씩)
오늘의 브랜드 특가 ✅ brandDeals GET /store/main/getHotdealList.do 페이지 HTML에 처음부터 포함
coming soon (다음 오특 예고) 💡 후속 제안 같은 페이지 HTML의 배너. 상품명과 이미지만 있음

관련 이슈

  • GitHub 이슈는 없습니다. MCP submit_request(개발자 요청)로 같은 기능 제안과 기여 의사를 먼저 전달했습니다 (요청 ID 2c639517-4ffe-4827-86e5-236f44b1af8c).

동작 방식

오특에는 JSON API가 없고 HTML로만 제공됩니다. 그래서 고정 경로와 고정 인수만 GET으로 읽고, 상품 카드 HTML을 파싱합니다. 원본 분석은 docs/oliveyoung-today-deals-analysis.md에 정리했습니다.

get_catalog(service=oliveyoung, operation=today_deals)
  oliveyoung_get_today_deals            # 내부 작업, 공개 목록에는 노출하지 않음
    fetchOliveyoungTodayDeals(KST 날짜)
      requestOliveyoung(today-deals-page, {date})            -> brandDeals (코너 "스페셜")
      repeat pageIdx = 1..20
        requestOliveyoung(today-deals-list, {date, pageIdx}) -> todayDeals
        stop when page has < 10 cards
 src/services/oliveyoung/
+├── htmlRoute.ts           # 고정 경로·인수 검증, 보안 확인 화면 거절, {status:SUCCESS, data:{html}} 포장
+├── todayDeals.ts          # 카드 파서와 페이지 순회 (전체 제한 시간 공유)
+├── tools/getTodayDeals.ts # 내부 작업 oliveyoung_get_today_deals
 ├── transport.ts           # 직접 조회는 GET HTML, 중계는 today-deals-* 작업 이름 사용
 └── api.ts                 # TODAY_DEALS_PAGE_PATH, TODAY_DEALS_LIST_PATH
 scripts/relay/
 ├── oliveyoung.ts          # today-deals-page {date}, today-deals-list {date, pageIdx} 허용 (TTL 5분)
 └── browser.ts             # HTML 경로만 GET으로 읽고 정상 오특 HTML이 아니면 invalid-html 실패
 src/core/compact/
 ├── catalog.ts             # get_catalog에 defaultService=seveneleven 및 oliveyoung/today_deals 경로
 └── tools.ts               # defaultService가 있으면 service 생략 가능, operation은 계속 필수

상품마다 반환하는 값: rank, goodsNo, name, brand, category, price, priceFrom(화면의 "원~", 옵션별 최저가), originalPrice, discountRate, flags(세일/쿠폰/증정/1+1 등), todayDelivery(오늘드림), imageUrl, productUrl

정확도 원칙 (ALIGNS.md)에 맞춰 처리한 부분:

  • 보안 확인 화면(_cf_chl, "잠시만 기다려"), 다른 페이지, HTTP 200이 아닌 응답, 필수 값이 빠진 카드는 빈 목록이 아니라 오류로 반환합니다.
  • 품절 표시는 관측하지 못해서 반환하지 않습니다. 실시간 재고가 아니므로 재고는 check_inventory(service=oliveyoung)로 안내합니다.
  • 날짜는 한국 시간 기준입니다. 오특은 한국 자정에 바뀝니다. 중계 캐시 키에도 날짜가 들어가서 자정 전후 응답이 섞이지 않습니다.

기능 제안: coming soon (다음 오특 예고)

화면 하단의 "잠시 후 12시 새로운 오특 오픈!" 배너는 같은 페이지 HTML(.csBanInner .slideItem)에 들어 있습니다. 관측 결과는 아래와 같습니다.

  • 상품명과 썸네일 이미지 URL만 있고 상품번호, 가격, 할인율은 없습니다. 썸네일 파일명에서 상품번호를 추정할 수는 있지만 확정된 계약은 아닙니다.
  • 넘기는 배너라서 같은 상품명이 반복되므로 중복 제거가 필요합니다.
  • 사용자 관측 기준으로 대략 한국 시간 9시부터 표시됩니다.

이번 PR에서 브랜드 특가 파싱에 쓰는 페이지 요청(today-deals-page)을 그대로 재사용할 수 있습니다. 그래서 추가 원본 요청 없이 upcomingDeals: [{ name, imageUrl }] 정도로 확장할 수 있습니다. 가격이 없는 예고 정보라서 범위에서 제외했고, 필요하시면 후속 PR로 기여하겠습니다.

체크리스트

  • npm run format:check 통과: Windows CRLF 작업 트리에서는 main에서도 25개 파일이 실패합니다. --end-of-line auto로 확인했고, 새 문서는 prettier로 정렬했습니다. CI 결과로 확인 부탁드립니다.
  • npm run lint 통과 (eslint, biome)
  • npm run typecheck 통과
  • npm run check:source-lines 통과 (최대 450줄)
  • npm test 통과: 이번 변경과 관련된 테스트는 모두 통과했습니다. 아래 "참고 사항"에 Windows 환경에서만 실패하는 테스트를 정리했습니다.
  • 필요 시 문서 업데이트 (docs/mcp-tools.md, docs/service-reference.md, docs/ai-instruction.md, 안내 프롬프트, 새 분석 문서)
  • 민감 정보(API 키/토큰/개인정보) 미포함 확인. 테스트 데이터는 공개 오특 화면 HTML을 줄여서 썼습니다.

테스트 방법

npx vitest run tests/services/oliveyoung tests/core/compact tests/relay/browser.test.ts tests/relay/oliveyoung.test.ts

단위 테스트

파일 검증 내용
tests/services/oliveyoung/todayDeals.test.ts 실제 2026-10-26 오특 HTML로 브랜드 특가 4개와 목록 파싱, 원~·오늘드림 미제공(B 상품번호)·1+1 구분, 문자 참조, 필수 값 누락 거절, KST 자정, 마지막 페이지·빈 페이지·중복 제거·20페이지 잘림, 보안 확인 화면·HTTP201 실패, 중계 작업 이름과 본문, 전체 제한 시간
tests/services/oliveyoung/htmlRoute.test.ts 계약 밖 경로·인수(날짜 형식, 페이지 범위, 추가 키) 거절, 정상 HTML만 성공 포장
tests/services/oliveyoung/tools/getTodayDeals.test.ts 출력 스키마, 기본 제한 시간(직접 15초 / 중계 60초), 실패 전파
tests/relay/browser.test.ts 오특은 GET으로 고정 주소 조회, 보안 확인 화면·HTTP403·잘못된 날짜 거절
tests/relay/oliveyoung.test.ts 오특 작업은 날짜·페이지만 허용, 추가 인수 400
tests/core/compact/tools.test.ts service 생략 시 세븐일레븐 유지, oliveyoung/today_deals 연결, 잘못된 조합 거절, 내부 작업 45개 매핑

새로 바꾼 파일(todayDeals.ts, htmlRoute.ts, getTodayDeals.ts, transport.ts, compact/*, relay/browser.ts 추가 분기)의 커버리지는 Statements/Branches/Functions/Lines 모두 100%입니다.

실제 원본 통합 테스트 (2026-10-26 KST)

로컬에서 실제 MCP 서버(/mcp) → get_catalog → 로컬 브라우저 중계(createOliveyoungRelay + createBrowserRunner, Playwright 화면 브라우저) → 올리브영 경로로 호출했습니다. Cloudflare 보안 확인은 사람이 직접 통과했고 자동으로 우회하지 않았습니다.

PASS 공개 도구 10개 유지                      10
PASS 내부 이름 비노출
PASS service enum                             ["seveneleven","oliveyoung"]
PASS operation 필수 유지                      ["operation"]
PASS operation enum                           ["popwords","snapshot","today_deals"]
PASS 오특 조회 성공                           3532ms, relay 4회
PASS 한국 날짜                                2026-10-26
PASS 브랜드 특가 4개                          4
PASS 오늘의 특가 27개                         27
PASS 브랜드 첫 상품                           [10월올영픽] 어노브 실크 헤어 오일 에센스 70ml 더블 기획 (27,200원~)
PASS 오늘 첫/끝 상품                          달바 퍼스트 스프레이 세럼 ... 아이코닉 2027 다이어리
PASS 재조회는 릴레이 캐시 사용                추가 브라우저 요청 0회
PASS 내부 이름 호출 시 get_catalog 안내
PASS 잘못된 조합 거절                         oliveyoung/popwords
PASS service 생략 시 오특은 세븐일레븐 조합으로 거절
PASS service 생략 기존 호출은 세븐일레븐으로 동작   popwords
  • 같은 시각 브라우저 화면과 대조해 브랜드 특가 4개(어노브 4종)와 오늘의 특가 27개의 상품명, 순서, 할인가, 정가, 할인율, 혜택 표시가 모두 일치했습니다.
  • 중계 없이 직접 조회하면 원본이 HTTP403을 반환하고, 빈 목록이 아니라 올리브영 직접 요청 실패 오류로 반환되는 것을 확인했습니다.

참고 사항

  • 중계 재배포가 필요합니다. 운영 환경의 직접 조회는 기존 상품 검색과 마찬가지로 403이라, 브라우저 중계에 이 PR의 scripts/relay/* 변경이 반영되어야 실제로 동작합니다.
  • HTTP 중계 모드는 미지원입니다. Python HTTP 중계(OY_RELAY_TRANSPORT=http)는 기존 JSON 경로 4개만 허용하므로 오특 작업은 실패로 반환합니다. 2026-10-11 기록상 HTTP 모드도 403이라 이번 범위에서 제외했습니다.
  • 중계 호출 한도: 조회 1번에 페이지 1번과 목록 페이지 수만큼 요청합니다(2026-10-26 기준 4번). 같은 날짜 재조회는 중계 캐시(5분)를 사용합니다.
  • 내부 작업 수: oliveyoung_get_today_deals가 추가되어 내부 작업 매핑이 44개에서 45개가 됩니다. 공개 목록에는 노출하지 않고 get_catalog로만 안내합니다. 구형 별칭과 같은 방식으로 등록되어 [Deprecated] 설명과 replacement=get_catalog가 붙습니다. 다른 방식을 원하시면 맞추겠습니다.
  • get_catalog 스키마 변화: service enum(seveneleven, oliveyoung)이 선택 항목으로 추가되고 operation은 계속 필수입니다. service를 생략한 기존 호출은 동작이 같습니다. PlayMCP 등록 스키마는 갱신이 필요할 수 있습니다.
  • 원본 변경 반영: 작업 중 main의 fix: 올리브영 중계의 안전 복구와 의존성 보안 보완 #311~#330을 받아 BrowserRequestError 구조 위에서 오특 분기를 다시 적용했습니다. 오특 경로의 HTTP429도 기존 쿨다운 처리를 그대로 탑니다.
  • Windows 로컬 테스트: 전체 npm test에서 16개 파일이 실패합니다. 13개는 이 변경 없이 main에서도 같은 파일이 실패합니다(tests/relay/*의 macOS 운영용 테스트, 심볼릭 링크 권한 EPERM, cli-output-drain, health-runner, geocode-fallback). 나머지 3개(dtryx-start-cgv, oliveyoung-http-python, workers-chart-entrypoint)는 전체를 함께 돌릴 때 시간 초과로 실패하고 단독으로는 통과합니다. CI 결과로 확인 부탁드립니다.

get_catalog에 service=oliveyoung, operation=today_deals를 추가해
오특 화면의 오늘의 브랜드 특가와 오늘의 특가 전체 목록을 한국 날짜 기준으로 반환합니다.

- 오특은 JSON API 없이 HTML로만 제공되어 고정 경로·인수 검증과 카드 파서를 추가
- 브라우저 중계에 today-deals-page, today-deals-list 작업을 추가하고 정상 오특 HTML만 성공으로 감쌈
- get_catalog의 service 생략 호출은 기존처럼 세븐일레븐으로 유지 (공개 도구 10개 유지)
- 보안 확인 화면·형식 변경은 빈 목록이 아니라 오류로 반환
@chuuminggg
chuuminggg requested a review from hmmhmmhm as a code owner October 26, 2026 13:51
@hmmhmmhm
Copy link
Copy Markdown
Owner

감사합니다. 퇴근하면 보겠습니다...!

@chuuminggg
Copy link
Copy Markdown
Contributor Author

@hmmhmmhm 감사합니다! 확인 후, 부족하거나 필요하신 부분 있으시면 말씀 부탁드립니다!

오특 상품 카드의 일시품절 표시(status_flag soldout)를 soldOut 필드로 반환합니다.

- 2026-10-27 화면에 일시품절로 표시된 실제 카드를 fixture로 추가
- outputSchema, 안내 문구, 서비스 문서와 분석 문서에 soldOut 반영
@chuuminggg
Copy link
Copy Markdown
Contributor Author

실제 화면 대조 결과 (2일 연속)

bb8c11d 코드로 이틀 동안 실제 MCP 경로(/mcp → get_catalog → 로컬 브라우저 릴레이 → 올리브영)를 호출하고, 같은 시각 오특 화면에 보이는 목록과 비교했습니다. Cloudflare 확인은 사람이 직접 통과했습니다.

날짜 (KST) 브랜드 특가 오늘의 특가 화면 대조 첫 조회 재조회
2026-10-26 4개 (어노브) 27개 (10·10·7) 31/31 일치 3532ms, 릴레이 4회 캐시, 0회
2026-10-27 4개 (존슨즈·라보에이치) 27개 (10·10·7) 31/31 일치 922ms, 릴레이 4회 캐시, 0회
  • 비교 항목: 순서, 상품명, 할인가, ~(옵션별 최저가), 정가, 할인율, 혜택 표시, 오늘드림 여부
  • 두 날짜에 겹치는 상품번호는 0개입니다. 개수는 같지만 날짜가 바뀐 목록을 실제로 읽었습니다.
  • 반환 date는 매번 한국 날짜와 같았고 truncated=false였습니다.
  • 공개 도구 10개 유지, service 생략 시 세븐일레븐 동작, 잘못된 조합 거절 등 계약 확인 항목도 이틀 모두 통과했습니다.

새로 확인한 점

  • 일시품절 표시: 10-27 오늘의 특가 27번(A000000263126)이 화면에 "일시품절"로 보였습니다. 카드 HTML에는 <span class="status_flag soldout">일시품절</span>이 있습니다. 상품명과 다른 값은 정상으로 읽었고, 이 표시를 soldOut 필드로 반환하도록 000ac17에서 추가했습니다. 실제 카드를 fixture로 넣어 단위 테스트를 추가했고, 실제 MCP 경로에서도 27번 1개만 soldOut=true로 나왔습니다.
  • coming soon: 22시쯤 화면에 "잠시 후 12시 새로운 오특 오픈!" 문구와 다음 날 상품 29개가 보였습니다. 배너는 슬라이드용으로 앞쪽 상품이 반복되어 중복 제거가 필요합니다. 후속 제안 내용과 같습니다.

@hmmhmmhm
hmmhmmhm merged commit d5e153c into hmmhmmhm:main Nov 1, 2026
10 checks passed
@hmmhmmhm
hmmhmmhm commented Nov 1, 2026
Copy link
Copy Markdown
Owner

감사합니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0