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
121 changes: 121 additions & 0 deletions docs/superpowers/plans/2026-07-28-cu-app-user-agent-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CU App User-Agent Recovery Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** PocketCU 공식 앱 User-Agent 표식을 사용해 CU 재고 직접 조회를 복구하고 불필요한 Zyte 폴백을 중단한다.

**Architecture:** 기존 `requestCuJson`의 direct-first 구조와 degraded 오류 계약은 유지한다. CU JSON 요청 공통 헤더에만 공식 앱 표식이 포함된 Android WebView User-Agent를 추가해 CU upstream 식별 조건을 충족한다.

**Tech Stack:** TypeScript, Vitest, Hono, Cloudflare Workers, GitHub Actions

---

### Task 1: CU 공식 앱 User-Agent 회귀 테스트

**Files:**

- Modify: `tests/services/cu/client.test.ts`

- [ ] **Step 1: 실패하는 테스트 작성**

`primeCuStockDisplay` describe에 다음 테스트를 추가한다.

```ts
it('공식 앱 식별자가 포함된 User-Agent를 전송한다', async () => {
mockFetch.mockResolvedValue(new Response(JSON.stringify({ areaList: [] })));

await primeCuStockDisplay();

const options = mockFetch.mock.calls[0][1] as RequestInit;
expect(new Headers(options.headers).get('user-agent')).toMatch(/;BGFCU$/);
});
```

- [ ] **Step 2: 단일 테스트 파일로 RED 확인**

Run:

```bash
npx vitest run tests/services/cu/client.test.ts --maxWorkers=1 --no-file-parallelism
```

Expected: 새 테스트가 현재 User-Agent 부재로 실패한다.

### Task 2: 최소 구현과 순차 검증

**Files:**

- Modify: `src/services/cu/client.ts`
- Test: `tests/services/cu/client.test.ts`

- [ ] **Step 1: CU 공통 헤더에 앱 User-Agent 추가**

`CU_DEFAULT_HEADERS`에 다음 값을 추가한다.

```ts
'User-Agent':
'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.0.0 Mobile Safari/537.36;BGFCU',
```

- [ ] **Step 2: 단일 테스트 파일로 GREEN 확인**

Run:

```bash
npx vitest run tests/services/cu/client.test.ts --maxWorkers=1 --no-file-parallelism
```

Expected: PASS.

- [ ] **Step 3: 품질 검사를 한 번에 하나씩 실행**

Run in order:

```bash
npm run format:check
npm run lint
npm run lint:biome
npm run typecheck
npm run check:source-lines
npm test -- --maxWorkers=1 --no-file-parallelism
npm run build
```

Expected: 모든 명령이 종료 코드 0이고 경고·실패가 없다.

- [ ] **Step 4: 구현 커밋**

```bash
git add src/services/cu/client.ts tests/services/cu/client.test.ts docs/superpowers/plans/2026-07-28-cu-app-user-agent-recovery.md
git commit -m "fix: CU 공식 앱 요청으로 재고 조회 복구"
```

### Task 3: 배포와 운영 확인

**Files:**

- No source file changes

- [ ] **Step 1: 현재 브랜치를 push**

```bash
git push origin main
```

- [ ] **Step 2: GitHub Actions를 순차 확인**

`gh run list`와 `gh run watch`로 push가 시작한 CI 및 Cloudflare 배포가 성공할 때까지 확인한다.

- [ ] **Step 3: 운영 CU 재고 확인**

Run:

```bash
curl -fsS 'https://mcp.aka.page/api/cu/inventory?keyword=%EC%82%BC%EA%B0%81%EA%B9%80%EB%B0%A5&storeCheck=false&size=3'
```

Expected: HTTP 200이며 `inventory.available=true`, `inventory.totalCount>0`, 상품 배열이 비어 있지 않다.

- [ ] **Step 4: 운영 헬스체크 확인**

운영 헬스체크에서 `cu.inventory` 상태가 `ok`이고 CU 재고 관련 degraded/failure가 없는지 확인한다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CU 공식 앱 User-Agent 기반 재고 조회 복구 설계

## 배경

CU 재고 API 직접 호출과 Zyte 폴백이 각각 `403 Request Blocked`, `520 Website Ban`으로 실패한다. 사용자 소유 Android 단말의 PocketCU 5.3.6 앱과 공개 웹 자산을 확인한 결과, 공식 앱은 WebView 기본 User-Agent 끝에 `;BGFCU`를 붙인다.

동일 요청을 비교한 운영 재현 결과는 다음과 같다.

- Android User-Agent만 사용: HTTP 403
- 공식 앱 표식 `;BGFCU`를 추가: HTTP 200, `resp_cd=0000`
- 세션 쿠키, DeviceId, 로그인 토큰: 성공에 필요하지 않음

따라서 결제나 Zyte 키 문제가 아니라, CU가 공식 앱 요청을 식별하는 User-Agent 규칙을 기존 클라이언트가 반영하지 못한 것이 원인이다.

## 접근 방식 비교

1. **공식 앱 User-Agent 표식만 추가 — 채택**
- 최소 변경이며 직접 호출을 복구한다.
- 단말·계정 식별값을 다루지 않는다.
- 직접 호출 성공 시 Zyte 사용량이 발생하지 않는다.
2. 재고 화면을 먼저 열어 세션 쿠키를 생성
- 추가 요청과 쿠키 관리가 필요하지만 실측상 성공 조건이 아니다.
3. Zyte 브라우저 호출로 강제 전환
- 비용과 지연이 증가하고 현재 Website Ban도 해결하지 못한다.

## 구현

`src/services/cu/client.ts`의 CU JSON 요청 공통 헤더에 일반 Android WebView 형식의 User-Agent와 공식 앱 표식 `;BGFCU`를 추가한다. 특정 단말 ID, 쿠키, Authorization은 추가하지 않는다. 기존 direct-first 및 Zyte fallback 흐름은 변경하지 않는다.

## 오류 처리

직접 호출이 향후 다시 400/403/429로 차단될 때만 기존 Zyte 폴백을 사용한다. upstream 차단이 계속되면 현재의 degraded 응답 계약을 그대로 유지한다.

## 테스트와 완료 조건

1. 회귀 테스트는 CU 재고 직접 요청의 User-Agent가 `;BGFCU`로 끝나는지 검증한다.
2. 해당 테스트가 구현 전 실패하고 구현 후 통과해야 한다.
3. CU 클라이언트 테스트, 전체 테스트, lint, 타입 검사, 빌드를 한 번에 하나씩 실행한다.
4. 배포 후 운영 `/api/cu/inventory`가 `available: true`, 상품 결과를 반환하는지 확인한다.
5. 운영 헬스체크에서 `cu.inventory`가 더 이상 degraded가 아닌지 확인한다.

## 범위 제외

- PocketCU 로그인 토큰 또는 사용자 단말 정보 수집
- CU 외 서비스의 User-Agent 변경
- 불필요한 Zyte 브라우저 자동화
2 changes: 2 additions & 0 deletions src/services/cu/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const CU_DEFAULT_HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent':
'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.0.0 Mobile Safari/537.36;BGFCU',
} as const;

const CU_WEB_DEFAULT_HEADERS = {
Expand Down
10 changes: 10 additions & 0 deletions tests/services/cu/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,16 @@ describe('primeCuStockDisplay', () => {
expect.objectContaining({ method: 'POST' }),
);
});

it('공식 앱 식별자가 포함된 User-Agent를 전송한다', async () => {
mockFetch.mockResolvedValue(new Response(JSON.stringify({ areaList: [] })));

await primeCuStockDisplay();

const options = mockFetch.mock.calls[0][1] as RequestInit;
const userAgent = new Headers(options.headers).get('user-agent') || '';
expect(userAgent).toMatch(/;BGFCU$/);
});
});

describe('fetchCuStock', () => {
Expand Down