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
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ZYTE_API_KEY=your_zyte_api_key_here
# Zyte 유료 호출은 중지되어 있으며 키를 설정해도 활성화되지 않습니다.
# ZYTE_API_KEY=legacy_key_unused
GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here
NAVER_CLIENT_ID=your_naver_client_id_here
NAVER_CLIENT_SECRET=your_naver_client_secret_here
Expand All @@ -8,3 +9,10 @@ HEALTH_CHECK_BASE_URL=http://localhost:8787
CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id_here
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token_here
CF_WORKER_SCRIPT_NAME=daiso-mcp

# 올리브영 무료 전용 브라우저 릴레이 (선택, 사용자 입력으로 전달하지 않음)
OY_RELAY_URL=
OY_RELAY_TOKEN=
# 로컬 릴레이 실행용 선택 설정
OY_RELAY_PORT=4319
OY_BROWSER_EXECUTABLE=
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm ci
cp .env.example .env
```

`.env`에는 필요한 경우 `ZYTE_API_KEY`를 설정하세요.
Zyte 유료 호출은 비용 정책에 따라 중지되어 있으며 `ZYTE_API_KEY`를 설정해도 활성화되지 않습니다. 무료 조회와 선택적 중계 구성은 [운영 정책](docs/zyte-free-access.md)을 참고하세요.

## 개발 명령어

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@

</div>

> Zyte 유료 호출을 중지했습니다. 무료 원본 조회를 우선하며 일부 서비스는 별도 실행 환경이 필요합니다. [비용 정책과 서비스별 상태](docs/zyte-free-access.md)를 확인하세요.

> [!IMPORTANT]
> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25·롯데마트의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.

Expand Down
52 changes: 52 additions & 0 deletions docs/oliveyoung-free-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 올리브영 무료 브라우저 릴레이

2026-09-15 검증: 이 Mac의 일반 HTTP 요청은 올리브영 검색 API에서 HTTP 403 보안 검증 HTML을 받았습니다. 개인 프로필과 분리한 **화면 있는 Chromium**에서는 네 API 모두 HTTP 200, `status: SUCCESS`였습니다. 새 headless 브라우저는 보안 검증 화면에 머물렀습니다. 따라서 GUI 로그인 세션, 브라우저, 전원과 네트워크를 유지할 수 있는 운영 호스트가 필요합니다. 설정만 추가했다고 복구된 것으로 판단하면 안 됩니다.

## 실행

저장소의 개발 의존성을 설치한 뒤 실행합니다. Playwright Chromium이 없다면 `npx playwright install chromium`으로 설치하거나, 이미 설치한 Chromium 실행 파일의 절대 경로를 `OY_BROWSER_EXECUTABLE`에 지정합니다.

```sh
# 토큰은 별도 비밀 저장소/환경에서 주입합니다. 로그나 저장소에 기록하지 않습니다.
export OY_RELAY_TOKEN='<전용 임의 토큰>'
# 선택 사항: 기본 포트 4319, Playwright 기본 Chromium 사용
export OY_RELAY_PORT=4319
# export OY_BROWSER_EXECUTABLE='/absolute/path/to/chromium'
npx tsx scripts/relay/start.ts
```

서버는 `127.0.0.1`에만 바인딩합니다. 홈페이지의 정상 `/store/` 페이지가 열린 뒤 준비 메시지를 출력합니다. 브라우저와 서버는 SIGINT/SIGTERM으로 종료합니다. 화면 없는 서버에서는 현재 검증 결과대로 작동하지 않을 수 있습니다. 세션이 보안 검증으로 바뀌면 조회는 실패하며, 운영자가 전용 브라우저 접속을 확인하고 릴레이를 재시작해야 합니다.

Cloudflare Worker에서 쓰려면 승인된 별도 연결 경로가 필요합니다. 이번 구현은 공개 터널, LaunchAgent 또는 상시 실행을 자동 생성하지 않습니다.

## Worker/MCP 설정

- `OY_RELAY_URL`: 운영자가 관리하는 릴레이의 기본 HTTPS 주소. 개발 시 `http://127.0.0.1:4319` 허용.
- `OY_RELAY_TOKEN`: 릴레이와 같은 전용 Bearer 토큰.

이 값은 Worker 바인딩에서 REST 핸들러와 MCP 서비스로 전달됩니다. MCP 도구 사용자 인자로 설정할 수 없습니다. 릴레이가 설정되어 있으면 먼저 릴레이를 호출하고, 없으면 공식 API에 직접 요청합니다. 직접 요청 실패 시 운영자에게 릴레이 설정 안내를 반환합니다. 어느 경로도 Zyte를 호출하지 않습니다.

## 허용 요청

`POST /v1/oliveyoung/{operation}`에 JSON 본문과 `Authorization: Bearer …`를 보냅니다. 대상 호스트는 `https://www.oliveyoung.co.kr`로 고정하며 임의 URL은 받지 않습니다.

| operation | JSON 필드 |
| --- | --- |
| `find-store` | `lat`, `lon`, `pageIdx`, `searchWords`, `pogKeys`, `serviceKeys`, `mapLat`, `mapLon` |
| `product-search-v3` | `includeSoldOut`, `keyword`, `page`, `sort`, `size` |
| `stock-goods-info-v3` | `goodsNo` |
| `stock-stores` | `productId`, `lat`, `lon`, `pageIdx`, `searchWords`, `mapLat`, `mapLon` |

숫자·문자열·불리언 타입과 필드 목록을 검증합니다. 요청 본문은 최대 16 KiB입니다. 인증 실패는 브라우저 작업 전에 401, 잘못된 경로는 404, 잘못된 본문은 400/413입니다. 한 브라우저 작업만 동시에 실행하며, 실행·대기를 합쳐 8개를 넘으면 503입니다. 취소되었거나 15초 이상 대기한 요청은 실행하지 않습니다. 브라우저 fetch는 본문을 읽을 때까지 15초 제한을 적용합니다. 정상 HTTP 200과 `SUCCESS` JSON만 반환하며, HTML·상태 오류·네트워크 실패는 원문이나 비밀을 노출하지 않는 502가 됩니다.

기존 상품·매장·재고의 실패 시 30분 캐시 재사용 동작은 이 전송 변경에서 확대하지 않았습니다. 캐시 데이터가 실시간 재고를 보장하지는 않습니다.

## 검증 기록

- 직접 Node POST: `find-store`, `product-search-v3` 모두 HTTP 403, 보안 검증 HTML.
- 새로 연 headed Chromium: 별도 로그인과 개인 쿠키 없이 네 API 성공.
- 구현한 서버를 임시 포트 `14319`에 띄워 로컬 HTTP로 검증: 잘못된 토큰 401; `find-store` 200/SUCCESS/9개, `product-search-v3` 200/SUCCESS/382개, `stock-goods-info-v3` 200/SUCCESS, `stock-stores` 200/SUCCESS/9개.
- 실제 smoke는 설치된 Chrome for Testing 148 실행 파일을 `OY_BROWSER_EXECUTABLE`로 지정했습니다. 종료 후 포트 리스너가 남지 않은 것을 확인했습니다.
- 테스트: `npx vitest run tests/relay tests/services/oliveyoung tests/api/oliveyoung-handlers.test.ts tests/app/app-api-oliveyoung.test.ts tests/app/app-api-actions.test.ts`.

공개 Worker에서 이 로컬 Mac으로 연결되는 경로는 이번 smoke로 검증한 범위에 포함되지 않습니다.
25 changes: 25 additions & 0 deletions docs/release-1.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 1.2.0 릴리스 노트

## Zyte 사용 중지

Zyte Extract 네트워크 호출과 유료 재시도 경로를 제거했습니다. 서버의 REST·MCP·운영 검사 모두 같은 정책을 따릅니다. 기존 ZYTE_API_KEY가 남아 있어도 유료 요청은 발생하지 않습니다. 비용 정책에 따른 조회 불가는 재시도 불가 진단을 반환합니다.

기존 무료 원본 요청과 롯데마트 소켓·세션 재시도는 유지합니다. 무료 경로가 차단된 서비스는 조회할 수 없을 수 있습니다. 계정의 구독·약정이나 다른 프로그램의 사용량은 별도이며 이 릴리스가 계정 계약을 변경하지는 않습니다.

## 올리브영 무료 전송

올리브영 클라이언트를 유료 전송에서 직접 HTTP 및 운영자가 구성하는 인증된 브라우저 중계로 전환했습니다. 중계 주소와 토큰은 운영 바인딩에서만 전달합니다. 잘못된 설정은 반복 재시도 대신 운영자 조치를 안내합니다.

선택적인 로컬 중계는 전용 headed Chromium을 사용하며 네 가지 고정 조회 경로만 제공합니다. 인증, 16KB 요청 제한, 필드 검증, 직렬 처리와 대기 상한을 적용합니다. 사용자 브라우저 프로필이나 로그인 정보를 가져오지 않습니다.

2026-09-15 로컬 실측에서 네 API 모두 정상 JSON을 반환했습니다. 일반 HTTP 및 headless 브라우저는 차단돼, 이 경로를 운영에 사용하려면 GUI 세션과 상시 실행 장비가 필요합니다. 중계 구현과 로컬 검증 완료는 공개 서버 복구 완료를 뜻하지 않습니다.

## 조회 재사용

세븐일레븐 상품·카탈로그·인기 검색어는 5분, 매장 검색은 30분 동안 서비스 계층에서 재사용합니다. REST와 MCP의 같은 조회를 합치고 결과 복제·실패 제거·메모리 상한을 적용했습니다. 실시간 재고는 이 새 캐시에 넣지 않습니다.

## 응답 검증

올리브영의 직접/중계 응답은 HTTP 200과 SUCCESS 본문을 확인합니다. 공용 HTTP 유틸리티는 선택적으로 기대 상태를 지정할 수 있으며 기존 요청의 기본 동작과 응답 본문 읽기 제한 시간은 유지합니다.

서비스별 검증 결과와 제약은 [Zyte 없는 운영 정책](zyte-free-access.md)을 참고하세요.
52 changes: 52 additions & 0 deletions docs/superpowers/plans/2026-09-15-zyte-reduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Zyte 비용 우선 절감 구현 계획

> For agentic workers: Use superpowers:subagent-driven-development for implementation and independent spec/quality review.

**Goal:** 운영 Zyte 호출을 0회로 만들고 무료 조회와 결과 재사용 범위를 확대합니다.

**Architecture:** 공용 Zyte 전송 함수에서 비용 정책을 강제합니다. 키 존재 여부나 REST/MCP/health 진입점에 의존하지 않습니다. 기존 API 계약을 유지하고 무료 원본 요청과 캐시를 보강합니다. 유료 재허용·호출 예산 모드·새 유료 서버 구입은 제외합니다.

**Tech Stack:** TypeScript, Vitest, Cloudflare Workers, 기존 HTTP 클라이언트와 캐시.

## Task 1: 유료 호출 0회

Files: src/utils/zyte.ts, src/core/errors.ts, src/api/configStatus.ts, tests/utils/zyte.test.ts, tests/core/errors.test.ts, 관련 config/app 테스트.

- [x] 실제 키와 process.env 키가 있어도 기본 requestByZyte는 정책 오류를 반환하고 fetch 호출은 0회인 실패 테스트를 작성합니다. 여러 동시 호출과 retries 옵션도 동일해야 합니다.
- [x] `await expect(requestByZyte({apiKey:'test',url:'https://example.com',retries:3})).rejects.toThrow('비용'); expect(fetchMock).not.toHaveBeenCalled()` 형태로 네트워크 차단을 확인합니다.
- [x] 공용 전송 경계에 정책 차단을 구현합니다. 필요 없는 유료 재시도 코드는 제거합니다. 명시적인 코드 변경 없이 운영 환경키만으로 다시 켜지지 않아야 합니다.
- [x] 정책 오류를 retryable:false로 진단하고 health 설정 화면은 키 보유와 유료 사용 중지를 구분합니다.
- [x] REST/MCP 및 health에서 키가 있어도 Zyte 요청 0회를 검증합니다. 기존 무료 요청이 계속 동작하는지도 확인합니다.
- [x] 표적 테스트, 독립 사양 리뷰, 품질 리뷰를 진행합니다. 공유 작업트리의 커밋은 통합 검증 후 부모가 수행합니다.

## Task 2: 직접 조회 조사와 적용

Files: src/services/oliveyoung/client.ts, 필요 시 src/services/oliveyoung/transport.ts, tests/services/oliveyoung/*, docs/zyte-free-access.md.

- [x] 실제 공식 올리브영 POST 요청의 로컬 응답과 Cloudflare 원격 응답을 비교합니다. Zyte는 호출하지 않습니다.
- [x] `fetch`가 직접 공식 URL을 호출하고 성공 JSON을 파싱하는 회귀 테스트를 먼저 추가합니다. 실패는 오류 또는 명시적인 기존 캐시 경로를 사용하며 빈 성공을 만들지 않습니다.
- [x] 직접 성공이 입증된 전송 경로를 반영합니다. 차단되는 경우 증거를 기록하고 동작하지 않는 대체 URL만 추가하지 않습니다.
- [x] CU·세븐일레븐·CGV·디트릭스 무료 경로와 로컬 실행 가능성을 조사합니다. 기존 개인 장비의 상시 가동 여부와 공개 중계 운영 가능성이 확인되지 않으면 새 인프라는 배포하지 않고 필요한 조건을 기록합니다.

## Task 3: 반복 조회 감소

Files: src/services/seveneleven/readCache.ts, src/services/seveneleven/client.ts, tests/services/seveneleven/readCache.test.ts 및 기존 호출부 테스트. 재고 필드가 없는 세븐일레븐 공개 목록에 먼저 적용합니다.

- [x] 캐시 적용 지점과 기존 TTL을 확인하고 REST와 MCP가 공유하는 서비스 계층에 적용합니다. 무관한 전역 캐시 재작성은 하지 않습니다.
- [x] 같은 키의 동시 요청 2개가 원본 호출 1회로 합쳐지는 테스트, 실패 후 다음 호출 재시도 가능 테스트, 서로 다른 키 분리 테스트를 작성합니다.
- [x] 세븐일레븐 상품·카탈로그·인기 검색어는 5분, 매장은 30분 재사용합니다. 이 새 캐시는 실시간 재고를 저장하지 않습니다. 올리브영의 기존 캐시 동작은 확대하지 않습니다.
- [x] 메모리 크기 제한과 실패 항목 제거를 검증하고 필요 시 결과 복제로 호출자 간 데이터 오염을 방지합니다.

## Task 4: 통합·배포

- [x] `npm run check`, `npm run test:coverage` 네 항목 100%, `npm audit`, `npm run build`, Wrangler dry-run을 실행합니다.
- [x] 전체 diff의 독립 사양/품질 리뷰를 완료했습니다.
- [ ] 문서에 사용 중지 정책, 무료 가능 범위와 미해결 조건을 기록하고 PR을 생성합니다. 검증 후 승인된 유지보수 범위로 병합·배포합니다.
- [ ] 실제 REST/MCP/health에서 유료 정책 오류와 정상 무료 조회를 검증합니다. 원본 장애와 비용 정책에 따른 조회 불가를 구분합니다.
- [ ] 패키지 변경을 버전/태그와 일치하도록 릴리스하고 공개 registry를 검증합니다. 개인 프로젝트 기록과 완료 알림을 갱신합니다.

## 운영 연결 상태

올리브영 로컬 브라우저 중계는 실제 네 API와 인증 거절을 검증하고 종료했습니다. 현재 Mac을 상시 GUI 중계 장비로 사용할지 사용자 응답을 기다리고 있어 공개 터널·LaunchAgent는 생성하지 않았습니다. 기존 개인 Cloudflare 계정에는 활성 터널이 없습니다. 이는 유료 호출 0회 정책의 배포를 막지 않습니다.

최종 로컬 검증: 165개 파일·1,874개 테스트 통과. Statements 4,510/4,510, Branches 4,017/4,017, Functions 1,057/1,057, Lines 4,364/4,364로 모두 100%입니다. 릴레이 핵심 두 모듈을 커버리지 대상에 포함했고 시작 스크립트는 실제 로컬 실행으로 검증했습니다. npm audit 0건, build와 Worker dry-run 통과입니다.
50 changes: 50 additions & 0 deletions docs/superpowers/specs/2026-09-15-zyte-reduction-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Zyte 비용 절감 설계 초안

## 목표와 현재 근거

사용자는 Zyte 비용이 과도해 사용을 최대한 줄이기를 원합니다. 월별 실제 청구액과 서비스별 유료 성공 건수는 아직 조회하지 못했습니다. 비용 절감률은 추정하지 않습니다.

- 올리브영은 src/services/oliveyoung/client.ts의 zyteExtract가 직접 Zyte를 호출합니다.
- CU·세븐일레븐 등은 이미 직접 요청 후 특정 실패에만 Zyte를 사용합니다. 요청 순서 변경만으로 모든 비용을 없앨 수 없습니다.
- 기존 dailyRateLimit은 일부 공개 GET API의 클라이언트별 제한입니다. 세븐일레븐·MCP·인증된 health 경로 전체를 포괄하는 Zyte 지출 상한이 아닙니다.
- 공용 Zyte 함수에 전역 호출 예산 검사가 없습니다. 일부 호출은 기본 1회 재시도가 가능합니다.
- 올리브영에는 프로세스 메모리 기반 장애 시 이전 결과 캐시가 있습니다. API에는 Edge Cache도 있으므로 캐시를 처음 추가하는 작업으로 설명해서는 안 됩니다.
- 이번 유지보수에서 CU·디트릭스는 로컬 정상/Cloudflare 실패가 재현됐습니다. 별도 실행 위치를 검증할 근거가 있으나 다른 서버에서의 성공을 보장하지는 않습니다.

## 선택지

1. 비용 우선: Zyte 기본 차단, 직접 요청과 유효한 캐시로 서비스 가능한 범위를 유지합니다. 일부 조회 불가를 허용하며 무료 대체 경로를 순서대로 검증합니다.
2. 균형: 작은 전역/서비스별 호출 예산 내에서만 유료 fallback을 허용합니다. 한도 도달 시 직접 요청·캐시·명시적 조회 불가로 처리합니다.
3. 가용성 우선: Zyte를 유지하고 공유 캐시와 중복 요청 합치기부터 적용합니다. 절감은 가능하나 강한 비용 상한 없이는 사용량 증가 위험이 남습니다.

권장 방향은 1번 또는 2번과 직접 조회 대체를 병행하는 것입니다. 사용자가 “네 다 진행합시다”로 승인했으며 권장안인 비용 우선·Zyte 기본 차단을 구현합니다. 0회를 초과하는 유료 호출 허용 모드는 이번 범위에서 제외합니다.

## 구체 설계

### 1단계: 유료 호출 통제

requestByZyte의 실제 네트워크 호출 직전에 중앙 정책 검사를 둡니다. REST·MCP·health·재시도 모두 같은 정책을 거치며, 꺼진 상태에서는 Zyte 네트워크 요청이 0회여야 합니다. 환경변수에 키가 남아 있어도 이 정책을 우회할 수 없어야 합니다.

선택한 비용 우선 모드에서는 전송 함수가 항상 정책 오류를 반환합니다. 유료 재시도·인증·네트워크 경로를 제거하므로 원격 카운터 장애나 동시성으로 한도를 초과할 여지가 없습니다. 별도 호출 허용 모드와 예산 카운터는 구현하지 않습니다. health도 같은 정책을 따르며 서비스 실패를 정상으로 표시하지 않습니다.

### 2단계: 무료 직접 조회 확대

올리브영의 직접 요청 가능성을 먼저 검증하고 성공한 요청만 직접 경로로 전환합니다. CU·세븐일레븐·CGV 등은 현재 직접 경로와 공식 웹/앱 경로를 재현해 비교합니다. Workers에서 실패하고 사용자의 컴퓨터에서 성공하는 요청은 기존 상시 실행 가능한 장비에서 제한된 중계 서버를 시험하는 후보입니다. 새 유료 서버 계약은 이 설계에 포함하지 않습니다.

중계가 필요하면 고정된 서비스/경로만 허용하고 인증·요청 제한을 적용합니다. 임의 URL을 받아 대신 접속하는 공개 프록시로 만들지 않습니다. 운영 장비의 상시 가동 가능 여부와 외부 접근 구성을 확인한 후 구현 범위를 결정합니다.

### 3단계: 반복 조회 감소

매장·상품 목록 등 상대적으로 덜 변하는 결과의 보관 시간을 늘리고 REST/MCP가 같은 조회 결과를 공유하도록 적용 지점을 검토합니다. 동시에 들어온 같은 조회는 합칩니다. 재고·좌석은 짧은 유효 시간을 적용하고 지난 결과 제공 시 조회 시각과 오래된 상태를 표시합니다. 최신 재고로 오인되는 성공 응답을 만들지 않습니다.

## 검증

Zyte 차단 시 실제 요청 0회, REST/MCP/health 정책 일치, 병렬 호출·재시도에서도 유료 요청 0회, 실시간 재고의 신규 캐시 제외, 직접 조회 성공/실패 구분을 회귀 테스트로 확인합니다. 기존 check·coverage 100%·audit·배포 검사를 유지합니다. 운영 성공률과 Zyte 호출 수를 함께 비교합니다.

## 미결정

- 비용 우선으로 서비스 일부 저하를 허용하는 권장안을 승인받았습니다.
- 이번 유료 호출 상한은 0회이며, 유료 재허용은 후속 명시 설정 없이 발생하지 않습니다.
- 중계 서버는 후보이며 가동할 장비와 성공 가능성을 먼저 검증해야 합니다.

승인된 설계에 따라 별도 작업트리에서 구현·검증·배포합니다.
Loading