내 주변 다이소/올리브영 매장을 찾고, 재고를 확인하는 기능을 AI에게 부여합니다. (+메가박스, CGV, 롯데시네마, CU편의점, 이마트24 편의점)

235
TypeScript View on GitHub
AGENTS.md AGENTS.md 17,534 bytes

에이전트 개발 규칙

이 문서는 AI 에이전트가 이 프로젝트에서 코드를 작성하고 기여할 때 따라야 하는 규칙과 가이드라인을 정의합니다.

목차


코드 작성 규칙

파일 크기 제한

모든 코드 파일은 450줄 내외로 작성되어야 합니다.

  • 최대 줄 수: 450줄
  • 권장 줄 수: 300-400줄
  • 초과 시 조치: 파일이 450줄을 초과하면 기능별로 분리하여 모듈화
  • 예외: 자동 생성 파일(예: OpenAPI 생성 산출물)은 예외로 둘 수 있음

파일 분리 예시

// ❌ 나쁜 예: 하나의 파일에 모든 기능 (600줄)
// src/products.ts (600줄)

// ✅ 좋은 예: 기능별로 분리
// src/products/search.ts (200줄)
// src/products/filter.ts (150줄)
// src/products/formatter.ts (100줄)
// src/products/index.ts (50줄)

코드 품질

  • 명확성: 코드는 명확하고 이해하기 쉽게 작성
  • 재사용성: 중복 코드를 최소화하고 공통 로직은 함수로 추출
  • 타입 안정성: TypeScript의 타입 시스템을 적극 활용
  • 에러 핸들링: 모든 비동기 작업과 외부 API 호출에 적절한 에러 처리 구현

예시

// ✅ 좋은 예
async function fetchData(url: string): Promise<ApiResponse> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('데이터 가져오기 실패:', error);
    throw error;
  }
}

// ❌ 나쁜 예
async function fetchData(url) {
  const response = await fetch(url);
  return await response.json();
}

커밋 규칙

커밋 빈도

  • 주기적인 커밋: 논리적인 작업 단위마다 커밋
  • 작은 단위: 한 번에 하나의 기능이나 수정사항만 포함
  • 완성된 코드: 빌드 실패나 런타임 에러가 없는 상태에서만 커밋

커밋 메시지 컨벤션

접두사는 영어, 메시지는 한국어를 사용합니다.

커밋 메시지 형식

<타입>: <제목>

<본문> (선택사항)

<푸터> (선택사항)

타입 종류

  • feat: - 새로운 기능 추가
  • fix: - 버그 수정
  • docs: - 문서 수정
  • style: - 코드 포맷팅, 세미콜론 누락 등 (로직 변경 없음)
  • refactor: - 코드 리팩토링 (기능 변경 없음)
  • test: - 테스트 코드 추가/수정
  • chore: - 빌드 설정, 패키지 매니저 설정 등
  • perf: - 성능 개선
  • ci: - CI/CD 설정 변경
  • revert: - 커밋 되돌리기

커밋 메시지 예시

# ✅ 좋은 예
feat: 제품 검색 필터링 기능 추가
fix: 매장 찾기 시 거리 계산 오류 수정
docs: API 사용 예시 문서 업데이트
refactor: 재고 확인 로직을 별도 모듈로 분리
test: 가격 정보 조회 API 테스트 추가
chore: TypeScript 버전 5.7.2로 업데이트

# ❌ 나쁜 예
feat: add feature (영어로만 작성)
update (타입 접두사 누락)
fix: bug fix (구체적이지 않음)
여러 기능 추가 (타입 접두사 누락)

제목 작성 규칙

  • 명확하고 구체적으로: 무엇을 변경했는지 명확히 기술
  • 50자 이내: 제목은 간결하게 작성
  • 명령형: "~했음" 대신 "~함" 또는 "~추가" 형태 사용
  • 마침표 없음: 제목 끝에 마침표를 붙이지 않음

본문 작성 규칙 (선택사항)

feat: 제품 카테고리별 필터링 기능 추가

사용자가 제품을 카테고리별로 필터링할 수 있는 기능을 추가했습니다.
지원 카테고리:
- 주방/생활
- 문구
- 완구/취미
- 화장품/미용

관련 이슈: #123

커밋 전 체크리스트

커밋하기 전에 다음 사항을 반드시 확인하세요:

  • 코드가 빌드되는가?
  • TypeScript 타입 에러가 없는가?
  • 개인정보나 민감한 정보가 포함되어 있지 않은가?
  • API 키, 비밀번호, 토큰 등이 포함되어 있지 않은가?
  • 테스트가 통과하는가? (테스트가 있는 경우)
  • npm run test:coverage가 100% (Statements/Branches/Functions/Lines)를 만족하는가?
  • 주석이 한국어로 작성되어 있는가?
  • 파일이 450줄을 초과하지 않는가?

보안 및 개인정보

절대 커밋하지 말아야 할 항목

다음 항목들은 절대 Git 저장소에 커밋되어서는 안 됩니다:

1. 인증 정보

// ❌ 절대 금지
const API_KEY = 'sk-1234567890abcdef';
const PASSWORD = 'mypassword123';
const DATABASE_URL = 'postgresql://user:pass@localhost/db';
// ✅ 올바른 방법: 환경 변수 사용
const API_KEY = process.env.API_KEY;
const PASSWORD = process.env.PASSWORD;
const DATABASE_URL = process.env.DATABASE_URL;

2. 개인정보

  • 실제 이메일 주소
  • 전화번호
  • 주소
  • 신용카드 정보
  • 주민등록번호
  • 기타 식별 가능한 개인정보

3. 민감한 설정 파일

# ❌ 커밋 금지
.env
.env.local
.env.production
.env.development
secrets.json
credentials.json
service-account-key.json

# ✅ .gitignore에 추가되어 있는지 확인

4. 프라이빗 키 및 인증서

  • SSH private keys (id_rsa, id_ed25519 등)
  • SSL/TLS 인증서의 private key
  • JWT secret keys
  • OAuth client secrets

환경 변수 사용

민감한 정보는 반드시 환경 변수로 관리하세요.

.env.example 파일 제공

# .env.example (커밋 가능)
API_KEY=your_api_key_here
DATABASE_URL=your_database_url_here
SECRET_KEY=your_secret_key_here
# .env (커밋 금지, .gitignore에 포함)
API_KEY=sk-real-api-key-1234567890
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=super-secret-key-xyz

Cloudflare Workers Secrets

Cloudflare Workers 환경 변수는 다음과 같이 관리:

# 로컬 개발용 (.dev.vars 파일 - .gitignore에 포함)
API_KEY=test-key-for-local-dev

# 프로덕션 배포용 (wrangler를 통해 설정)
npx wrangler secret put API_KEY

보안 체크리스트

커밋 전 다음 사항을 확인하세요:

  • .env 파일이 .gitignore에 포함되어 있는가?
  • 하드코딩된 API 키나 비밀번호가 없는가?
  • console.log()에 민감한 정보가 출력되지 않는가?
  • 주석에 실제 계정 정보가 포함되어 있지 않는가?
  • 테스트 데이터가 실제 개인정보를 포함하지 않는가?

파일 구조

플러그인 기반 아키텍처

이 프로젝트는 확장 가능한 플러그인 아키텍처로 설계되었습니다. 다이소 외에 편의점, 백화점, 영화관 등 다양한 서비스를 쉽게 추가할 수 있습니다.

핵심 설계 원칙

  1. ServiceProvider 인터페이스: 모든 서비스가 구현해야 하는 표준 계약
  2. ServiceRegistry: 서비스를 동적으로 등록하고 MCP 서버에 연결
  3. 도구 이름 네임스페이스: 서비스별 접두사 사용 (예: daiso_, cu_, cgv_)

디렉토리 구조

daiso-mcp/
├── src/
│   ├── index.ts              # MCP 서버 진입점 (450줄 이하)
│   ├── core/                 # 핵심 모듈 (확장 기반)
│   │   ├── types.ts          # 공통 타입 정의
│   │   ├── interfaces.ts     # ServiceProvider 인터페이스
│   │   └── registry.ts       # ServiceRegistry 클래스
│   ├── services/             # 서비스 프로바이더
│   │   └── daiso/            # 다이소 서비스
│   │       ├── index.ts      # 서비스 팩토리
│   │       ├── types.ts      # 다이소 전용 타입
│   │       ├── api.ts        # API 엔드포인트 중앙화
│   │       └── tools/        # 도구 구현
│   │           ├── searchProducts.ts
│   │           ├── findStores.ts
│   │           ├── checkInventory.ts
│   │           └── getPriceInfo.ts
│   └── utils/                # 유틸리티 함수
├── docs/                     # 네트워크 파싱/리서치 문서
├── examples/                 # 사용 예시
├── tests/                    # 테스트 파일
└── scripts/                  # 빌드/배포 스크립트

### 네트워크 파싱 기록

`docs/` 폴더에는 API 엔드포인트 분석을 위한 네트워크 파싱 기록이 저장됩니다.
새로운 서비스를 추가할 때 네트워크 분석 결과를 이 폴더에 `{서비스명}-` 접두사를 붙여 저장하세요.

docs/
├── daiso-network-analysis-result.md # 다이소 API 분석 결과
├── daiso-playwright-network-analysis.md # Playwright 네트워크 분석
├── daiso-replay-session-test.html # 세션 테스트 HTML
└── daiso-test-replay.js # 테스트 리플레이 스크립트


### 문서 기록

`docs/` 폴더에는 네트워크 파싱 기록과 API 리서치 문서를 함께 저장합니다.
새로운 서비스를 추가할 때 관련 문서를 이 폴더에 `{서비스명}-` 접두사로 저장하세요.

docs/
├── daiso-network-analysis-result.md # 다이소 API 분석 결과
├── daiso-playwright-network-analysis.md # Playwright 네트워크 분석
├── daiso-replay-session-test.html # 세션 테스트 HTML
└── daiso-test-replay.js # 테스트 리플레이 스크립트


### 새 서비스 추가 방법

새로운 서비스(예: CU 편의점)를 추가하려면:

#### 1. 서비스 디렉토리 생성

src/services/cu/
├── index.ts # CuService 클래스 및 팩토리
├── types.ts # CU 전용 타입 정의
├── api.ts # CU API 엔드포인트
└── tools/ # CU 도구 구현
├── searchProducts.ts
└── findStores.ts


#### 2. ServiceProvider 구현

```typescript
// src/services/cu/index.ts
import type { ServiceProvider } from '../../core/interfaces.js';
import type { ToolRegistration } from '../../core/types.js';

class CuService implements ServiceProvider {
  readonly metadata = {
    id: 'cu',
    name: 'CU 편의점',
    version: '1.0.0',
    description: 'CU 편의점 제품 검색 및 매장 찾기',
  };

  getTools(): ToolRegistration[] {
    return [
      // cu_search_products, cu_find_stores 등
    ];
  }
}

export function createCuService(): ServiceProvider {
  return new CuService();
}

3. 레지스트리에 등록

// src/index.ts
import { createDaisoService } from './services/daiso/index.js';
import { createCuService } from './services/cu/index.js';

registry.registerAll([createDaisoService, createCuService]);

도구 이름 규칙

서비스별 도구는 접두사로 구분합니다:

서비스 접두사 예시
다이소 daiso_ daiso_search_products
CU cu_ cu_search_products
CGV cgv_ cgv_search_movies

파일 명명 규칙

  • camelCase: 함수, 변수명
  • PascalCase: 클래스, 인터페이스, 타입명
  • kebab-case: 파일명 (선택사항)
  • SCREAMING_SNAKE_CASE: 상수
// 함수, 변수
const userName = 'John';
function getUserData() {}

// 클래스, 인터페이스, 타입
class UserService {}
interface UserData {}
type UserId = string;

// 상수
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';

코드 스타일

주석 작성

모든 주석은 한국어로 작성합니다.

파일 헤더 주석

/**
 * 제품 검색 도구
 *
 * 다이소 제품을 검색하고 필터링하는 기능을 제공합니다.
 * 검색 조건: 키워드, 카테고리, 가격 범위
 *
 * @module tools/searchProducts
 */

함수 주석

/**
 * 제품을 검색하고 필터링된 결과를 반환합니다.
 *
 * @param query - 검색 키워드
 * @param category - 제품 카테고리 (선택사항)
 * @param maxPrice - 최대 가격 (선택사항)
 * @returns 검색 결과 객체
 * @throws {Error} API 호출 실패 시
 */
async function searchProducts(
  query: string,
  category?: string,
  maxPrice?: number,
): Promise<SearchResult> {
  // 구현
}

인라인 주석

// 사용자 입력 검증
if (!query || query.trim().length === 0) {
  throw new Error('검색어를 입력해주세요');
}

// TODO: 캐싱 기능 추가 필요
const results = await fetchFromAPI(query);

// FIXME: 대소문자 구분 없이 검색하도록 수정 필요
const filtered = results.filter((item) => item.name.includes(query));

TypeScript 타입 정의

// 인터페이스 정의
interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  description?: string; // 선택적 속성
  inStock: boolean;
}

// 타입 별칭
type ProductId = string;
type ProductCategory = '주방/생활' | '문구' | '완구/취미' | '화장품/미용';

// 유니온 타입
type SearchFilter = {
  query: string;
  category?: ProductCategory;
  maxPrice?: number;
};

코드 포맷팅

  • 들여쓰기: 2 spaces
  • 따옴표: single quotes (') 우선
  • 세미콜론: 사용
  • 줄 길이: 최대 100자 권장
// ✅ 좋은 예
const message = '안녕하세요';
const user = {
  name: '홍길동',
  age: 30,
};

// ❌ 나쁜 예
const message = '안녕하세요';
const user = { name: '홍길동', age: 30 };

문서화

README.md

프로젝트의 메인 문서로 다음 내용을 포함:

  • 프로젝트 개요
  • 설치 방법
  • 사용 방법
  • API 문서
  • 배포 가이드

API 문서

각 도구의 입력/출력 스키마를 명확히 문서화:

/**
 * 제품 검색 API
 *
 * 요청 예시:
 * ```json
 * {
 *   "name": "search_products",
 *   "arguments": {
 *     "query": "수납박스",
 *     "category": "주방/생활",
 *     "maxPrice": 5000
 *   }
 * }
 * ```
 *
 * 응답 예시:
 * ```json
 * {
 *   "content": [{
 *     "type": "text",
 *     "text": "{ ... }"
 *   }]
 * }
 * ```
 */

CHANGELOG.md (권장)

주요 변경사항을 기록:

# Changelog

## [1.1.0] - 2025-03-01

### Added

- 제품 카테고리별 필터링 기능 추가
- 가격 히스토리 조회 기능 추가

### Fixed

- 매장 찾기 시 거리 계산 오류 수정

### Changed

- API 응답 형식 개선

테스트

테스트 작성 원칙

  • 모든 도구는 테스트를 작성해야 합니다
  • 엣지 케이스를 고려한 테스트 작성
  • 에러 처리 로직도 테스트
  • 코드 커버리지 100% 유지: 커밋 전 npm run test:coverage를 실행해 100%를 유지해야 합니다

테스트 파일 명명

src/tools/searchProducts.ts
tests/tools/searchProducts.test.ts

테스트 예시

import { searchProducts } from '../src/tools/searchProducts';

describe('제품 검색', () => {
  test('키워드로 제품을 검색할 수 있다', async () => {
    const result = await searchProducts({ query: '수납박스' });
    expect(result.content).toBeDefined();
    expect(result.content[0].type).toBe('text');
  });

  test('존재하지 않는 제품은 빈 결과를 반환한다', async () => {
    const result = await searchProducts({ query: '존재하지않는제품xyz' });
    const data = JSON.parse(result.content[0].text);
    expect(data.count).toBe(0);
  });

  test('빈 검색어는 에러를 발생시킨다', async () => {
    await expect(searchProducts({ query: '' })).rejects.toThrow();
  });
});

체크리스트 요약

코드 작성 전

  • 어떤 기능을 구현할지 명확히 이해했는가?
  • 파일이 450줄을 초과하지 않도록 설계했는가?
  • 적절한 파일 구조와 모듈 분리를 계획했는가?

코드 작성 중

  • TypeScript 타입을 명확히 정의했는가?
  • 주석을 한국어로 작성했는가?
  • 에러 핸들링을 적절히 구현했는가?
  • 보안에 취약한 코드가 없는가?

커밋 전

  • 빌드가 성공하는가?
  • 타입 에러가 없는가?
  • 개인정보나 민감한 정보가 포함되지 않았는가?
  • 커밋 메시지가 컨벤션을 따르는가?
  • 파일이 450줄 이하인가?
  • npm run test:coverage 결과가 100% (Statements/Branches/Functions/Lines)인가?

커밋 메시지 작성 시

# 형식 확인
<타입>: <한국어 제목>

# 예시
feat: 제품 검색 필터링 기능 추가
fix: 매장 거리 계산 오류 수정
docs: API 사용 가이드 업데이트

참고 자료


이 규칙을 따라 일관되고 안전하며 유지보수 가능한 코드를 작성해주세요.

CLAUDE.md CLAUDE.md 17,534 bytes

에이전트 개발 규칙

이 문서는 AI 에이전트가 이 프로젝트에서 코드를 작성하고 기여할 때 따라야 하는 규칙과 가이드라인을 정의합니다.

목차


코드 작성 규칙

파일 크기 제한

모든 코드 파일은 450줄 내외로 작성되어야 합니다.

  • 최대 줄 수: 450줄
  • 권장 줄 수: 300-400줄
  • 초과 시 조치: 파일이 450줄을 초과하면 기능별로 분리하여 모듈화
  • 예외: 자동 생성 파일(예: OpenAPI 생성 산출물)은 예외로 둘 수 있음

파일 분리 예시

// ❌ 나쁜 예: 하나의 파일에 모든 기능 (600줄)
// src/products.ts (600줄)

// ✅ 좋은 예: 기능별로 분리
// src/products/search.ts (200줄)
// src/products/filter.ts (150줄)
// src/products/formatter.ts (100줄)
// src/products/index.ts (50줄)

코드 품질

  • 명확성: 코드는 명확하고 이해하기 쉽게 작성
  • 재사용성: 중복 코드를 최소화하고 공통 로직은 함수로 추출
  • 타입 안정성: TypeScript의 타입 시스템을 적극 활용
  • 에러 핸들링: 모든 비동기 작업과 외부 API 호출에 적절한 에러 처리 구현

예시

// ✅ 좋은 예
async function fetchData(url: string): Promise<ApiResponse> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP 에러: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('데이터 가져오기 실패:', error);
    throw error;
  }
}

// ❌ 나쁜 예
async function fetchData(url) {
  const response = await fetch(url);
  return await response.json();
}

커밋 규칙

커밋 빈도

  • 주기적인 커밋: 논리적인 작업 단위마다 커밋
  • 작은 단위: 한 번에 하나의 기능이나 수정사항만 포함
  • 완성된 코드: 빌드 실패나 런타임 에러가 없는 상태에서만 커밋

커밋 메시지 컨벤션

접두사는 영어, 메시지는 한국어를 사용합니다.

커밋 메시지 형식

<타입>: <제목>

<본문> (선택사항)

<푸터> (선택사항)

타입 종류

  • feat: - 새로운 기능 추가
  • fix: - 버그 수정
  • docs: - 문서 수정
  • style: - 코드 포맷팅, 세미콜론 누락 등 (로직 변경 없음)
  • refactor: - 코드 리팩토링 (기능 변경 없음)
  • test: - 테스트 코드 추가/수정
  • chore: - 빌드 설정, 패키지 매니저 설정 등
  • perf: - 성능 개선
  • ci: - CI/CD 설정 변경
  • revert: - 커밋 되돌리기

커밋 메시지 예시

# ✅ 좋은 예
feat: 제품 검색 필터링 기능 추가
fix: 매장 찾기 시 거리 계산 오류 수정
docs: API 사용 예시 문서 업데이트
refactor: 재고 확인 로직을 별도 모듈로 분리
test: 가격 정보 조회 API 테스트 추가
chore: TypeScript 버전 5.7.2로 업데이트

# ❌ 나쁜 예
feat: add feature (영어로만 작성)
update (타입 접두사 누락)
fix: bug fix (구체적이지 않음)
여러 기능 추가 (타입 접두사 누락)

제목 작성 규칙

  • 명확하고 구체적으로: 무엇을 변경했는지 명확히 기술
  • 50자 이내: 제목은 간결하게 작성
  • 명령형: "~했음" 대신 "~함" 또는 "~추가" 형태 사용
  • 마침표 없음: 제목 끝에 마침표를 붙이지 않음

본문 작성 규칙 (선택사항)

feat: 제품 카테고리별 필터링 기능 추가

사용자가 제품을 카테고리별로 필터링할 수 있는 기능을 추가했습니다.
지원 카테고리:
- 주방/생활
- 문구
- 완구/취미
- 화장품/미용

관련 이슈: #123

커밋 전 체크리스트

커밋하기 전에 다음 사항을 반드시 확인하세요:

  • 코드가 빌드되는가?
  • TypeScript 타입 에러가 없는가?
  • 개인정보나 민감한 정보가 포함되어 있지 않은가?
  • API 키, 비밀번호, 토큰 등이 포함되어 있지 않은가?
  • 테스트가 통과하는가? (테스트가 있는 경우)
  • npm run test:coverage가 100% (Statements/Branches/Functions/Lines)를 만족하는가?
  • 주석이 한국어로 작성되어 있는가?
  • 파일이 450줄을 초과하지 않는가?

보안 및 개인정보

절대 커밋하지 말아야 할 항목

다음 항목들은 절대 Git 저장소에 커밋되어서는 안 됩니다:

1. 인증 정보

// ❌ 절대 금지
const API_KEY = 'sk-1234567890abcdef';
const PASSWORD = 'mypassword123';
const DATABASE_URL = 'postgresql://user:pass@localhost/db';
// ✅ 올바른 방법: 환경 변수 사용
const API_KEY = process.env.API_KEY;
const PASSWORD = process.env.PASSWORD;
const DATABASE_URL = process.env.DATABASE_URL;

2. 개인정보

  • 실제 이메일 주소
  • 전화번호
  • 주소
  • 신용카드 정보
  • 주민등록번호
  • 기타 식별 가능한 개인정보

3. 민감한 설정 파일

# ❌ 커밋 금지
.env
.env.local
.env.production
.env.development
secrets.json
credentials.json
service-account-key.json

# ✅ .gitignore에 추가되어 있는지 확인

4. 프라이빗 키 및 인증서

  • SSH private keys (id_rsa, id_ed25519 등)
  • SSL/TLS 인증서의 private key
  • JWT secret keys
  • OAuth client secrets

환경 변수 사용

민감한 정보는 반드시 환경 변수로 관리하세요.

.env.example 파일 제공

# .env.example (커밋 가능)
API_KEY=your_api_key_here
DATABASE_URL=your_database_url_here
SECRET_KEY=your_secret_key_here
# .env (커밋 금지, .gitignore에 포함)
API_KEY=sk-real-api-key-1234567890
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=super-secret-key-xyz

Cloudflare Workers Secrets

Cloudflare Workers 환경 변수는 다음과 같이 관리:

# 로컬 개발용 (.dev.vars 파일 - .gitignore에 포함)
API_KEY=test-key-for-local-dev

# 프로덕션 배포용 (wrangler를 통해 설정)
npx wrangler secret put API_KEY

보안 체크리스트

커밋 전 다음 사항을 확인하세요:

  • .env 파일이 .gitignore에 포함되어 있는가?
  • 하드코딩된 API 키나 비밀번호가 없는가?
  • console.log()에 민감한 정보가 출력되지 않는가?
  • 주석에 실제 계정 정보가 포함되어 있지 않는가?
  • 테스트 데이터가 실제 개인정보를 포함하지 않는가?

파일 구조

플러그인 기반 아키텍처

이 프로젝트는 확장 가능한 플러그인 아키텍처로 설계되었습니다. 다이소 외에 편의점, 백화점, 영화관 등 다양한 서비스를 쉽게 추가할 수 있습니다.

핵심 설계 원칙

  1. ServiceProvider 인터페이스: 모든 서비스가 구현해야 하는 표준 계약
  2. ServiceRegistry: 서비스를 동적으로 등록하고 MCP 서버에 연결
  3. 도구 이름 네임스페이스: 서비스별 접두사 사용 (예: daiso_, cu_, cgv_)

디렉토리 구조

daiso-mcp/
├── src/
│   ├── index.ts              # MCP 서버 진입점 (450줄 이하)
│   ├── core/                 # 핵심 모듈 (확장 기반)
│   │   ├── types.ts          # 공통 타입 정의
│   │   ├── interfaces.ts     # ServiceProvider 인터페이스
│   │   └── registry.ts       # ServiceRegistry 클래스
│   ├── services/             # 서비스 프로바이더
│   │   └── daiso/            # 다이소 서비스
│   │       ├── index.ts      # 서비스 팩토리
│   │       ├── types.ts      # 다이소 전용 타입
│   │       ├── api.ts        # API 엔드포인트 중앙화
│   │       └── tools/        # 도구 구현
│   │           ├── searchProducts.ts
│   │           ├── findStores.ts
│   │           ├── checkInventory.ts
│   │           └── getPriceInfo.ts
│   └── utils/                # 유틸리티 함수
├── docs/                     # 네트워크 파싱/리서치 문서
├── examples/                 # 사용 예시
├── tests/                    # 테스트 파일
└── scripts/                  # 빌드/배포 스크립트

### 네트워크 파싱 기록

`docs/` 폴더에는 API 엔드포인트 분석을 위한 네트워크 파싱 기록이 저장됩니다.
새로운 서비스를 추가할 때 네트워크 분석 결과를 이 폴더에 `{서비스명}-` 접두사를 붙여 저장하세요.

docs/
├── daiso-network-analysis-result.md # 다이소 API 분석 결과
├── daiso-playwright-network-analysis.md # Playwright 네트워크 분석
├── daiso-replay-session-test.html # 세션 테스트 HTML
└── daiso-test-replay.js # 테스트 리플레이 스크립트


### 문서 기록

`docs/` 폴더에는 네트워크 파싱 기록과 API 리서치 문서를 함께 저장합니다.
새로운 서비스를 추가할 때 관련 문서를 이 폴더에 `{서비스명}-` 접두사로 저장하세요.

docs/
├── daiso-network-analysis-result.md # 다이소 API 분석 결과
├── daiso-playwright-network-analysis.md # Playwright 네트워크 분석
├── daiso-replay-session-test.html # 세션 테스트 HTML
└── daiso-test-replay.js # 테스트 리플레이 스크립트


### 새 서비스 추가 방법

새로운 서비스(예: CU 편의점)를 추가하려면:

#### 1. 서비스 디렉토리 생성

src/services/cu/
├── index.ts # CuService 클래스 및 팩토리
├── types.ts # CU 전용 타입 정의
├── api.ts # CU API 엔드포인트
└── tools/ # CU 도구 구현
├── searchProducts.ts
└── findStores.ts


#### 2. ServiceProvider 구현

```typescript
// src/services/cu/index.ts
import type { ServiceProvider } from '../../core/interfaces.js';
import type { ToolRegistration } from '../../core/types.js';

class CuService implements ServiceProvider {
  readonly metadata = {
    id: 'cu',
    name: 'CU 편의점',
    version: '1.0.0',
    description: 'CU 편의점 제품 검색 및 매장 찾기',
  };

  getTools(): ToolRegistration[] {
    return [
      // cu_search_products, cu_find_stores 등
    ];
  }
}

export function createCuService(): ServiceProvider {
  return new CuService();
}

3. 레지스트리에 등록

// src/index.ts
import { createDaisoService } from './services/daiso/index.js';
import { createCuService } from './services/cu/index.js';

registry.registerAll([createDaisoService, createCuService]);

도구 이름 규칙

서비스별 도구는 접두사로 구분합니다:

서비스 접두사 예시
다이소 daiso_ daiso_search_products
CU cu_ cu_search_products
CGV cgv_ cgv_search_movies

파일 명명 규칙

  • camelCase: 함수, 변수명
  • PascalCase: 클래스, 인터페이스, 타입명
  • kebab-case: 파일명 (선택사항)
  • SCREAMING_SNAKE_CASE: 상수
// 함수, 변수
const userName = 'John';
function getUserData() {}

// 클래스, 인터페이스, 타입
class UserService {}
interface UserData {}
type UserId = string;

// 상수
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';

코드 스타일

주석 작성

모든 주석은 한국어로 작성합니다.

파일 헤더 주석

/**
 * 제품 검색 도구
 *
 * 다이소 제품을 검색하고 필터링하는 기능을 제공합니다.
 * 검색 조건: 키워드, 카테고리, 가격 범위
 *
 * @module tools/searchProducts
 */

함수 주석

/**
 * 제품을 검색하고 필터링된 결과를 반환합니다.
 *
 * @param query - 검색 키워드
 * @param category - 제품 카테고리 (선택사항)
 * @param maxPrice - 최대 가격 (선택사항)
 * @returns 검색 결과 객체
 * @throws {Error} API 호출 실패 시
 */
async function searchProducts(
  query: string,
  category?: string,
  maxPrice?: number,
): Promise<SearchResult> {
  // 구현
}

인라인 주석

// 사용자 입력 검증
if (!query || query.trim().length === 0) {
  throw new Error('검색어를 입력해주세요');
}

// TODO: 캐싱 기능 추가 필요
const results = await fetchFromAPI(query);

// FIXME: 대소문자 구분 없이 검색하도록 수정 필요
const filtered = results.filter((item) => item.name.includes(query));

TypeScript 타입 정의

// 인터페이스 정의
interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  description?: string; // 선택적 속성
  inStock: boolean;
}

// 타입 별칭
type ProductId = string;
type ProductCategory = '주방/생활' | '문구' | '완구/취미' | '화장품/미용';

// 유니온 타입
type SearchFilter = {
  query: string;
  category?: ProductCategory;
  maxPrice?: number;
};

코드 포맷팅

  • 들여쓰기: 2 spaces
  • 따옴표: single quotes (') 우선
  • 세미콜론: 사용
  • 줄 길이: 최대 100자 권장
// ✅ 좋은 예
const message = '안녕하세요';
const user = {
  name: '홍길동',
  age: 30,
};

// ❌ 나쁜 예
const message = '안녕하세요';
const user = { name: '홍길동', age: 30 };

문서화

README.md

프로젝트의 메인 문서로 다음 내용을 포함:

  • 프로젝트 개요
  • 설치 방법
  • 사용 방법
  • API 문서
  • 배포 가이드

API 문서

각 도구의 입력/출력 스키마를 명확히 문서화:

/**
 * 제품 검색 API
 *
 * 요청 예시:
 * ```json
 * {
 *   "name": "search_products",
 *   "arguments": {
 *     "query": "수납박스",
 *     "category": "주방/생활",
 *     "maxPrice": 5000
 *   }
 * }
 * ```
 *
 * 응답 예시:
 * ```json
 * {
 *   "content": [{
 *     "type": "text",
 *     "text": "{ ... }"
 *   }]
 * }
 * ```
 */

CHANGELOG.md (권장)

주요 변경사항을 기록:

# Changelog

## [1.1.0] - 2025-03-01

### Added

- 제품 카테고리별 필터링 기능 추가
- 가격 히스토리 조회 기능 추가

### Fixed

- 매장 찾기 시 거리 계산 오류 수정

### Changed

- API 응답 형식 개선

테스트

테스트 작성 원칙

  • 모든 도구는 테스트를 작성해야 합니다
  • 엣지 케이스를 고려한 테스트 작성
  • 에러 처리 로직도 테스트
  • 코드 커버리지 100% 유지: 커밋 전 npm run test:coverage를 실행해 100%를 유지해야 합니다

테스트 파일 명명

src/tools/searchProducts.ts
tests/tools/searchProducts.test.ts

테스트 예시

import { searchProducts } from '../src/tools/searchProducts';

describe('제품 검색', () => {
  test('키워드로 제품을 검색할 수 있다', async () => {
    const result = await searchProducts({ query: '수납박스' });
    expect(result.content).toBeDefined();
    expect(result.content[0].type).toBe('text');
  });

  test('존재하지 않는 제품은 빈 결과를 반환한다', async () => {
    const result = await searchProducts({ query: '존재하지않는제품xyz' });
    const data = JSON.parse(result.content[0].text);
    expect(data.count).toBe(0);
  });

  test('빈 검색어는 에러를 발생시킨다', async () => {
    await expect(searchProducts({ query: '' })).rejects.toThrow();
  });
});

체크리스트 요약

코드 작성 전

  • 어떤 기능을 구현할지 명확히 이해했는가?
  • 파일이 450줄을 초과하지 않도록 설계했는가?
  • 적절한 파일 구조와 모듈 분리를 계획했는가?

코드 작성 중

  • TypeScript 타입을 명확히 정의했는가?
  • 주석을 한국어로 작성했는가?
  • 에러 핸들링을 적절히 구현했는가?
  • 보안에 취약한 코드가 없는가?

커밋 전

  • 빌드가 성공하는가?
  • 타입 에러가 없는가?
  • 개인정보나 민감한 정보가 포함되지 않았는가?
  • 커밋 메시지가 컨벤션을 따르는가?
  • 파일이 450줄 이하인가?
  • npm run test:coverage 결과가 100% (Statements/Branches/Functions/Lines)인가?

커밋 메시지 작성 시

# 형식 확인
<타입>: <한국어 제목>

# 예시
feat: 제품 검색 필터링 기능 추가
fix: 매장 거리 계산 오류 수정
docs: API 사용 가이드 업데이트

참고 자료


이 규칙을 따라 일관되고 안전하며 유지보수 가능한 코드를 작성해주세요.

Linked Files

docs/ai-instruction.md 13,352 bytes

당신은 다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 생활·쇼핑·영화 도우미입니다.

다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 관련 정보는 모두 mcp.aka.page 작업 기능만
사용하고, 웹검색은 하지 마세요.
mcp.aka.page 작업 기능 호출 중 에러가 나면 아래 문구로 안내하세요:
"ChatGPT 계정 로그인이 필요합니다. (다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV 서비스
자체 로그인은 필요 없습니다.)"

언어 감지 및 역할 설정

한국어 사용자: 간결하게 정보를 안내합니다.

외국어 사용자 (Foreign Travelers): 한국이 처음인 여행객을 위해 친절하게 안내합니다.

  • 상품 검색 시 해당 과자/음료가 어떤 맛인지, 인기 있는 이유를 설명
  • 한국 편의점 문화나 추천 상품도 함께 안내
  • 예: "Hot6 is Korea's most popular energy drink, similar to Red Bull but with a unique Korean twist!"

외국인 여행객 추천 상품:

  • Honey Butter Chips (허니버터칩) - Korea's viral sensation! Sweet and savory potato chips
  • Choco Pie (초코파이) - Iconic Korean chocolate-marshmallow treat
  • Banana Milk (바나나맛우유) - Must-try Korean banana flavored milk in a cute bottle
  • Milkis (밀키스) - Refreshing milk-soda fusion, uniquely Korean
  • Vita500 (비타500) - Popular vitamin C drink, great for hangovers
  • Soju (소주) - Korea's famous clear spirit, available in many flavors

사용 도구 원칙

  • 다이소: daiso_* 도구만 사용
  • GS25: gs25_* 도구만 사용
  • 세븐일레븐: seveneleven_* 도구만 사용
  • CU: cu_* 도구만 사용
  • 이마트24: emart24_* 도구만 사용
  • 롯데마트: lottemart_* 도구만 사용
  • 올리브영: oliveyoung_* 도구만 사용
  • 메가박스: megabox_* 도구만 사용
  • 롯데시네마: lottecinema_* 도구만 사용
  • CGV: cgv_* 도구만 사용
  • 사용자가 특정 브랜드를 명시하면 품목상 어색해 보여도 먼저 그 브랜드 도구로 실제 조회를 수행하고, 사용자가 요청하지 않은 다른 브랜드로 임의 우회하지 않음
  • 한 문장 안에서 브랜드명이 앞부분에만 한 번 등장해도 뒤의 상품/재고 요청까지 같은 브랜드로 해석함
  • 명시된 브랜드에서 실제 조회 결과가 없을 때만 "해당 브랜드 기준 결과 없음"을 분명히 설명한 뒤 다른 브랜드 대안을 짧게 제안
  • "핫식스는 다이소가 아니라 편의점 상품이라서 조회할 수 없다"처럼 카테고리 추정만으로 조회를 거부하지 않음
  • MCP/REST 호출 실패 시 임의 추측 답변 금지, 에러 안내 후 재시도 유도

기능

  • 다이소 제품 검색: 사용자가 찾는 제품을 검색
  • 다이소 매장 찾기: 지역명/키워드로 매장 조회
  • 다이소 재고 확인: 특정 제품의 온라인/오프라인 재고 확인
  • 다이소 제품 상세: 제품 ID로 상세 조회
  • 다이소 진열 위치 조회: 제품 ID+매장 코드로 구역/층 확인
  • GS25 주변 매장 탐색: 사용자 위치/키워드 기반 GS25 매장 조회
  • GS25 상품 검색: 상품 키워드 기반 후보 상품 조회
  • GS25 재고 확인: 상품 키워드 기준 재고 및 주변 매장 정보 조회
  • 세븐일레븐 상품 검색: 상품 키워드 기반 후보 상품 조회
  • 세븐일레븐 매장 검색: 지역/매장 키워드 기반 세븐일레븐 매장 조회
  • 세븐일레븐 재고 확인: 상품 키워드 기준 재고 및 주변 매장 정보 조회
  • CU 주변 매장 탐색: 사용자 위치/키워드 기반 CU 매장 조회
  • CU 재고 확인: 상품 키워드 기준 재고 및 주변 매장 정보 조회
  • 이마트24 매장 탐색: 키워드/지역 조건 기반 이마트24 매장 조회
  • 이마트24 상품 검색: 키워드 기반 상품 목록 조회
  • 이마트24 재고 확인: PLU 코드 + 매장 코드 목록 기반 재고 조회
  • 롯데마트 주변 매장 탐색: 지역/키워드/좌표 기반 롯데마트 계열 매장 조회
  • 롯데마트 상품 검색: 특정 매장을 기준으로 상품 가격/재고 조회
  • 올리브영 주변 매장 탐색: 사용자 위치 기반 매장 조회
  • 올리브영 재고 파악: 상품 키워드 기준 재고/품절 여부 및 주변 매장 정보 조회
  • 메가박스 주변 지점 탐색: 사용자 위치 기준 가까운 영화관 조회
  • 메가박스 상영작/회차 조회: 날짜/지점/영화 기준 상영 정보 조회
  • 메가박스 잔여 좌석 조회: 날짜/지점/영화 기준 회차별 잔여 좌석 조회
  • 롯데시네마 주변 지점 탐색: 사용자 위치 기준 가까운 영화관 조회
  • 롯데시네마 상영작/회차 조회: 날짜/지점/영화 기준 상영 정보 조회
  • 롯데시네마 잔여 좌석 조회: 날짜/지점/영화 기준 회차별 잔여 좌석 조회
  • CGV 주변 지점 탐색: 사용자 위치 기준 가까운 영화관 조회
  • CGV 상영작/회차 조회: 날짜/지점/영화 기준 상영 정보 조회
  • CGV 잔여 좌석 조회: 날짜/지점/영화 기준 회차별 잔여 좌석 조회

워크플로우

  1. 사용자가 브랜드(다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV)를 명시하면 품목상 어울리지 않아 보여도 먼저 해당 서비스 도구로 실제 조회를 수행
    • 금지 예시: "핫식스는 다이소가 아니라 편의점 상품이라서 다이소 조회를 생략하겠습니다."
    • 올바른 예시: "먼저 다이소 기준으로 검색해보고, 없으면 그때 다른 브랜드를 제안하겠습니다."
    • 예시 해석: "안산 중앙역 주변 다이소 찾아주시고 핫식스 재고 찾아주세요"는 뒤의 핫식스 재고 요청도 다이소 기준으로 처리
  2. 브랜드를 명시하지 않으면 먼저 어느 브랜드를 원하는지 짧게 확인
  3. 재고/좌석 확인 요청 시:
    • 다이소: 먼저 제품 검색 후 productId로 재고 확인
    • 다이소 진열 위치: productId + storeCodedaiso_get_display_location 호출
    • GS25 (필수 2단계 검색):
      1. 반드시 먼저 gs25_search_products로 상품 검색 (바로 gs25_check_inventory 호출 금지)
      2. 검색 결과를 사용자에게 보여주고 선택 유도 (예: "1. 핫식스250ML / 2. 핫식스더킹")
      3. 사용자가 선택한 상품의 itemCodegs25_check_inventory 호출
    • 세븐일레븐: keyword로 재고 조회, 필요 시 storeKeyword를 함께 사용
    • CU: keyword로 재고 조회, 필요 시 storeKeyword/위치(lat,lng) 반영
    • 이마트24: 먼저 상품 검색으로 pluCd를 확인하고 storeKeyword 또는 bizNoArr와 함께 재고 조회
    • 롯데마트: 먼저 lottemart_find_nearby_stores 또는 매장명으로 점포를 특정한 뒤 lottemart_search_products 호출
    • 올리브영: 상품 후보/종류 질문은 먼저 상품 검색, 재고 질문은 keyword로 재고 조회, 필요 시 storeKeyword/위치(lat,lng) 반영
  • 메가박스: keyword나 좌표가 있으면 먼저 가까운 지점을 특정하고, 상영작/회차 조회 후 필요 시 theaterId/movieId로 잔여 좌석 조회
  • 롯데시네마: 먼저 상영작/회차 조회 후 필요 시 theaterId/movieId로 잔여 좌석 조회
  • CGV: keyword나 좌표가 있으면 먼저 가까운 극장을 특정하고, 상영작/회차 조회 후 필요 시 theaterCode/movieCode로 잔여 좌석 조회
  1. 결과는 핵심 정보 중심으로 보기 쉽게 정리
    • 다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영: 상품명, 가격, 재고 여부, 매장명/거리
    • 메가박스/롯데시네마/CGV: 영화명, 극장명, 상영시간, 잔여좌석/총좌석, 거리
    • 이미지 렌더링 강제 규칙:
      • 규칙 1: MCP/REST 응답에 imageUrl이 있으면 반드시 마크다운 이미지로 렌더링
      • 규칙 2: 사용자가 따로 요청하기 전까지 이미지를 임의로 생략하지 않음
      • 규칙 3: 이미지가 여러 개이면 각 항목의 이미지를 전부 표시하고 첫 번째만 대표로 남기지 않음
      • 규칙 4: "길어서 일부만 표시", "대표 이미지만 표시", "나머지는 생략" 같은 동작 금지
      • 규칙 5: 텍스트 요약보다 이미지 표시 의무를 우선하고, 답변 길이 때문에 이미지를 빼지 않음
      • 규칙 6: imageUrl에 query string이 있으면 ?l=ko 같은 쿼리를 삭제하거나 정규화하지 않고 전체 URL 그대로 마크다운 이미지 src에 사용
      • 규칙 7: 이미지 src를 비교하거나 캐시 키를 판단할 때도 hostname + pathname만 쓰지 말고 query string까지 포함한 전체 URL 기준으로 처리

주의사항

  • 다이소 재고 확인 전에는 반드시 제품 검색으로 ID를 확인
  • 다이소 재고 조회는 productId + keyword 또는 위치(lat,lng)만으로 가능하며 storeCode가 필요하지 않음
  • 상품/재고 응답에 imageUrl이 있으면 답변에서 반드시 마크다운 이미지로 함께 표시함
  • 상품/재고 이미지가 여러 개면 첫 번째만 남기지 말고 전부 표시함
  • 답변 길이, 대표 이미지 관행, 임의 판단을 이유로 이미지를 일부만 생략하지 않음
  • 다이소 storeCode는 매장 검색 결과가 아니라 재고 조회 결과에서 확보하는 값으로 취급
  • 다이소 안산 중앙역 같은 역명 키워드가 비면 안산중앙역, 안산중앙, 고잔 등 변형 키워드로 재시도
  • 다이소 진열 위치 조회는 productIdstoreCode가 모두 필요
  • 다이소 매장 검색 시 keyword 또는 sido 중 하나 필요
  • GS25 재고 조회 전에는 반드시 gs25_search_products로 상품을 먼저 검색 (바로 재고 조회 금지)
  • GS25 검색 결과가 여러 개면 사용자에게 번호로 선택하게 한 후 해당 itemCode로 재고 조회
  • GS25 재고 조회 시 storeKeyword/위치(lat,lng)를 함께 사용하면 정확도 향상
  • GS25 매장/재고 조회는 위치값(lat,lng)이 있으면 함께 사용
  • 세븐일레븐 재고 조회 시 keyword는 필수이며 storeKeyword를 함께 사용하면 정확도 향상
  • 세븐일레븐 매장 조회는 storeKeyword를 지역/역명 기준으로 정리해 함께 안내
  • CU 매장/재고 조회는 위치값(lat,lng)이 있으면 함께 사용
  • CU 재고 조회 시 keyword는 필수이며 필요 시 storeKeyword를 함께 사용
  • 이마트24 재고 조회는 pluCd + storeKeyword 또는 pluCd + bizNoArr 조합을 사용
  • 이마트24 매장 조회 후 storeCode를 모아 bizNoArr로 전달할 수 있고, 대화형 흐름에서는 기존 매장 키워드를 다시 넣어도 됨
  • 롯데마트 상품 검색 시 keyword는 필수이며 storeCode 또는 storeName 중 하나가 필요
  • 롯데마트 주변 매장 조회는 좌표가 없으면 keyword 또는 주소 지오코딩 결과에 따라 거리 계산이 보강될 수 있음
  • 올리브영에서 "어떤 거 있나요", "종류 보여줘", "상품 목록" 요청은 먼저 oliveyoung_search_products 또는 /api/oliveyoung/products 사용
  • oliveyoung_search_products 또는 /api/oliveyoung/products 응답의 products[].imageUrl이 있으면 각 상품 이미지를 반드시 마크다운으로 전부 렌더링
  • 올리브영 이미지 URL의 query string(?l=ko 등)은 마크다운 렌더링 시 절대 삭제하지 않으며, 전체 URL을 그대로 사용
  • 올리브영 매장/재고 조회는 위치값(lat,lng)이 있으면 함께 사용
  • 올리브영 재고 조회 결과에 storeInventory가 있으면 매장별 stockLabel, remainQuantity를 우선 사용
  • 올리브영 inStock는 주변 매장 기준 결과로 해석하고, o2oStockFlag/o2oRemainQuantity는 보조값으로만 사용
  • 메가박스 지점/영화/좌석 조회는 keyword(예: 안산 중앙역) 또는 위치값(lat,lng)이 있으면 함께 사용
  • 메가박스 좌석 조회는 playDate(YYYYMMDD) 사용, 가능하면 theaterId/movieId를 함께 사용
  • 롯데시네마 지점/영화/좌석 조회는 keyword(예: 안산 중앙역) 또는 위치값(lat,lng)이 있으면 함께 사용
  • 롯데시네마 영화/좌석 조회는 theaterId가 없으면 위치 키워드 기준 최근접 지점을 먼저 선택
  • 롯데시네마 좌석 조회는 playDate(YYYYMMDD) 사용, 가능하면 theaterId/movieId를 함께 사용
  • CGV 극장/영화/시간표 조회는 keyword(예: 안산 중앙역) 또는 위치값(lat,lng)이 있으면 함께 사용
  • CGV 좌석 조회는 playDate(YYYYMMDD) 사용, 가능하면 theaterCode/movieCode를 함께 사용
  • CGV 잔여 좌석은 별도 좌석 API가 아니라 cgv_get_timetable 또는 /api/cgv/timetable 응답의 timetable[].remainingSeats로 확인
  • 위치값이 없으면 기본 위치(서울 시청 좌표)를 사용한다고 명시
  • 데이터가 비어 있으면 "조건을 바꿔 다시 검색"을 제안
docs/cgv-network-analysis-result.md 7,204 bytes

CGV 네트워크 분석 결과 (실측 업데이트)

작성일: 2026-03-04 (KST)
대상:

  • https://www.cgv.co.kr
  • https://api.cgv.co.kr
  • https://oidc.cgv.co.kr

결론 요약

  • Playwright(로컬 브라우저) 직접 접속: 실패
    • 차단 페이지 노출(Cloudflare / 비정상 접속 안내)
  • 비브라우저 직접 호출(서버 IP): 실패
    • https://api.cgv.co.kr에서 403 차단
  • Zyte 프록시 + 신 API + 서명 헤더: 성공
    • 극장/영화/시간표 모두 실데이터 수신 확인

스크래핑 플레이북 기준 판정

  1. Playwright MCP로 브라우저 동작 재현
  • 결과: 차단 재현(실패)
  1. 브라우저 요청 체인 분석
  • 기존 m.cgv.co.kr/WebAPP/ReservationV5/* 경로는 현재 실효성 없음
  • 신 프론트 번들에서 api.cgv.co.kr 티켓 API 확인
  1. 비브라우저 재현 시도
  • 직접 호출은 403
  • Zyte 프록시 경유 시 성공

현재 유효 엔드포인트 (티켓)

  • GET /cnm/atkt/searchRegnList

    • 목적: 지역/극장 목록
    • 필수 쿼리: coCd=A420
  • GET /cnm/atkt/searchOnlyCgvMovList

    • 목적: 특정 극장/날짜 영화 목록
    • 필수 쿼리: coCd, siteNo, scnYmd(YYYYMMDD)
  • GET /cnm/atkt/searchSchByMov

    • 목적: 특정 극장/날짜/영화 시간표
    • 필수 쿼리: coCd, siteNo, scnYmd, movNo, rtctlScopCd
    • rtctlScopCd 누락 시 에러: 발매통제범위코드는 필수 요청 파라미터
    • 실측 성공값: rtctlScopCd=01

필수 요청 헤더

  • Accept: application/json
  • Accept-Language: ko-KR
  • X-TIMESTAMP: <epoch-seconds>
  • X-SIGNATURE: <base64-hmac-sha256>

서명 규칙:

  • 메시지: {timestamp}|{pathname}|{bodyText}
  • 알고리즘: HMAC-SHA256
  • 키: 프론트 번들에 하드코딩된 secret 사용

실데이터 확인 샘플 (2026-03-04)

  • 극장 목록:
    • siteNo=0056, siteNm=강남 포함
  • 영화 목록(강남/20260304):
    • movNo=30000985, movNm=엔하이픈 [워크 더 라인 썸머 에디션] 인 시네마
  • 시간표(강남/20260304/30000985/rtctlScopCd=01):
    • 회차 2건 수신
    • 예: scnsrtTm=1230, scnendTm=1443, frSeatCnt=118

정규화 매핑

극장 목록

  • 입력: siteNo, siteNm, regnGrpCd
  • 출력: theaterCode, theaterName, regionCode

영화 목록

  • 입력: movNo, movNm, cratgClsNm
  • 출력: movieCode, movieName, rating

시간표

  • 입력: scnYmd, scnSseq, siteNo, siteNm, movNo, movNm, scnsrtTm, scnendTm, stcnt, frSeatCnt
  • 출력: scheduleId, playDate, theaterCode, theaterName, movieCode, movieName, startTime, endTime, totalSeats, remainingSeats

구현 전략 (우선순위)

  1. 브라우저 기반 성공 경로 확보
  • Playwright는 차단되므로 Zyte 프록시를 브라우저 대체 경로로 사용
  1. 비브라우저 직접 호출 우선
  • 직접 호출 시도 후 403이면 fallback
  1. 불가피 시 Zyte 프록시 fallback
  • 동일 헤더/서명 규칙으로 api.cgv.co.kr 호출

후속 TODO

  • OIDC(oidc.cgv.co.kr) 기반 토큰 흐름이 필요한 API 범위 추가 분석
  • rtctlScopCd 값 체계 및 의미 문서화
  • 요일/상영관 유형 필터링 파라미터 정리

문제 구간 업데이트 (2026-03-04, KST)

스크래핑 플레이북의 "응답이 비어 있을 때 조건 변경 재검증" 절차에 따라
/api/cgv/timetable만 별도 재검증했습니다.

재검증 결과

  • 정상:
    • /api/cgv/theaters?playDate=20260304&limit=3 -> 극장 3건
    • /api/cgv/movies?playDate=20260304&theaterCode=0056 -> 영화 11건
  • 문제:
    • /api/cgv/timetable?playDate=20260304&limit=3 -> timetable=[]
    • /api/cgv/timetable?playDate=20260304&theaterCode=0056&limit=3 -> timetable=[]
    • /api/cgv/timetable?playDate=20260304&theaterCode=0056&movieCode=30001010&limit=5 -> timetable=[]
    • /api/cgv/timetable?playDate=20260305&theaterCode=0056&limit=5 -> timetable=[]

현재 판정

  • theaters, movies는 실응답 정상
  • timetable은 성공 응답(success=true)이지만 데이터 0건으로 고정되는 현상 존재
  • 따라서 현재 시점에서는 "시간표 API 데이터 경로 이상/조건 불일치" 상태로 분류

추정 원인 (실측 기반 가설)

  • movieCode 자동 선택 로직이 실제 상영 스케줄이 없는 코드로 고정될 가능성
  • CGV 상영 시간표 조회 조건(movNo, rtctlScopCd, 날짜/지점 조합) 유효성 변화 가능성
  • 데이터 공급 API는 성공하지만, 최종 스케줄 데이터셋이 빈 조건으로 조회되는 가능성

다음 조사 포인트

  • Zyte 원본 응답에서 searchSchByMov 요청/응답 payload를 그대로 로그하여
    빈 배열이 upstream인지, 정규화 단계인지 분리 확인
  • movieCode 미지정 시 "첫 영화 고정" 대신 상영건 존재하는 영화를 탐색하는 로직 검토

원인 확인 및 코드 반영 (2026-03-04, KST)

  • 원인 확인:

    • movieCode 미지정 시 첫 영화(30001010)를 고정 조회
    • 해당 코드의 시간표가 0건이라 /api/cgv/timetable 기본 호출이 빈 배열로 귀결
    • 같은 조건에서 movieCode=30000985 지정 시 시간표 2건 확인
  • 코드 반영:

    • src/services/cgv/client.tsfetchCgvTimetable을 수정해
      movieCode가 없을 때 영화 목록을 순차 조회하며 상영건이 있는 영화를 탐색
    • 최초로 시간표가 1건 이상 나오는 영화의 결과를 반환하도록 변경
  • 검증:

    • CGV 관련 테스트 재실행 통과
    • tests/services/cgv/client.test.ts에 순차 탐색 동작 테스트 추가/갱신

브라우저 재검증 업데이트 (2026-03-06, KST)

사용자 제보 기준(웹 브라우저에서는 좌석이 보이는데 API는 비어 있음)으로
Playwright 브라우저에서 CGV 예매 페이지를 직접 재검증했습니다.

재현 결과

  • 브라우저 예매 UI:

    • 경로: https://cgv.co.kr/cnm/movieBook
    • 극장: 안산(0211)
    • 날짜: 20260306
    • 회차/잔여좌석이 실제로 노출됨
      • 예: 왕과 사는 남자 22:30-24:37 175/248석
      • 예: 호퍼스 22:25-24:19 124/132석
  • 우리 API:

    • GET /api/cgv/timetable?playDate=20260306&theaterCode=0211
    • 응답: success=true, total=0, timetable=[]

브라우저 네트워크 실측

  • 브라우저가 실제로 호출한 시간표 API:
    • GET /cnm/atkt/searchMovScnInfo
    • 필수 파라미터: coCd, siteNo, scnYmd, rtctlScopCd
    • 실측값: rtctlScopCd=08
  • 해당 응답은 statusCode=0이며 data[]movNo, movNm, scnsrtTm, scnendTm, frSeatCnt, stcnt 포함

원인 결론

  • 기존 서버 구현은 주로 searchSchByMov(영화코드 중심) + rtctlScopCd=01 경로를 사용
  • 브라우저 실사용 경로는 searchMovScnInfo + rtctlScopCd=08
  • 따라서 극장/일자 조합에 따라 서버 경로에서는 빈 배열, 브라우저 경로에서는 정상 좌석 데이터가 발생

반영 방향

  • 시간표 1차 조회를 searchMovScnInfo + rtctlScopCd=08로 전환
  • 비정상 시 기존 searchSchByMov 경로를 fallback으로 유지
docs/cu-app-request-capture-guide.md 3,197 bytes

CU 앱 요청 수집/전달 가이드 (mitmproxy 기반)

작성일: 2026-03-08 (KST)
대상: 포켓CU 앱 스크래핑 분석용 요청 전달

1. 목적

이 문서는 사용자가 포켓CU 앱 트래픽을 수집한 뒤, 분석자가 바로 재현 가능한 형태로 전달할 수 있게 구성한 절차입니다.

핵심 산출물:

  • raw.mitm: 원본 mitmproxy 플로우 파일
  • requests.jsonl: 민감정보 마스킹된 요청/응답 레코드
  • summary.json: 시나리오/건수 요약

2. 사전 준비

  1. Mac/iPhone 동일 Wi-Fi 연결
  2. iOS 프록시 및 인증서 신뢰 설정 완료
  3. mitmproxy 설치
  4. 대상 시나리오 정의

권장 시나리오 예시:

  • 로그인 상태에서 점포 선택
  • 상품 검색
  • 재고조회 버튼 실행

3. 수집 명령

프로젝트 루트에서 아래를 실행합니다.

mkdir -p captures/cu-20260308
mitmdump \
  --listen-host 0.0.0.0 \
  --listen-port 8080 \
  -s scripts/mitmproxy/cu_capture_export.py \
  --set cu_capture_dir=captures/cu-20260308 \
  --set cu_capture_scenario='로그인 후 강남구 점포에서 상품 재고조회' \
  --set cu_capture_hosts='cu.bgfretail.com,pocketcu.co.kr' \
  -w captures/cu-20260308/raw.mitm

수집이 끝나면 Ctrl+C로 종료합니다.

4. 생성 파일 설명

A. requests.jsonl

  • 한 줄당 1개 요청/응답 레코드(JSON)
  • 포함 정보:
    • 요청: method, host, path, query, headers, body preview
    • 응답: statusCode, headers, body preview
  • 기본 마스킹:
    • 헤더: Authorization, Cookie, Set-Cookie, 토큰 계열
    • 쿼리: token, password, session, jwt 계열

B. summary.json

  • 시나리오명
  • 대상 호스트
  • 전체/매칭/스킵 건수
  • 산출물 경로

C. raw.mitm

  • 원본 플로우 파일
  • 필요 시 분석자가 mitmweb -r로 재열람

5. 전달 패키지 구성

아래 파일만 전달하면 됩니다.

captures/cu-20260308/
├── raw.mitm
├── requests.jsonl
└── summary.json

전달 전 확인:

  • 개인정보(전화번호/주소/멤버십 번호)가 본문 preview에 남아 있지 않은지
  • 테스트 계정 사용 여부

6. 분석자가 바로 쓰는 확인 명령

# 요청 개수 확인
wc -l captures/cu-20260308/requests.jsonl

# 재고 관련 경로 빠르게 탐색
rg -n 'stock|inventory|goods|product|재고' captures/cu-20260308/requests.jsonl

# HTTP 상태코드 분포 확인
jq -r '.response.statusCode // "NA"' captures/cu-20260308/requests.jsonl | sort | uniq -c

7. 실패 시 점검

  1. HTTPS가 안 보임
  • iOS 인증서 신뢰 설정 재확인
  1. 트래픽이 거의 없음
  • 앱 완전 종료 후 재실행
  • 프록시 IP/포트 재확인
  1. 특정 요청만 안 보임
  • 앱 certificate pinning 가능성
  • 동일 시나리오를 Android에서도 비교 수집

8. 기존 문서와의 연결

  • 기본 MITM 세팅: docs/mitmproxy-guide.md
  • CU 웹 실측 결과: docs/cu-network-analysis-result.md
  • CU 스크래핑 재현: docs/cu-app-scraping-replay-guide.md

이 문서는 위 두 문서를 바탕으로, "분석 가능한 전달 포맷"까지 포함한 실행 가이드입니다.

docs/cu-app-scraping-replay-guide.md 3,927 bytes

CU 앱 스크래핑 재현 가이드 (실측 기반)

작성일: 2026-03-08 (KST)
기준 캡처: captures/cu-20260308/requests-from-0234-kst.jsonl

1. 목적

이 문서는 포켓CU 앱 트래픽 실측 결과를 바탕으로,
재고/점포 조회를 로컬에서 재현하는 최소 절차를 제공합니다.

2. 핵심 엔드포인트

  • 재고 화면 데이터: POST /api/search/display/stock
  • 재고 검색: POST /api/search/rest/stock/main
  • 점포 조회: POST /api/store

베이스 URL:

  • https://www.pocketcu.co.kr

3. 공통 요청 조건

최소 헤더:

  • content-type: application/json
  • x-requested-with: XMLHttpRequest

권장:

  • 브라우저 세션/쿠키 없이도 2026-03-08 실측 기준 응답 확인됨
  • 다만 차단 정책 변경에 대비해 실패 시 최신 캡처로 헤더 재검증 필요

4. 재현 명령

A. 재고 화면 초기 데이터

curl -sS 'https://www.pocketcu.co.kr/api/search/display/stock' \
  -H 'content-type: application/json' \
  -H 'x-requested-with: XMLHttpRequest' \
  --data '{}' \
  | jq '{keys: keys}'

기대 결과:

  • areaCateList, areaItemList, areaList, cuconList, productList 포함

B. 재고 검색

curl -sS 'https://www.pocketcu.co.kr/api/search/rest/stock/main' \
  -H 'content-type: application/json' \
  -H 'x-requested-with: XMLHttpRequest' \
  --data '{
    "searchWord":"과자",
    "prevSearchWord":"두바이",
    "spellModifyUseYn":"Y",
    "offset":0,
    "limit":8,
    "searchSort":"recom"
  }' \
  | jq '{
      spellModifyYn,
      total: .data.stockResult.result.total_count,
      first: .data.stockResult.result.rows[0].fields
      | {item_cd,item_nm,hyun_maega,pickup_yn,deliv_yn,reserv_yn}
    }'

기대 결과:

  • total 양수
  • first.item_cd, first.item_nm 존재

C. 좌표 기반 점포 조회

curl -sS 'https://www.pocketcu.co.kr/api/store' \
  -H 'content-type: application/json' \
  -H 'x-requested-with: XMLHttpRequest' \
  --data '{
    "latVal":"37.3206029",
    "longVal":"126.8374892",
    "baseLatVal":"37.3206029",
    "baseLongVal":"126.8374892",
    "items":"",
    "jipCd":"",
    "voucher_cd":"",
    "exPin":"",
    "custId":"",
    "isRecommend":"",
    "recommendId":"",
    "pageType":"search_improve",
    "item_cd":"",
    "storeCd":"",
    "isCoupon":"",
    "firstRowNum":"",
    "tabId":"2",
    "filterSvcList":[],
    "filterAdtList":[],
    "stockCdcYn":"N",
    "alcProdYn":"",
    "searchStock":false,
    "pickupType":"change",
    "getRoute":"IOS",
    "areaTplNo":"0",
    "itemCd":"",
    "onItemNo":"",
    "childMealPickUpYn":"N",
    "onlineType":"",
    "searchWord":"",
    "stockChkYn":"",
    "isCurrentSearch":"N"
  }' \
  | jq '{
      totalCnt,
      first: .storeList[0] | {storeCd,storeNm,storeTelNo}
    }'

기대 결과:

  • totalCnt 양수
  • first.storeCd, first.storeNm 존재

5. 분석/검증 명령

# 캡처 구간 요청 수
wc -l captures/cu-20260308/requests-from-0234-kst.jsonl

# 재고 관련 요청만 확인
jq -r 'select(.request.path|test("stock";"i")) | [.capturedAt,.request.method,.request.path] | @tsv' \
  captures/cu-20260308/requests-from-0234-kst.jsonl

# 경로별 빈도
jq -r '.request.path' captures/cu-20260308/requests-from-0234-kst.jsonl | sort | uniq -c | sort -nr | head

6. 실패 대응

  1. 403/5xx 또는 빈 응답
  • 앱/웹 정책 변경 가능성. 최신 캡처로 파라미터 재동기화
  1. DNS/연결 실패
  • 실행 환경 네트워크 정책 확인(샌드박스/사내망)
  1. 결과는 오는데 필드 누락
  • offset, limit, searchSort, 검색어를 바꿔 재검증

7. 운영 권장

  • 스크래퍼는 display/stock + rest/stock/main 조합으로 구성
  • 응답 스키마 변화를 대비해 필드 접근에 방어 로직 적용
  • 민감정보 마스킹 정책(Authorization, Cookie, 토큰류) 유지
docs/cu-network-analysis-result.md 3,717 bytes

CU 편의점 네트워크 분석 결과 (웹 + 앱 실측 통합)

최종 업데이트: 2026-03-08 (KST)

실측 이력:

  • 2026-03-02: 웹 채널 실측 (Playwright MCP)
  • 2026-03-08: iOS 앱 트래픽 실측 (mitmproxy)

대상:

  • https://cu.bgfretail.com/store/list.do?category=store
  • https://www.pocketcu.co.kr/
  • 포켓CU iOS 앱 시나리오: 로그인 -> 점포 선택 -> 상품 검색 -> 재고조회

결론 요약

  • 주변 매장 조회: 가능
    • 웹: POST /store/list_Ajax.do 기반 즉시 구현 가능
    • 앱: POST /api/store 기반 좌표 중심 조회 가능
  • 재고 조회: 가능
    • 앱/웹 공통 도메인(www.pocketcu.co.kr)에서 재고 검색 API 실측 완료
  • 구현 판정:
    • cu_find_nearby_stores 즉시 구현 가능
    • cu_check_inventory 즉시 구현 가능 (요청/응답 스키마 확인 완료)

1) 웹 채널 실측 결과 (2026-03-02)

확인된 API

  • POST /store/GugunList.do (시/도 -> 구/군)
  • POST /store/DongList.do (구/군 -> 동)
  • POST /store/list_Ajax.do (매장 목록)

판정

  • 웹 매장 탐색은 안정적으로 재현 가능
  • 당시 기준으로는 재고 수량 API가 웹에서 보이지 않아 앱 실측 필요로 분류

2) 앱 트래픽 실측 결과 (2026-03-08)

실측 파일:

  • captures/cu-20260308/raw.mitm
  • captures/cu-20260308/requests.jsonl
  • captures/cu-20260308/summary.json
  • captures/cu-20260308/requests-from-0234-kst.jsonl (02:34 KST 이후 필터)

집계(02:34 KST 이후):

  • 총 191건
  • 상태코드: 200(190건), 302(1건)
  • 호스트: www.pocketcu.co.kr(187건), cloud.pocketcu.co.kr(4건)

A. 재고 화면 초기 데이터

  • Endpoint: POST /api/search/display/stock
  • 요청 body: {}
  • 응답 키 예시:
    • areaCateList
    • areaItemList
    • areaList
    • cuconList
    • productList

B. 재고 검색 핵심 API

  • Endpoint: POST /api/search/rest/stock/main
  • 샘플 요청:
{
  "searchWord": "과자",
  "prevSearchWord": "두바이",
  "spellModifyUseYn": "Y",
  "offset": 0,
  "limit": 8,
  "searchSort": "recom"
}
  • 응답 필드 예시:
    • data.stockResult.result.total_count
    • rows[].fields.item_cd
    • rows[].fields.item_nm
    • rows[].fields.hyun_maega
    • rows[].fields.pickup_yn
    • rows[].fields.deliv_yn
    • rows[].fields.reserv_yn

C. 점포 조회 API (앱)

  • Endpoint: POST /api/store
  • 주요 입력:
    • latVal, longVal
    • tabId
    • filterSvcList
    • itemCd, onItemNo (상황별)
  • 응답 필드 예시:
    • totalCnt
    • storeList[].storeCd
    • storeList[].storeNm

3) 재현성 검증 (curl)

2026-03-08 기준, 아래 API는 최소 헤더로 curl 재현 성공:

  • POST https://www.pocketcu.co.kr/api/search/display/stock
  • POST https://www.pocketcu.co.kr/api/search/rest/stock/main
  • POST https://www.pocketcu.co.kr/api/store

공통 최소 헤더:

  • content-type: application/json
  • x-requested-with: XMLHttpRequest

상세 재현 명령은 docs/cu-app-scraping-replay-guide.md 참고.

4) 구현 권장안

즉시 구현

  • cu_find_nearby_stores

    • 1순위: 웹 list_Ajax.do 파싱
    • 2순위: 앱 api/store 기반 좌표 조회
  • cu_check_inventory

    • 흐름:
      1. api/search/display/stock 초기 데이터
      2. api/search/rest/stock/main 검색
      3. 필요 시 item_cd 기반 후속 상세 조회

주의사항

  • 민감정보(Cookie, 토큰)는 저장/로그에서 마스킹 유지
  • 앱 버전 변화로 파라미터가 변경될 수 있으므로, 배포 전 재실측 권장

5) 관련 문서

  • docs/mitmproxy-guide.md
  • docs/cu-app-request-capture-guide.md
  • docs/cu-app-scraping-replay-guide.md
docs/daiso-network-analysis-result.md 12,072 bytes

다이소 매장 검색 API 분석 결과

분석 일시

2026-02-28

결론: 리플레이 가능 ✅

쿠키 및 특별한 헤더 없이 API 호출이 가능합니다.


발견된 API 엔드포인트

1. 매장 검색 (키워드)

GET /cs/ajax/shop_search?name_address={검색어}&sido=&gugun=&dong=

요청 예시:

curl 'https://www.daiso.co.kr/cs/ajax/shop_search?name_address=%EA%B0%95%EB%82%A8&sido=&gugun=&dong='

응답 형식: HTML

  • 전체 HTML 페이지 반환
  • 매장 데이터는 HTML 내 div.bx-store 요소에 포함
  • 파싱 필요

매장 데이터 구조 (HTML data 속성):

<div class="bx-store"
     data-start="1000"
     data-end="2200"
     data-lat="37.5171892352971"
     data-lng="127.04130142966"
     data-info='{"shp_pak":"N","entrramp":"N","elvtor":"N",...}'
     data-opnday="20160423">
  <h4 class="place">강남구청역점</h4>
  <em class="phone">T.1522-4400</em>
  <p class="addr">서울특별시 강남구 학동로 지하 346 (삼성동) B1층</p>
</div>

2. 시/도 → 구/군 목록 조회

GET /cs/ajax/sido_search?sido={시도명}

요청 예시:

curl 'https://www.daiso.co.kr/cs/ajax/sido_search?sido=%EC%84%9C%EC%9A%B8'

응답 형식: JSON

[
  {"value":"동대문구"},
  {"value":"강남구"},
  {"value":"중랑구"},
  {"value":"은평구"},
  ...
]

3. 구/군 → 동 목록 조회

GET /cs/ajax/gugun_search?sido={시도명}&gugun={구군명}

요청 예시:

curl 'https://www.daiso.co.kr/cs/ajax/gugun_search?sido=%EC%84%9C%EC%9A%B8&gugun=%EA%B0%95%EB%82%A8%EA%B5%AC'

응답 형식: JSON

[
  {"value":"일원동"},
  {"value":"도곡동"},
  {"value":"논현동"},
  {"value":"역삼동"},
  ...
]

4. 지역으로 매장 검색

GET /cs/ajax/shop_search?name_address=&sido={시도}&gugun={구군}&dong={동}

요청 예시:

curl 'https://www.daiso.co.kr/cs/ajax/shop_search?name_address=&sido=%EC%84%9C%EC%9A%B8&gugun=%EA%B0%95%EB%82%A8%EA%B5%AC&dong='

응답 형식: HTML (키워드 검색과 동일)


시/도 코드 목록

코드 지역명
서울 서울특별시
경기 경기도
인천 인천광역시
강원 강원도
광주 광주광역시
대전 대전광역시
울산 울산광역시
세종 세종특별자치시
충북 충청북도
충남 충청남도
전북 전라북도
전남 전라남도
경북 경상북도
경남 경상남도
대구 대구광역시
부산 부산광역시
제주 제주특별자치도

매장 옵션 필터 파라미터

파라미터 설명
shp_pak 주차 가능
entrramp 출입구 경사로
elvtor 엘리베이터
ptcard 현금없는매장
ptstk 포토 스티커
nmstk 네임 스티커
usim_yn 심카드
tax_free 택스리펀드
group_yn 단체주문
online_yn 매장픽업

인증 요구사항

항목 필요 여부
세션 쿠키 ❌ 불필요
X-Requested-With 헤더 ❌ 불필요
Referer 헤더 ❌ 불필요
CSRF 토큰 ❌ 불필요

구현 권장사항

Cloudflare Workers 구현 가능 ✅

  1. HTTP 클라이언트만으로 충분

    • Puppeteer/Playwright 불필요
    • fetch API로 직접 호출 가능
  2. HTML 파싱 필요

    • 매장 검색 응답은 HTML
    • cheerio 또는 정규식으로 파싱 권장
  3. JSON API 활용

    • 시/도, 구/군 목록은 JSON
    • 직접 파싱 가능

구현 예시

// 매장 검색
async function searchStores(query: string): Promise<Store[]> {
  const url = `https://www.daiso.co.kr/cs/ajax/shop_search?name_address=${encodeURIComponent(query)}&sido=&gugun=&dong=`;
  const response = await fetch(url);
  const html = await response.text();
  return parseStoresFromHtml(html);
}

// 시/도 → 구/군 목록
async function getDistricts(sido: string): Promise<string[]> {
  const url = `https://www.daiso.co.kr/cs/ajax/sido_search?sido=${encodeURIComponent(sido)}`;
  const response = await fetch(url);
  const data = await response.json();
  return data.map((item: {value: string}) => item.value);
}

// 구/군 → 동 목록
async function getNeighborhoods(sido: string, gugun: string): Promise<string[]> {
  const url = `https://www.daiso.co.kr/cs/ajax/gugun_search?sido=${encodeURIComponent(sido)}&gugun=${encodeURIComponent(gugun)}`;
  const response = await fetch(url);
  const data = await response.json();
  return data.map((item: {value: string}) => item.value);
}

매장 데이터 파싱 예시

interface Store {
  name: string;
  phone: string;
  address: string;
  lat: number;
  lng: number;
  openTime: string;
  closeTime: string;
  options: {
    parking: boolean;
    ramp: boolean;
    elevator: boolean;
    cashless: boolean;
    photoSticker: boolean;
    nameSticker: boolean;
    simCard: boolean;
    taxFree: boolean;
    groupOrder: boolean;
    pickup: boolean;
  };
}

function parseStoresFromHtml(html: string): Store[] {
  const stores: Store[] = [];
  const regex = /<div class="bx-store"[^>]*data-start="(\d+)"[^>]*data-end="(\d+)"[^>]*data-lat="([^"]+)"[^>]*data-lng="([^"]+)"[^>]*data-info="([^"]+)"[^>]*>[\s\S]*?<h4 class="place">([^<]+)<\/h4>[\s\S]*?<em class="phone">([^<]*)<\/em>[\s\S]*?<p class="addr">([^<]+)<\/p>/g;

  let match;
  while ((match = regex.exec(html)) !== null) {
    const info = JSON.parse(match[5].replace(/&quot;/g, '"'));
    stores.push({
      name: match[6],
      phone: match[7].replace('T.', '').trim(),
      address: match[8],
      lat: parseFloat(match[3]),
      lng: parseFloat(match[4]),
      openTime: match[1],
      closeTime: match[2],
      options: {
        parking: info.shp_pak === 'Y',
        ramp: info.entrramp === 'Y',
        elevator: info.elvtor === 'Y',
        cashless: info.ptcard === 'Y',
        photoSticker: info.ptstk === 'Y',
        nameSticker: info.nmstk === 'Y',
        simCard: info.usim_yn === 'Y',
        taxFree: info.tax_free === 'Y',
        groupOrder: info['ext.group_yn'] === 'Y',
        pickup: info.online_yn === 'Y',
      }
    });
  }

  return stores;
}

테스트 결과 요약

테스트 결과
쿠키 없이 매장 검색 ✅ 성공
헤더 없이 매장 검색 ✅ 성공
쿠키 없이 시/도 API ✅ 성공
쿠키 없이 구/군 API ✅ 성공
curl로 리플레이 ✅ 성공

다이소몰 재고 조회 API (mapi.daisomall.co.kr)

1. 상품 검색

GET https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods?searchTerm={검색어}&cntPerPage=30&pageNum=1

요청 예시:

curl 'https://prdm.daisomall.co.kr/ssn/search/FindStoreGoods?searchTerm=%EB%AC%BC%ED%8B%B0%EC%8A%88&cntPerPage=30&pageNum=1'

응답 형식: JSON

{
  "resultSet": {
    "result": [{
      "totalSize": 250,
      "resultDocuments": [
        {"PD_NO": "1068725", "PD_NM": "픽사_토이스토리_빅사이즈물티슈80매", ...}
      ]
    }]
  }
}

2. 온라인 재고 조회

POST https://mapi.daisomall.co.kr/ms/msg/selOnlStck
Content-Type: application/json

요청 Body:

{"pdNo": "1068725"}

요청 예시:

curl 'https://mapi.daisomall.co.kr/ms/msg/selOnlStck' \
  -H 'Content-Type: application/json' \
  -d '{"pdNo":"1068725"}'

응답 형식: JSON

{
  "data": {
    "pdNo": "1068725",
    "stck": 17
  },
  "success": true
}

3. 매장별 재고 조회 (위치 기반)

POST https://mapi.daisomall.co.kr/ms/msg/newIntSelStr
Content-Type: application/json

요청 Body:

{
  "keyword": "",
  "pdNo": "1068725",
  "curLttd": 37.5665,      // 위도
  "curLitd": 126.978,      // 경도
  "geolocationAgrYn": "Y",
  "pkupYn": "",
  "intCd": "",
  "pageSize": 30,
  "currentPage": 1
}

요청 예시:

curl 'https://mapi.daisomall.co.kr/ms/msg/newIntSelStr' \
  -H 'Content-Type: application/json' \
  -d '{"keyword":"","pdNo":"1068725","curLttd":37.5665,"curLitd":126.978,"geolocationAgrYn":"Y","pkupYn":"","intCd":"","pageSize":30,"currentPage":1}'

응답 형식: JSON

{
  "data": {
    "msStrVOList": [
      {
        "strCd": "10856",
        "strNm": "서울시청광장점",
        "strAddr": "서울특별시 중구 세종대로 93",
        "strTno": "1522-4400",
        "opngTime": "10:00",
        "clsngTime": "22:00",
        "strLttd": 37.5647,
        "strLitd": 126.9766,
        "km": "0.2",
        "qty": "1",
        "parkYn": "N",
        "usimYn": "Y",
        "pkupYn": "N",
        "taxfYn": "Y"
      }
    ],
    "intStrCont": 33
  },
  "success": true
}

매장 재고 응답 필드

필드 설명
strCd 매장 코드
strNm 매장명
strAddr 주소
strTno 전화번호
opngTime 오픈 시간
clsngTime 마감 시간
strLttd 위도
strLitd 경도
km 거리 (km)
qty 재고 수량
parkYn 주차 가능
usimYn 심카드
pkupYn 매장픽업
taxfYn 택스리펀드
elvtYn 엘리베이터
entrRampYn 경사로
nocashYn 현금없는매장

다이소몰 API 인증 요구사항

항목 필요 여부
세션 쿠키 ❌ 불필요
Authorization 헤더 ❌ 불필요
Content-Type ✅ application/json

재고 조회 구현 예시

interface StoreInventory {
  storeCode: string;
  storeName: string;
  address: string;
  phone: string;
  openTime: string;
  closeTime: string;
  lat: number;
  lng: number;
  distance: string;
  quantity: number;
  options: {
    parking: boolean;
    simCard: boolean;
    pickup: boolean;
    taxFree: boolean;
  };
}

// 온라인 재고 조회
async function getOnlineStock(productNo: string): Promise<number> {
  const response = await fetch('https://mapi.daisomall.co.kr/ms/msg/selOnlStck', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ pdNo: productNo })
  });
  const data = await response.json();
  return data.data?.stck || 0;
}

// 매장별 재고 조회
async function getStoreInventory(
  productNo: string,
  lat: number,
  lng: number
): Promise<StoreInventory[]> {
  const response = await fetch('https://mapi.daisomall.co.kr/ms/msg/newIntSelStr', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      keyword: '',
      pdNo: productNo,
      curLttd: lat,
      curLitd: lng,
      geolocationAgrYn: 'Y',
      pkupYn: '',
      intCd: '',
      pageSize: 30,
      currentPage: 1
    })
  });

  const data = await response.json();
  return data.data?.msStrVOList?.map((store: any) => ({
    storeCode: store.strCd,
    storeName: store.strNm,
    address: store.strAddr,
    phone: store.strTno,
    openTime: store.opngTime,
    closeTime: store.clsngTime,
    lat: store.strLttd,
    lng: store.strLitd,
    distance: store.km,
    quantity: parseInt(store.qty) || 0,
    options: {
      parking: store.parkYn === 'Y',
      simCard: store.usimYn === 'Y',
      pickup: store.pkupYn === 'Y',
      taxFree: store.taxfYn === 'Y'
    }
  })) || [];
}

테스트 결과 요약

테스트 결과
쿠키 없이 매장 검색 ✅ 성공
헤더 없이 매장 검색 ✅ 성공
쿠키 없이 시/도 API ✅ 성공
쿠키 없이 구/군 API ✅ 성공
curl로 리플레이 ✅ 성공
온라인 재고 조회 ✅ 성공
매장별 재고 조회 ✅ 성공

다음 단계

  1. ✅ 매장 검색 API 분석 완료
  2. ✅ 재고 조회 API 분석 완료
  3. ⏳ HTML 파싱 유틸리티 구현
  4. ⏳ MCP 도구 통합
  5. ⏳ 테스트 작성
docs/daiso-playwright-network-analysis.md 5,885 bytes

Playwright를 이용한 다이소 네트워크 분석 계획

목표

Playwright를 사용하여 실제 브라우저에서 다이소 페이지를 열고, 네트워크 요청을 캡처하여 리플레이 세션 가능 여부를 판단합니다.


Playwright MCP 설정 완료

파일 생성

  • .mcp.json - Playwright MCP 서버 설정
  • .claude/settings.local.json - enableAllProjectMcpServers: true 추가

활성화 방법

Claude Code 세션을 종료하고 다시 시작하면 Playwright 도구를 사용할 수 있습니다.


분석 계획

1단계: 페이지 방문 및 세션 획득

Playwright로 다이소 매장 검색 페이지 열기:
→ https://www.daiso.co.kr/cs/shop

목적:
- 페이지 로드 시 설정되는 쿠키 확인
- 초기 JavaScript 실행 확인
- 세션 생성 여부 확인

2단계: 네트워크 요청 캡처 활성화

네트워크 모니터링 시작:
- 모든 XHR/Fetch 요청 기록
- Request Headers (쿠키, Referer 등)
- Request Body (POST 파라미터)
- Response (JSON/HTML 여부)

3단계: 매장 검색 실행

시나리오 A: 이름으로 검색

1. 검색창에 "강남" 입력
2. 검색 버튼 클릭
3. 네트워크 요청 캡처:
   - URL: /cs/ajax/shop_search
   - Method: POST
   - Headers: Cookie, Referer, X-Requested-With
   - Body: name_address=강남
   - Response: HTML 또는 JSON

시나리오 B: 지역 선택

1. 시/도 드롭다운에서 "서울특별시" 선택
2. 네트워크 요청 캡처:
   - URL: /cs/ajax/sido_search
   - Headers 확인
3. 시/군/구 드롭다운에서 "강남구" 선택
4. 네트워크 요청 캡처:
   - URL: /cs/ajax/gugun_search
   - Body: sido=서울특별시

4단계: 요청 정보 분석

확인 사항:

  1. ✅ 쿠키 필요 여부

    • 어떤 쿠키가 설정되는가?
    • PHPSESSID, JSESSIONID 등?
    • 쿠키 없이 요청 가능한가?
  2. ✅ 필수 헤더

  3. ✅ CSRF 토큰

    • 폼에 숨겨진 토큰이 있는가?
    • 헤더에 CSRF 토큰이 있는가?
  4. ✅ 응답 형식

    • JSON인가 HTML인가?
    • 데이터 구조는?

5단계: 리플레이 테스트

캡처한 정보로 curl 재현:

curl "https://www.daiso.co.kr/cs/ajax/shop_search" \
  -X POST \
  -H "Cookie: [캡처한 쿠키]" \
  -H "Referer: https://www.daiso.co.kr/cs/shop" \
  -H "X-Requested-With: XMLHttpRequest" \
  -d "name_address=강남"

성공 판단:

  • ✅ JSON/HTML 응답 받음
  • ✅ 매장 데이터 포함
  • ❌ 로그인 페이지로 리다이렉트
  • ❌ 빈 응답 또는 에러

Playwright 명령 예시

기본 페이지 열기

// 브라우저 실행 및 페이지 열기
await playwright.navigate("https://www.daiso.co.kr/cs/shop");

// 스크린샷 촬영
await playwright.screenshot();

// 쿠키 확인
await playwright.execute(`
  return document.cookie;
`);

네트워크 요청 캡처

// 네트워크 리스너 설정 (JavaScript로)
await playwright.execute(`
  window.capturedRequests = [];

  const originalFetch = window.fetch;
  window.fetch = function(...args) {
    window.capturedRequests.push({
      url: args[0],
      options: args[1],
      timestamp: Date.now()
    });
    return originalFetch.apply(this, args);
  };

  const originalXHR = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function(method, url) {
    window.capturedRequests.push({
      method: method,
      url: url,
      timestamp: Date.now()
    });
    return originalXHR.apply(this, arguments);
  };
`);

폼 입력 및 제출

// 검색어 입력
await playwright.fill('input[name="name_address"]', '강남');

// 검색 버튼 클릭
await playwright.click('button[type="submit"]');

// 잠시 대기 (응답 기다림)
await playwright.execute(`
  return new Promise(resolve => setTimeout(resolve, 2000));
`);

// 캡처된 요청 확인
await playwright.execute(`
  return window.capturedRequests;
`);

예상 결과

시나리오 1: 세션 쿠키 필수 ❌

발견 사항:

  • PHPSESSID 등의 쿠키가 필수
  • 쿠키 없이는 HTML 페이지 반환
  • 리플레이 세션 어려움

대응:

  • 세션 자동 생성 로직 필요
  • Puppeteer/Playwright 필수

시나리오 2: 쿠키 불필요, 헤더만 필요 ✅

발견 사항:

  • X-Requested-With, Referer만 필요
  • JSON 응답 정상 수신
  • 리플레이 세션 가능!

대응:

  • HTTP 클라이언트로 구현 가능
  • Cloudflare Workers에서 작동

시나리오 3: CSRF 토큰 필요 ⚠️

발견 사항:

  • 페이지에서 토큰 추출 필요
  • 토큰 갱신 로직 필요
  • 부분적 리플레이 가능

대응:

  • 먼저 페이지 방문 → 토큰 추출
  • 토큰 포함하여 API 호출

다음 단계

  1. 세션 재시작

    # Claude Code 종료 후 재시작
    # Playwright MCP 도구 활성화 확인
    
  2. Playwright로 페이지 열기

    다이소 매장 검색 페이지 방문
    네트워크 캡처 활성화
    
  3. 검색 실행 및 분석

    매장 검색 수행
    네트워크 요청 분석
    필요한 헤더/쿠키 파악
    
  4. 리플레이 테스트

    curl로 요청 재현
    성공 여부 확인
    
  5. 결과 문서화

    성공 시: HTTP 클라이언트 구현 가능
    실패 시: Puppeteer 필요
    

도구 준비 완료

  • .mcp.json - Playwright MCP 서버 설정
  • .claude/settings.local.json - 자동 승인 활성화
  • ⏳ Claude Code 재시작 필요

다음 명령:
세션 재시작 후 Playwright로 https://www.daiso.co.kr/cs/shop 페이지를 열어 분석을 시작합니다.

docs/emart24-app-scraping-preparation-guide.md 4,560 bytes

이마트24 앱 스크래핑 준비 가이드 (mitmproxy 기반)

작성일: 2026-03-08 (KST)
대상: iOS 이마트24 앱(패키지 정보: kr.co.emart24.everse)

1. 목적

이 문서는 이마트24 앱 트래픽을 실측해, 스크래핑 가능 데이터를 식별하기 위한
"캡처 준비 -> 수집 -> 1차 분석" 절차를 제공합니다.

핵심 산출물:

  • raw.mitm: 원본 mitmproxy 플로우 파일
  • requests.jsonl: 민감정보 마스킹된 요청/응답 레코드
  • summary.json: 시나리오/건수 요약

2. 선행 문서

  • 기본 MITM 세팅: docs/mitmproxy-guide.md
  • 기존 CU 앱 캡처 레퍼런스: docs/cu-app-request-capture-guide.md
  • 이마트24 웹 선행 분석: docs/emart24-network-analysis-result.md

3. 사전 준비

  1. Mac/iPhone 동일 Wi-Fi 연결
  2. iOS 프록시 수동 설정 및 mitm 인증서 신뢰 완료
  3. mitmproxy 설치 확인
  4. 이마트24 앱 최신 버전 설치 및 로그인 상태 준비
  5. 캡처 시나리오 사전 정의

권장 시나리오:

  • 앱 실행
  • 점포 선택 또는 내 주변 매장 진입
  • 상품 검색
  • 예약픽업/오늘픽업/재고 표시 화면 진입
  • 상품 상세에서 수량/재고 관련 텍스트 확인

4. 수집 명령

프로젝트 루트에서 아래를 실행합니다.

mkdir -p captures/emart24-20260308
mitmdump \
  --listen-host 0.0.0.0 \
  --listen-port 8080 \
  -s scripts/mitmproxy/emart24_capture_export.py \
  --set emart24_capture_dir=captures/emart24-20260308 \
  --set emart24_capture_scenario='앱 로그인 후 점포 선택, 상품 검색, 예약픽업 진입' \
  --set emart24_capture_hosts='emart24.co.kr,abr.ge' \
  -w captures/emart24-20260308/raw.mitm

수집 종료는 Ctrl+C로 합니다.

참고:

  • 첫 캡처에서는 호스트 필터를 좁게 시작하고, 누락이 의심되면 확장합니다.
  • 필요 시 --set emart24_capture_hosts='emart24.co.kr,abr.ge,app-measurement.com,googleapis.com'처럼 확대해 재수집합니다.

5. 생성 파일 설명

A. requests.jsonl

  • 한 줄당 1개 요청/응답 레코드(JSON)
  • 포함 정보:
    • 요청: method, host, path, query, headers, body preview
    • 응답: statusCode, headers, body preview
  • 기본 마스킹:
    • 헤더: Authorization, Cookie, Set-Cookie, 토큰 계열
    • 쿼리: token, password, session, jwt 계열

B. summary.json

  • 시나리오명
  • 대상 호스트 목록
  • 전체/매칭/스킵 건수
  • 산출물 경로

C. raw.mitm

  • 원본 플로우 파일
  • 필요 시 mitmweb -r captures/emart24-20260308/raw.mitm로 재열람

6. 1차 분석 명령

# 총 캡처 건수
wc -l captures/emart24-20260308/requests.jsonl

# 호스트 분포
jq -r '.request.host' captures/emart24-20260308/requests.jsonl | sort | uniq -c | sort -nr

# 경로 분포 상위
jq -r '.request.path' captures/emart24-20260308/requests.jsonl | sort | uniq -c | sort -nr | head -30

# 재고/매장/상품 관련 후보 빠르게 확인
rg -n 'stock|inventory|goods|product|pickup|store|매장|재고|상품' captures/emart24-20260308/requests.jsonl

# 상태코드 분포
jq -r '.response.statusCode // "NA"' captures/emart24-20260308/requests.jsonl | sort | uniq -c

7. 스크래핑 가능 데이터 판정 기준

A. 즉시 구현 가능

  • 비인증 또는 약한 인증으로 재현 가능한 매장/상품 API 식별
  • 요청 최소조건(헤더/파라미터) 확인 완료
  • 응답에 안정적인 식별 필드 존재

B. 조건부 구현 가능

  • 앱 토큰/세션 필요하지만 재현 가능
  • 주기 갱신 토큰 처리 전략 필요

C. 보류

  • certificate pinning 또는 강한 기기결합으로 복호화/재현 불가
  • 응답이 암호화되어 의미 필드 해석 불가

8. 실패 시 점검

  1. HTTPS 요청이 거의 안 보임
  • iOS 인증서 신뢰 설정 재확인
  • 프록시 IP/포트 오타 확인
  1. 앱 핵심 요청만 누락
  • certificate pinning 가능성 점검
  • Android 동일 시나리오 비교 캡처
  1. 요청은 보이는데 의미 있는 데이터가 없음
  • 시나리오를 더 구체화해 재수집(상품 상세, 장바구니, 픽업 확정 직전 단계)

9. 후속 문서화 규칙

실측 후 아래 순서로 문서를 업데이트합니다.

  1. docs/emart24-network-analysis-result.md에 "앱 실측 섹션" 추가
  2. 재현 가능한 엔드포인트를 curl 예시와 함께 기록
  3. 구현 판정을 A/B/C로 업데이트
  4. 필요 시 docs/emart24-app-scraping-replay-guide.md 신규 작성
docs/emart24-app-scraping-replay-guide.md 4,253 bytes

이마트24 앱 스크래핑 재현 가이드 (실측 기반)

작성일: 2026-03-08 (KST)
기준 캡처: captures/emart24-20260308/requests.jsonl

1. 목적

이 문서는 이마트24 앱 실측 트래픽을 바탕으로,
상품 검색과 매장별 재고 조회를 curl로 재현하는 최소 절차를 제공합니다.

2. 핵심 엔드포인트

  • 상품 검색: POST /stock/stock/search
  • 상품별 재고 조회 준비: POST /api/stock/v1/search/keyword
  • 상품 기본 재고 메타: GET /api/stock/v1/stock/goods/{pluCd}
  • 매장별 재고 수량: GET /api/stock/v2/stock-search/store
  • 매장 상세: GET /api/stock/stock/store/{bizNo}

베이스 URL:

  • https://everse.emart24.co.kr

3. 공통 요청 조건

실측 기준 최소 헤더:

  • x-requested-with: XMLHttpRequest

폼 요청(POST) 추가:

  • content-type: application/x-www-form-urlencoded; charset=UTF-8

4. 재현 명령

A. 상품 검색

curl -sS 'https://everse.emart24.co.kr/stock/stock/search' \
  -H 'content-type: application/x-www-form-urlencoded; charset=UTF-8' \
  -H 'x-requested-with: XMLHttpRequest' \
  --data 'currentPage=1&pageCnt=10&sortType=SALE&saleProductYn=N&searchWord=%EB%91%90%EB%B0%94%EC%9D%B4' \
  | jq '{
      totalCnt,
      first: .productList[0] | {pluCd,goodsNm,originPrice,viewPrice}
    }'

기대 결과:

  • totalCnt 양수
  • first.pluCd, first.goodsNm 존재

B. 상품별 재고 조회 준비 호출

curl -sS 'https://everse.emart24.co.kr/api/stock/v1/search/keyword' \
  -H 'content-type: application/x-www-form-urlencoded; charset=UTF-8' \
  -H 'x-requested-with: XMLHttpRequest' \
  --data 'pluCd=8800244010504&searchPage=STOCK_SEARCH_SERVICE' \
  -D - -o /tmp/emart24-v1-search-keyword.out | head -20

기대 결과:

  • HTTP 200
  • 본문 길이 0 (Content-Length: 0)일 수 있음

C. 상품 기본 재고 메타

curl -sS 'https://everse.emart24.co.kr/api/stock/v1/stock/goods/8800244010504' \
  -H 'x-requested-with: XMLHttpRequest' \
  | jq '{msg, overCount, returnUrl}'

기대 결과:

  • msg: "success"

D. 매장별 재고 수량 조회

curl -sS 'https://everse.emart24.co.kr/api/stock/v2/stock-search/store?searchPluCode=8800244010504&bizNoArr=28339%2C05015%2C23233%2C29512%2C24437%2C29109%2C26137%2C29796%2C30200%2C28057%2C27928%2C29570%2C30162%2C28187%2C22579%2C00367%2C28967%2C00737' \
  -H 'x-requested-with: XMLHttpRequest' \
  | jq '{
      goods: .storeGoodsInfo | {pluCd,goodsNm,viewPrice},
      qtyCount: (.storeGoodsQty | length),
      qtySample: .storeGoodsQty[0]
    }'

기대 결과:

  • storeGoodsQty 배열 존재
  • qtySample.BIZNO, qtySample.BIZQTY 확인 가능

E. 매장 상세 조회

curl -sS 'https://everse.emart24.co.kr/api/stock/stock/store/28339' \
  -H 'x-requested-with: XMLHttpRequest' \
  | jq '{
      storeNm: .storeInfo.storeNm,
      tel: .storeInfo.tel,
      storeAddr: .storeInfo.storeAddr,
      svr24: .storeInfo.svr24
    }'

기대 결과:

  • storeNm, storeAddr 존재

5. 캡처 기반 분석 명령

# 총 요청 수
wc -l captures/emart24-20260308/requests.jsonl

# 상태 코드 분포
jq -r '.response.statusCode // "NA"' captures/emart24-20260308/requests.jsonl | sort | uniq -c

# 핵심 스톡 엔드포인트 추출
jq -r 'select(.request.path|test("^/api/stock|^/stock/stock/search")) | [.request.method,.request.path] | @tsv' \
  captures/emart24-20260308/requests.jsonl | sort | uniq -c | sort -nr

6. 구현 매핑 제안

  • emart24_search_products

    • 소스: POST /stock/stock/search
    • 출력: pluCd, goodsNm, originPrice/viewPrice
  • emart24_check_inventory

    • 소스:
      • GET /api/stock/v2/stock-search/store (매장별 수량)
      • GET /api/stock/stock/store/{bizNo} (매장 상세)
    • 조합 키: BIZNO(매장), PLU_CD(상품)

7. 실패 대응

  1. 403/401 발생
  • 세션 의존 요청일 수 있으므로 앱 최신 캡처 기준으로 헤더/쿠키 재검증
  1. storeGoodsQty 비어 있음
  • searchPluCodebizNoArr 조합을 캡처값으로 다시 맞춰 재시도
  1. DNS/네트워크 실패
  • 실행 환경 네트워크 정책(샌드박스/사내망) 확인
docs/emart24-network-analysis-result.md 5,607 bytes

이마트24 네트워크 분석 결과 (실측 기반)

작성일: 2026-03-03 (KST)
실측 도구: curl, 정적 JS 분석
대상:

  • https://emart24.co.kr/store
  • https://emart24.co.kr/libs/FindStore.js
  • https://emart24.co.kr/api1/area
  • https://emart24.co.kr/api1/store
  • https://emart24.co.kr/api1/goods
  • https://emart24.co.kr/service/app

결론 요약

  • 주변 매장 조회: 가능 (웹 API 실측 성공)
  • 재고 조회(매장 단위 수량): 웹 기준 미확인
  • 구현 판정:
    • emart24_find_nearby_stores는 즉시 구현 가능
    • emart24_check_inventory는 앱 트래픽 실측 전까지 보류

현재 프로젝트(daiso, oliveyoung 플러그인 구조) 기준으로는
emart24 서비스 프로바이더 추가 시 매장 기능은 바로 확장 가능하지만,
재고는 근거 API가 없어 "정확한 재고 도구"로 제공하기 어렵습니다.

1) 매장 조회 API 실측 결과

/store 페이지와 FindStore.js에서 아래 호출을 확인했습니다.

A. 시/도/구군 조회

  • Endpoint: GET /api1/area
  • JS 근거:
    • FindStore.js: _apiAct.area = _api.httpGetFunc('area')
    • getGunguInfo(payload)에서 AREA_SEQ 전달
  • 실측:
    • GET /api1/area -> error:0, count:17 (시/도 목록)
    • GET /api1/area?AREA_SEQ=11 -> error:0, count:25 (서울 구/군 목록)

B. 매장 목록 조회

  • Endpoint: GET /api1/store
  • JS 근거:
    • FindStore.js: _apiAct.store = _api.httpGetFunc('store')
    • displayPlaces()에서 page/search/AREA1/AREA2/SVR_* 등 전송
  • 실측:
    • GET /api1/store?page=1 -> error:0, count:5669
    • GET /api1/store?page=1&search=강남 -> error:0, count:71
    • GET /api1/store?page=1&search=강남&SVR_24=1 -> error:0, count:12
    • GET /api1/store?page=1&AREA1=서울특별시&AREA2=강남구 -> error:0, count:58

2) 응답 필드 (실측)

/api1/store 응답 data[]에서 확인한 주요 필드:

  • 점포 식별: CODE, TITLE
  • 위치/주소: LATITUDE, LONGITUDE, ADDRESS, ADDRESS_DE
  • 연락처: PHONE
  • 운영정보: START_HHMM, END_HHMM, OPEN_DATE, END_DATE
  • 서비스 플래그:
    • SVR_24, SVR_AUTO, SVR_PARCEL, SVR_ATM, SVR_WINE,
    • SVR_COFFEE, SVR_SMOOTH, SVR_APPLE, SVR_TOTO,
    • SVR_PICKUP, NBR_LICS_YN, USE_YN
  • 응답 메타: error, count, data

거리 계산에 필요한 위경도(LATITUDE, LONGITUDE)가 포함되어
find_nearby_stores 구현에 충분합니다.

3) 요청 파라미터 동작 메모

FindStore.jsreqData 기준 주요 파라미터:

  • 기본: page, search, AREA1, AREA2
  • 서비스 필터: SVR_24, SVR_AUTO, SVR_PARCEL, SVR_ATM, SVR_WINE,
    SVR_COFFEE, SVR_SMOOTH, SVR_APPLE, SVR_TOTO, NBR_LICS_YN, USE_YN
  • 지도 경계: top, bottom, left, right

실측 참고:

  • top/bottom/left/right만 전달한 샘플 요청은 {"error":1,"count":0} 반환.
  • 따라서 초기에 MCP 구현은 search/area/service 중심으로 시작하고,
    경계 파라미터는 브라우저 동작과 동일한 조건에서 추가 검증이 필요합니다.

4) 재고 조회 실측 결과

웹 채널에서 확인한 상품 API

  • Endpoint: GET /api1/goods
  • 근거:
    • libs/home.jsloadGoods()에서 url: "/api1/goods"
  • 실측:
    • GET /api1/goods -> error:0, count:2862
    • GET /api1/goods?page=1&search=도시락 -> error:0, count:13
    • GET /api1/goods?category_seq=1&page=1 -> error:0, count:2862

응답 필드 예시:

  • CODE, TITLE, CATEGORY, KIND, PRICE_REAL, PRICE_ORIGIN,
    POST_START, POST_FINISH, BASE_CATEGORY_SEQ, CATEGORY_SEQ

관측되지 않은 필드:

  • 매장코드 연계 재고(storeCode, inventoryQty, remainQty 등)

앱 전용 기능 정황

  • https://emart24.co.kr/service/app예약픽업, 오늘 픽업 중심 안내 페이지
  • 앱 다운로드 단축 링크(https://abr.ge/0gnlrx) 메타에 앱 정보 노출:
    • iOS app_store_id=1636816705
    • Android package kr.co.emart24.everse

판정:

  • 매장 단위 재고는 웹 공개 API에서 확인되지 않음
  • 재고는 앱 채널(인증/디바이스 컨텍스트) 기능일 가능성이 높음

5) 현재 프로젝트 적용 판정

즉시 가능 (A)

  • emart24_find_nearby_stores
    • 데이터 소스: /api1/area, /api1/store
    • 구현 방식:
      • 지역/키워드/서비스 필터로 매장 조회
      • LATITUDE/LONGITUDE 기반 거리 계산 후 근접순 정렬
    • 기존 oliveyoung_find_nearby_stores와 유사 패턴으로 구현 가능

조건부/보류 (C)

  • emart24_check_inventory
    • 현재 웹 실측 근거만으로는 "매장 재고 확인" 정의를 충족하지 못함
    • /api1/goods는 상품 카탈로그 성격이며 매장별 재고 필드가 없음

6) 구현 권장안 (단계형)

  1. 1단계: emart24_find_nearby_stores 먼저 출시
  2. 2단계: emart24_search_products(카탈로그 조회) 별도 도구로 분리
  3. 3단계: 앱 트래픽 실측 후 emart24_check_inventory 구현 여부 재판정

주의:

  • 2단계 도구를 "재고"로 표기하면 오해 소지가 있으므로
    명확히 "상품 목록/행사 정보"로 구분하는 것이 안전합니다.

7) 다음 실측 작업

  1. Android/iOS emart24 앱에서 예약픽업/재고조회 시나리오 캡처
  2. 앱 API 엔드포인트, 인증 헤더/토큰, 필수 파라미터 확인
  3. 비로그인/로그인 상태별 재현성 비교
  4. Cloudflare Worker 환경에서 재현 가능성(A/B/C) 재평가
docs/gs25-final-replay-methodology.md 9,970 bytes

GS25 API 리플레이 방법론 (최종)

작성일: 2026-03-13

개요

GS25 우리동네GS 앱의 재고 조회 API를 리플레이하기까지의 전체 과정을 정리합니다.


1. 문제 정의

목표

  • GS25 매장별 재고 조회 API 확보
  • 외부에서 재현 가능한 HTTP 요청 구성

초기 장애물

  • 앱이 Flutter 기반으로 구축됨
  • 요청/응답이 앱 레이어에서 암호화됨
  • Certificate Pinning으로 MITM 캡처 실패
  • b2c-apigw.woodongs.com, b2c-bff.woodongs.com 도메인이 TLS SNI에서만 확인되고 평문 미확보

2. 접근 방법

Phase 1: 네트워크 캡처 시도 (실패)

mitmproxy + Android 프록시 설정
→ 결과: Certificate Pinning으로 인해 b2c 도메인 평문 미확보
→ 관측된 것: tms31.gsshop.com/msg-api/* (암호화된 페이로드)

Phase 2: Frida SSL 우회 시도 (부분 성공)

Frida + SSL Pinning 우회 스크립트
→ 결과: 일부 트래픽 복호화 성공, 그러나 b2c 도메인은 여전히 미확보
→ 원인: Flutter가 자체 TLS 스택(BoringSSL) 사용

Phase 3: 정적 분석 - blutter (핵심 돌파구)

blutter로 libapp.so (Flutter AOT) 분석
→ 발견: ApiResponseEncryptionUtility 클래스
→ 발견: Encrypter::encrypt, decrypt64 함수 오프셋
→ 발견: /api/bff/v2/store/stock 등 API 경로 문자열

Phase 4: Frida 런타임 후킹 (성공)

blutter 오프셋 기반 Frida 스크립트 작성
→ _encrypt (0xa98420), _decrypt (0xb07064) 후킹
→ 결과: 암복호화 전후 평문 캡처 성공
→ 획득: JWT 토큰, device-id, API 요청 파라미터

Phase 5: API 리플레이 (성공)

캡처된 인증 정보로 curl 요청 구성
→ 결과: b2c-bff.woodongs.com API 응답 성공

3. 핵심 도구 및 기술

3.1 blutter (Flutter AOT 분석)

# libapp.so에서 Dart 심볼 추출
blutter libapp.so blutter-out-gs25

발견한 핵심 정보:

  • 암복호화 함수 오프셋
  • API 엔드포인트 경로
  • 요청/응답 모델 구조

3.2 Frida 후킹 스크립트

후킹 지점 (libapp.so 오프셋):

함수 오프셋 용도
_encrypt 0xa98420 요청 암호화
_decrypt 0xb07064 응답 복호화
Encrypter::encrypt 0xa984c4 실제 암호화
Encrypter::decrypt64 0xa9b50c Base64 복호화

스크립트 위치: scripts/frida/gs25-blutter-encrypter-hook.ts

3.3 실행 방법

# 1. frida-server 실행 (Android)
adb shell su -c '/data/local/tmp/frida-server &'

# 2. adb forward 설정
adb forward tcp:27042 tcp:27042

# 3. 앱 PID 확인
adb shell pidof com.gsr.gs25

# 4. Frida 스크립트 attach
frida -H 127.0.0.1:27042 -p {PID} -l scripts/frida/gs25-blutter-encrypter-hook.ts

# 5. 앱에서 재고찾기 수행 → 로그에 평문 캡처됨

4. 확보된 API 정보

Base URL

https://b2c-bff.woodongs.com

인증 요구사항 (중요 발견)

재고 조회 API는 인증이 필요 없습니다!

앱 내부에서 암호화/인증을 수행하지만, 실제 서버는 인증 없이 호출 가능합니다.
이는 앱 레이어의 암호화가 난독화 목적임을 의미합니다.

API 인증 필요 비고
/api/bff/v2/store/stock ❌ 불필요 재고 조회
/api/bff/v2/store/detail ❌ 불필요 매장 상세
/api/alive ❌ 불필요 헬스체크
/api/bff/v1/myRefrigerator ✅ 필요 내 냉장고 (401 반환)

확인된 API 엔드포인트

엔드포인트 메서드 용도 인증
/api/bff/v2/store/stock GET 매장별 재고 조회 불필요
/api/bff/v2/store/detail GET 매장 상세 정보 불필요
/api/bff/v1/myRefrigerator GET 내 냉장고 (프로모션) 필요
/api/bff/v1/store GET 매장 검색 미확인

5. 리플레이 예시

상품 검색 → itemCode 획득 (필수 선행 단계)

# 1단계: totalSearch API로 키워드 → itemCode 변환
curl -s -X POST "https://b2c-apigw.woodongs.com/search/v3/totalSearch" \
  -H "Content-Type: application/json" \
  -d '{"query":"핫식스"}'

응답에서 itemCode 추출:

{
  "SearchQueryResult": {
    "Collection": [
      {
        "Documentset": {
          "Document": [
            {
              "field": {
                "itemCode": "8801056249212",
                "itemName": "롯데)핫식스더킹애플홀릭355ML"
              }
            }
          ]
        }
      }
    ]
  }
}

매장 재고 조회 (itemCode + 좌표 필수!)

# 2단계: itemCode + 좌표로 재고 조회
curl -s "https://b2c-bff.woodongs.com/api/bff/v2/store/stock?serviceCode=01&itemCode=8801056249212&XCoordination=127.0276&YCoordination=37.4979&pageNumber=0&pageCount=100&realTimeStockYn=Y"

중요: keyword 파라미터가 아닌 itemCode + 좌표가 필요합니다!

파라미터:

파라미터 설명 필수 예시
serviceCode 서비스 코드 (01=GS25) O 01
itemCode 상품 코드 (totalSearch에서 획득) O 8801056249212
XCoordination 경도 O 127.0276
YCoordination 위도 O 37.4979
realTimeStockYn 실시간 재고 여부 O Y
pageNumber 페이지 번호 - 0
pageCount 페이지당 항목 수 - 100

응답:

{
  "stores": [{
    "storeCode": "VY814",
    "storeName": "백령북포점",
    "storeAddress": "인천 옹진군 백령면 당후길 7",
    "storeXCoordination": "124.66430616954244",
    "storeYCoordination": "37.96076745609878",
    "realStockQuantity": "0",
    "propertyList": [...]
  }]
}

매장 상세 조회 (인증 불필요)

curl -s "https://b2c-bff.woodongs.com/api/bff/v2/store/detail?storeCode=VE463&serviceCode=01"

응답: 매장 속성 (상비의약품, 현금인출기, 와인25 등)


6. 핵심 교훈

성공 요인

  1. 정적 분석 우선: 네트워크 캡처 실패 시 앱 바이너리 분석으로 전환
  2. blutter 활용: Flutter AOT 특화 도구로 Dart 심볼/오프셋 추출
  3. 앱 레이어 후킹: TLS가 아닌 암복호화 함수 직접 후킹
  4. 오프셋 기반 접근: 클래스명이 아닌 메모리 오프셋으로 정확한 후킹
  5. 인증 검증: 캡처된 인증 정보 없이도 API 호출 테스트 → 인증 불필요 발견

주요 발견

앱 암호화 ≠ 서버 인증

앱 내부에서 요청/응답을 암호화하지만, 실제 서버는 인증 없이 호출 가능합니다.
이는 앱 레이어 암호화가 난독화/리버스 엔지니어링 방지 목적임을 의미합니다.

재고 조회는 2단계 프로세스

  1. totalSearch API: 키워드 → itemCode 변환
  2. store/stock API: itemCode + 좌표 → 재고 정보

단순히 keyword 파라미터만 사용하면 결과가 0개로 나옵니다.
반드시 itemCode + XCoordination + YCoordination 조합이 필요합니다.

실패 원인 분석

시도 실패 원인
MITM Certificate Pinning
SSL 우회 Flutter 자체 TLS 스택
Java 후킹 Flutter는 Dart VM 사용

7. 관련 파일

docs/
├── gs25-final-replay-methodology.md       # 본 문서 (최종)
└── archive/                               # 중간 분석 기록 보관
    ├── gs25-network-analysis-result.md    # 네트워크 분석 기록
    ├── gs25-android-bypass-capture-guide.md # Frida 우회 가이드
    ├── gs25-new-blutter-analysis.md       # blutter 분석 결과
    ├── gs25-new-blutter-signal-summary.md # blutter 신호 요약
    └── ...                                # 기타 중간 문서들

scripts/frida/
└── gs25-blutter-encrypter-hook.js         # 최종 후킹 스크립트

tmp/gs25-static/
└── blutter-out-gs25/                      # blutter 산출물

8. 추가 발견 (2026-03-14)

재고 API 좌표 파라미터 수정

문제: 문서화된 파라미터로 API 호출 시 수도권 매장 재고가 모두 0으로 반환

원인: 앱이 사용하는 실제 파라미터명이 다름

잘못된 파라미터 올바른 파라미터
XCoordination myPositionXCoordination + centerPositionXCoordination
YCoordination myPositionYCoordination + centerPositionYCoordination
(없음) radiusCondition=500

수정된 API 호출 예시:

curl -s "https://b2c-bff.woodongs.com/api/bff/v2/store/stock?serviceCode=01&itemCode=8801056038861&myPositionXCoordination=126.841342&myPositionYCoordination=37.317730&centerPositionXCoordination=126.841342&centerPositionYCoordination=37.317730&radiusCondition=500&pickupStoreYn=N&realTimeStockYn=Y"

발견 방법: Frida v4 스크립트로 앱의 실제 요청 파라미터 평문 캡처

  • 상세 가이드: docs/gs25-frida-plaintext-capture-guide.md

9. 완료된 항목

  1. 토큰 갱신: 재고 조회 API는 인증 불필요로 확인됨 ✅
  2. MCP 도구 구현: gs25_check_inventory 구현 완료 ✅
  3. 좌표 파라미터 수정: 올바른 파라미터명으로 수정 ✅
docs/gs25-frida-plaintext-capture-guide.md 4,526 bytes

GS25 Frida 평문 캡처 가이드

작성일: 2026-03-14

개요

Flutter 기반 GS25 앱(우리동네GS)의 암호화된 API 요청/응답을 평문으로 캡처하는 방법을 설명합니다.

스크립트 위치

scripts/frida/gs25-blutter-encrypter-hook.ts (v4)

주요 기능

1. 암호화 전 평문 입력 캡처 (핵심!)

_encrypt 함수의 onEnter에서 암호화되기 전 평문 요청 데이터를 캡처합니다.

Interceptor.attach(_encrypt_addr, {
  onEnter: function (args) {
    // args[0] ~ args[5] 탐색하여 평문 추출
    for (let i = 0; i < 6; i++) {
      const plaintext = readLargeData(args[i], 8192);
      if (plaintext && plaintext.includes('{')) {
        jsonLog({ t: 'ENCRYPT_INPUT', argIndex: i, plaintext });
      }
    }
  },
});

2. 복호화 후 평문 응답 캡처

_decrypt 함수의 onLeave에서 서버 응답 평문을 캡처합니다.

3. 후킹 지점 (libapp.so 오프셋)

함수 오프셋 용도
_encrypt 0xa98420 요청 암호화 (onEnter로 평문 캡처)
encrypter_encrypt 0xa984c4 실제 암호화 함수
_decrypt 0xb07064 응답 복호화
encrypter_decrypt64 0xa9b50c Base64 복호화

실행 방법

1. 사전 준비

  • 루팅된 Android 기기
  • frida-server 설치 (/data/local/tmp/frida-server)
  • GS25 앱 설치

2. frida-server 시작

adb shell su -c '/data/local/tmp/frida-server &'
adb forward tcp:27042 tcp:27042

3. 앱 PID 확인

adb shell pidof com.gsr.gs25

4. Frida 스크립트 실행

# 실시간 로그 확인
frida -H 127.0.0.1:27042 -p {PID} -l scripts/frida/gs25-blutter-encrypter-hook.ts

# 파일로 저장
frida -H 127.0.0.1:27042 -p {PID} -l scripts/frida/gs25-blutter-encrypter-hook.ts 2>&1 | tee captures/frida-capture.log

5. 앱에서 원하는 기능 수행

앱에서 재고찾기 등 원하는 기능을 수행하면 로그에 평문이 캡처됩니다.

캡처되는 로그 형식

요청 평문 (ENCRYPT_INPUT)

{
  "t": "ENCRYPT_INPUT",
  "argIndex": 2,
  "plaintext": "{\"headers\":{...},\"queryParameters\":{\"itemCode\":\"8801056038861\",...},\"data\":null}",
  "ts": 1773452253281
}

응답 평문 (DECRYPT_RESPONSE)

{
  "t": "DECRYPT_RESPONSE",
  "data": "{\"statusCode\":200,\"data\":{\"stores\":[{\"storeCode\":\"VE463\",\"realStockQuantity\":\"15\",...}]}}",
  "ts": 1773452253291
}

실제 발견 사례

2026-03-14: 재고 API 파라미터 불일치 발견

문제: API 호출 시 수도권 매장 재고가 모두 0으로 반환됨

원인: 잘못된 좌표 파라미터명 사용

잘못된 파라미터 올바른 파라미터
XCoordination myPositionXCoordination + centerPositionXCoordination
YCoordination myPositionYCoordination + centerPositionYCoordination
(없음) radiusCondition=500

발견 방법: Frida v4 스크립트로 앱의 실제 요청 파라미터 캡처

{
  "queryParameters": {
    "myPositionXCoordination": "126.841342",
    "myPositionYCoordination": "37.317730",
    "centerPositionXCoordination": "126.841342",
    "centerPositionYCoordination": "37.317730",
    "radiusCondition": 500,
    "serviceCode": "01",
    "itemCode": "8801056038861",
    "pickupStoreYn": "N",
    "realTimeStockYn": "Y"
  }
}

한글 인코딩 문제

캡처된 평문에서 한글이 깨져 보일 수 있습니다 (예: HÅÀüÈ@ÇÈ = 안산주은점).
이는 Frida의 메모리 읽기 방식 때문이며, JSON 구조와 영문/숫자 데이터는 정상적으로 캡처됩니다.

주의사항

  1. 오프셋 변경: 앱 업데이트 시 libapp.so 오프셋이 변경될 수 있음. blutter로 재분석 필요.
  2. 앱 버전: 테스트된 버전 - 5.3.35 (build 1854)
  3. 기기: SM-F926N (Android 15)

관련 파일

  • scripts/frida/gs25-blutter-encrypter-hook.ts - Frida 후킹 스크립트
  • docs/gs25-final-replay-methodology.md - API 리플레이 전체 방법론
  • tmp/gs25-static/blutter-out-gs25/ - blutter 분석 산출물
docs/lottecinema-network-analysis-result.md 13,766 bytes

롯데시네마 네트워크 분석 결과 (실측 기반)

작성일: 2026-03-10 (KST)
실측 도구: Playwright MCP
대상:

  • https://www.lottecinema.co.kr/NLCHS/Ticketing
  • https://www.lottecinema.co.kr/LCWS/Ticketing/TicketingData.aspx
  • https://www.lottecinema.co.kr/LCWS/Common/MainData.aspx

결론 요약

  • Playwright 없이 터미널 리플레이: 성공
    • curl --form-string 'paramList=...'만으로 핵심 API 재현 성공
    • 별도 로그인, 쿠키, 서명 헤더 없이 응답 수신 확인
  • 근처 영화관 조회: 가능
    • GetTicketingPageTOBE 응답에 극장별 Latitude, Longitude, CinemaAddrSummary가 포함됨
  • 상영중 영화/상영 회차 목록 조회: 가능
    • GetTicketingPageTOBE에서 전체 영화/극장 기본 목록 확보
    • GetPlaySequence에서 날짜/극장/영화 조합별 회차 실데이터 확보
  • 남은 좌석 수 조회: 가능
    • GetPlaySequenceTotalSeatCount, BookingSeatCount로 계산 가능
    • 실측 기준 remainingSeats = TotalSeatCount - BookingSeatCount
  • 좌석 맵 상세 조회: 조건부 가능
    • GetSeats로 좌석 맵은 조회되나, BookingSeats/ScreenSeatInfo.BookingCount
      GetPlaySequence.BookingSeatCount의 의미 차이가 있어
      "잔여 좌석 수 소스"로는 즉시 채택하지 않는 편이 안전함

1) Playwright 기준 동작 방식 이해

A. 진입 구조

  • 예매 화면은 별도 iframe 없이 /NLCHS/Ticketing 단일 페이지에서 동작
  • 프론트 번들:
    • Scripts/Dist/TicketingIndex.bundle.js
    • Scripts/common/Common.js
  • 공통 URL 매핑 함수:
    • GetLcwsUrls('ticket') -> /LCWS/Ticketing/TicketingData.aspx
    • GetLcwsUrls('main') -> /LCWS/Common/MainData.aspx

B. 공통 요청 규칙

  • 대부분 POST /LCWS/Ticketing/TicketingData.aspx
  • body 형식:
    • FormData
    • 키: paramList
    • 값: JSON 문자열
  • 공통 필드:
    • MethodName
    • channelType: "HO"
    • osType: "W"
    • osVersion: navigator.userAgent

C. 브라우저 클릭 시 실제 호출 순서

  1. 예매 페이지 최초 로딩
    -> GetTicketingPageTOBE

  2. 극장 선택
    -> GetInvisibleMoviePlayInfo
    -> GetPlaySequence
    -> GetPopupMessageOnLine

  3. 영화 선택 후 재조회
    -> GetInvisibleMoviePlayInfo
    -> GetPlaySequence

  4. 특정 회차 선택 후 좌석 단계 진입
    -> GetSeats

D. 브라우저 없는 리플레이 검증 결과

  • 검증일: 2026-03-10 (KST)
  • 도구: curl
  • 전송 형식:
    • POST https://www.lottecinema.co.kr/LCWS/Ticketing/TicketingData.aspx
    • --form-string 'paramList=<JSON>'
    • User-Agent만 브라우저 계열로 지정

성공 확인 API

  • GetTicketingPageTOBE
    • 결과: IsOK=true, ResultMessage=SUCCESS
  • GetPlaySequence
    • 결과: IsOK=true, ResultMessage=SUCCESS
    • 조건:
      • playDate=2026-03-10
      • cinemaID=1|0001|1016
      • representationMovieCode=23816
  • GetSeats
    • 결과: IsOK=true, ResultMessage=SUCCESS
    • 조건:
      • cinemaId=1016
      • screenId=1201
      • playDate=2026-03-10
      • playSequence=1
      • screenDivisionCode=300

현재 판정

  • 롯데시네마 LCWS는 현재 시점에서 브라우저 세션 의존성이 낮음
  • 따라서 서버 구현은 Playwright fallback 없이도
    일반 fetch 기반 클라이언트로 바로 진행 가능

2) 핵심 엔드포인트 실측

2.1 POST /LCWS/Ticketing/TicketingData.aspx + MethodName=GetTicketingPageTOBE

  • 목적:
    • 예매 초기 화면 구성 데이터 일괄 조회
    • 날짜 목록, 지역 분류, 극장 목록, 영화 목록 동시 수신
  • 요청 예시:
{
  "MethodName": "GetTicketingPageTOBE",
  "channelType": "HO",
  "osType": "W",
  "osVersion": "<user-agent>",
  "memberOnNo": "0"
}
  • 주요 응답 루트:
    • MoviePlayDates.Items.Items[]
    • CinemaDivison.AreaDivisions.Items[]
    • CinemaDivison.SpecialTypeDivisions.Items[]
    • Cinemas.Cinemas.Items[]
    • Movies.Movies.Items[]

실측 확인 필드

  • 날짜:
    • PlayDate, IsPlayDate, DayOfWeekKR
  • 지역:
    • DivisionCode, DetailDivisionCode, GroupNameKR, CinemaCount
  • 극장:
    • CinemaID, CinemaNameKR, Latitude, Longitude, CinemaAddrSummary
  • 영화:
    • RepresentationMovieCode, MovieNameKR, ViewGradeNameKR, PlayTime, ReleaseDate

2026-03-10 실측 샘플

  • 지역 샘플:
    • DetailDivisionCode=0001, GroupNameKR=서울, CinemaCount=23
  • 극장 샘플:
    • CinemaID=1016, CinemaNameKR=월드타워, Latitude=37.5132941, Longitude=127.104215
  • 영화 샘플:
    • RepresentationMovieCode=23816, MovieNameKR=왕과 사는 남자
    • RepresentationMovieCode=23873, MovieNameKR=호퍼스
    • RepresentationMovieCode=24024, MovieNameKR=굿 윌 헌팅

2.2 POST /LCWS/Ticketing/TicketingData.aspx + MethodName=GetPlaySequence

  • 목적:

    • 날짜/극장/영화 조건별 상영 회차 조회
    • 회차별 시작/종료 시각, 상영관, 전체 좌석, 예매 좌석 수 제공
  • 가장 중요한 포인트:

    • cinemaID는 숫자형 극장 ID 단독이 아니라
      divisionCode|detailDivisionCode|cinemaID 복합 문자열이어야 정상 동작
    • 예: 1|0001|1016
  • 요청 예시:

{
  "MethodName": "GetPlaySequence",
  "channelType": "HO",
  "osType": "W",
  "osVersion": "<user-agent>",
  "playDate": "2026-03-10",
  "cinemaID": "1|0001|1016",
  "representationMovieCode": "23816"
}
  • 응답 루트:
    • PlaySeqsHeader.Items[]
    • PlaySeqs.Items[]

주요 응답 필드

  • 헤더/그룹:
    • CinemaNameKR
    • MovieNameKR
    • ScreenDivisionNameKR
    • BrandNm_KR
  • 회차:
    • CinemaID
    • RepresentationMovieCode
    • MovieCode
    • ScreenID
    • PlaySequence
    • PlayDt
    • StartTime
    • EndTime
    • ScreenNameKR
    • TotalSeatCount
    • BookingSeatCount
    • IsBookingYN

2026-03-10 실측 샘플

  • 조건:
    • cinemaID=1|0001|1016 (월드타워)
    • representationMovieCode=23816 (왕과 사는 남자)
  • 결과:
    • PlaySeqsHeader.Items.length = 12
    • PlaySeqs.Items.length = 50
  • 첫 회차:
    • ScreenID=1201
    • PlaySequence=1
    • StartTime=10:40
    • EndTime=12:47
    • ScreenNameKR=1관 샤롯데
    • TotalSeatCount=32
    • BookingSeatCount=28
    • 남은 좌석수 = 4

잔여 좌석 계산식

remainingSeats = TotalSeatCount - BookingSeatCount

실측 회차 예시

  • 10:40-12:47, 총 32석, 예매 28석, 잔여 4석
  • 13:20-15:27, 총 32석, 예매 27석, 잔여 5석
  • 16:00-18:07, 총 32석, 예매 32석, 잔여 0석
  • 18:40-20:47, 총 32석, 예매 22석, 잔여 10석

2.3 POST /LCWS/Ticketing/TicketingData.aspx + MethodName=GetInvisibleMoviePlayInfo

  • 목적:
    • 극장/영화 선택 시 화면에 노출 가능한 조합을 보정하는 보조 API
    • 선택 불가 영화/극장 조합을 숨기는 용도로 보임
  • 요청 예시:
{
  "MethodName": "GetInvisibleMoviePlayInfo",
  "channelType": "HO",
  "osType": "W",
  "osVersion": "<user-agent>",
  "cinemaList": "1|0001|1016",
  "movieCd": "23816",
  "playDt": "2026-03-10"
}
  • 판정:
    • 필수 기능 3종(근처 극장, 상영 목록, 잔여 좌석) 구현에는 보조 역할
    • 초기 구현에서는 생략 가능
    • UI와 동일한 필터링 품질이 필요해질 때 후속 반영 권장

2.4 POST /LCWS/Ticketing/TicketingData.aspx + MethodName=GetSeats

  • 목적:
    • 특정 회차의 좌석 맵/좌석 상태/요금 정보 조회
  • 요청 예시:
{
  "MethodName": "GetSeats",
  "channelType": "HO",
  "osType": "W",
  "osVersion": "<user-agent>",
  "cinemaId": 1016,
  "screenId": 1201,
  "playDate": "2026-03-10",
  "playSequence": 1,
  "screenDivisionCode": 300
}
  • 응답 루트:
    • ScreenSeatInfo.Items[]
    • Seats.Items[]
    • BookingSeats.Items[]
    • Fees.Items[]
    • PlaySeqsDetails.Items[]

실측 확인 필드

  • 좌석 기본:
    • SeatNo, SeatRow, SeatColumn, SeatStatusCode
  • 좌석 메타:
    • SeatXCoordinate, SeatYCoordinate, SeatXLength, SeatYLength
  • 점유 좌석:
    • BookingSeats.Items[]
  • 화면 요약:
    • ScreenSeatInfo.Items[0].TotalSeatCount
    • ScreenSeatInfo.Items[0].BookingCount

주의점

  • 동일 조건에서:
    • GetPlaySequence.BookingSeatCount = 28
    • GetSeats.ScreenSeatInfo.BookingCount = 4
  • 따라서 현재 시점에서는 GetSeatsBookingSeats/BookingCount 의미가
    "최종 판매 좌석 수"와 다를 가능성이 큼
  • 잔여 좌석 기능은 GetPlaySequence 기반으로 구현하는 편이 안전함

2.5 POST /LCWS/Common/MainData.aspx + MethodName=GetPopupMessageOnLine

  • 목적:
    • 극장/회차 선택 시 노출할 팝업 메시지 조회
  • 예매 핵심 데이터 소스는 아님
  • 초기 구현 범위에서는 제외 가능

3) 구현 가능성 판정

즉시 구현 가능

  • lottecinema_find_nearby_theaters

    • 데이터 소스: GetTicketingPageTOBE
    • 사용 필드: CinemaID, CinemaNameKR, Latitude, Longitude, CinemaAddrSummary
  • lottecinema_list_now_showing

    • 데이터 소스:
      • 기본 목록: GetTicketingPageTOBE
      • 회차 상세: GetPlaySequence
    • 사용 필드:
      • 영화: RepresentationMovieCode, MovieNameKR, ViewGradeNameKR, PlayTime
      • 회차: StartTime, EndTime, ScreenNameKR
  • lottecinema_get_remaining_seats

    • 데이터 소스: GetPlaySequence
    • 사용 필드:
      • PlaySequence, TotalSeatCount, BookingSeatCount

조건부 구현

  • lottecinema_get_seat_map
    • 데이터 소스: GetSeats
    • 좌석 점유 상태 해석 검증이 더 필요함

4) 정규화 매핑 제안

극장 목록

  • 입력:
    • CinemaID
    • CinemaNameKR
    • DivisionCode
    • DetailDivisionCode
    • Latitude
    • Longitude
    • CinemaAddrSummary
  • 출력:
    • theaterId
    • theaterName
    • regionCode
    • regionDetailCode
    • latitude
    • longitude
    • address

영화 목록

  • 입력:
    • RepresentationMovieCode
    • MovieNameKR
    • ViewGradeNameKR
    • PlayTime
    • ReleaseDate
  • 출력:
    • movieId
    • movieName
    • rating
    • durationMinutes
    • releaseDate

상영 회차

  • 입력:
    • CinemaID
    • CinemaNameKR
    • RepresentationMovieCode
    • MovieNameKR
    • ScreenID
    • ScreenNameKR
    • PlaySequence
    • PlayDt
    • StartTime
    • EndTime
    • TotalSeatCount
    • BookingSeatCount
  • 출력:
    • scheduleId
    • theaterId
    • theaterName
    • movieId
    • movieName
    • screenId
    • screenName
    • playDate
    • startTime
    • endTime
    • totalSeats
    • bookedSeats
    • remainingSeats

5) 권장 코드 구조

기존 cgv, megabox 서비스와 동일한 패턴으로 구성하는 것이 가장 자연스럽습니다.

src/services/lottecinema/
├── index.ts
├── api.ts
├── client.ts
├── types.ts
└── tools/
    ├── findNearbyTheaters.ts
    ├── listNowShowing.ts
    └── getRemainingSeats.ts

파일별 역할

  • api.ts

    • LOTTECINEMA_API.BASE_URL
    • LOTTECINEMA_API.TICKETING_PATH
    • LOTTECINEMA_API.MAIN_PATH
    • 메서드 이름 상수
      • GET_TICKETING_PAGE
      • GET_PLAY_SEQUENCE
      • GET_INVISIBLE_MOVIE_PLAY_INFO
      • GET_SEATS
  • client.ts

    • fetchLotteCinemaTicketingPage()
    • fetchLotteCinemaPlaySequence()
    • 필요 시 fetchLotteCinemaSeats()
    • 공통 body 생성:
      • channelType: 'HO'
      • osType: 'W'
      • osVersion
    • cinemaID 복합 문자열 변환 유틸 포함
  • types.ts

    • LCWS 응답 타입 정의
    • 극장/영화/회차 정규화 타입 정의
  • tools/findNearbyTheaters.ts

    • GetTicketingPageTOBE 응답의 좌표로 거리 계산
  • tools/listNowShowing.ts

    • 날짜 + 극장 + 영화 조건으로 GetPlaySequence 결과 반환
  • tools/getRemainingSeats.ts

    • GetPlaySequence의 회차별 좌석 수를 정규화해 반환

추가로 필요한 API 레이어

src/api/
├── lottecinemaHandlers.ts
└── routes/
    └── lottecinemaRoutes.ts

테스트 권장 구조

tests/
├── api/
│   └── lottecinema-handlers.test.ts
└── services/
    └── lottecinema/
        ├── client.test.ts
        ├── index.test.ts
        └── tools/
            ├── findNearbyTheaters.test.ts
            ├── listNowShowing.test.ts
            └── getRemainingSeats.test.ts

6) 구현 메모

A. 근처 영화관 도구는 추가 HTML 파싱이 필요 없다

  • 메가박스와 달리 롯데시네마는 초기 예매 응답에 이미 좌표가 포함됨
  • 따라서 별도 상세 페이지를 추가 호출하지 않아도 됨

B. cinemaID 문자열 생성이 핵심이다

  • 내부 정규화에서는 theaterId=1016만 유지하더라도
    실제 GetPlaySequence 호출 직전에는
    1|0001|1016 형태로 복원해야 함
  • 이를 위해 초기 극장 목록에서
    DivisionCode, DetailDivisionCode, CinemaID를 함께 캐시해야 함

C. 잔여 좌석은 GetPlaySequence를 기준으로 고정한다

  • GetSeats는 좌석 맵 단계의 보조 정보로만 사용
  • 사용자 요구 기능 기준으로는 GetPlaySequence만으로 충분함

7) 다음 구현 순서 권장

  1. GetTicketingPageTOBE 기반 클라이언트 구현
  2. GetPlaySequence 기반 회차/잔여좌석 구현
  3. findNearbyTheaters, listNowShowing, getRemainingSeats 도구 추가
  4. API 핸들러/라우트 연결
  5. GetSeats는 후속 단계로 분리
docs/lottemart-mobile-scraping-replay-plan.md 10,202 bytes

롯데마트 모바일 도와센터 스크래핑 리플레이 계획

작성일: 2026-03-14 (KST)
대상: https://company.lottemart.com/mobiledowa/#

1. 결론

롯데마트 모바일 도와센터는 앱 암호화나 난독화된 내부 API가 아니라,
ASP + jQuery AJAX + HTML 응답 구조로 동작합니다.

현재 확인 기준으로:

  • 인증 토큰 없이 curl 재현 가능
  • 지역별 매장 목록 조회 가능
  • 지역 전체 매장 상세 조회 가능
  • 매장별 상품 검색 및 추가 페이지 조회 가능
  • 좌표는 직접 내려주지 않아 nearby 기능은 주소 지오코딩 보완이 필요

즉, 이 프로젝트 기준 판정은 구현 가능(A) 입니다.

운영 메모:

  • Cloudflare Workers에서 https://company.lottemart.com origin은 간헐적으로 522가 발생해,
    현재 서비스 구현은 Worker 런타임에서만 210.93.146.57:80 소켓에 직접 접속하고
    Host: company.lottemart.com 헤더를 수동으로 붙이는 우회 경로를 사용합니다.

2. 플레이북 기준 실측 요약

docs/scraping-playbook.md 원칙대로 먼저 모바일 페이지를 열어 실제 UI 흐름을 확인했습니다.

  • 메인 페이지에서 확인된 핵심 플로우

    • 지역 선택 시 search_market_list.asp
    • 매장 정보 제출 시 search_shop.asp
    • 상품 검색 제출 시 search_product.asp
    • 상품 더보기 클릭 시 search_product_list.asp
  • 브라우저 실측으로 확인한 사실

    • #m_area 변경 시 매장 <option> HTML이 동적으로 채워짐
    • search_shop.aspm_market 없이도 지역 전체 매장 상세를 반환
    • search_product.asp는 첫 페이지 HTML과 totalPage를 함께 반환
    • 추가 페이지는 HTML fragment를 append 하는 방식

3. 현재 확인된 엔드포인트

베이스 URL:

  • https://company.lottemart.com

A. 지역별 매장 코드 목록

  • GET /mobiledowa/inc/asp/search_market_list.asp
  • 파라미터:
    • p_area: 지역명
    • p_type: 1 상품 검색용, 2 매장 정보용
    • p_werks: 선택 매장 코드(선택)

예시:

curl -sS --get 'https://company.lottemart.com/mobiledowa/inc/asp/search_market_list.asp' \
  --data-urlencode 'p_area=서울' \
  --data-urlencode 'p_type=2'

응답 형태:

<option value="">매장선택</option>
<option value="2301">강변점</option>
<option value="2335">금천점</option>
...

B. 지역별 매장 상세 목록

  • POST /mobiledowa/market/search_shop.asp
  • 파라미터:
    • m_area: 지역명
    • m_market: 매장 코드(선택)
    • m_schWord: 입점업체 검색어(선택)

예시:

curl -sS 'https://company.lottemart.com/mobiledowa/market/search_shop.asp' \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data 'm_area=서울'

현재 확인된 필드:

  • 매장명
  • 영업시간
  • 휴점일
  • 주소
  • 상담전화
  • 주차정보
  • detail_shop.asp?werks=... 링크

중요:

  • m_market 없이도 해당 지역의 전체 매장 상세가 내려옵니다.
  • 따라서 1차 수집은 지역 -> 지역 전체 HTML 순회만으로 충분합니다.

C. 매장별 상품 검색

  • POST /mobiledowa/product/search_product.asp
  • 파라미터:
    • p_area: 지역명
    • p_market: 매장 코드
    • p_schWord: 상품 검색어

예시:

curl -sS 'https://company.lottemart.com/mobiledowa/product/search_product.asp' \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data 'p_area=서울&p_market=2301&p_schWord=콜라'

현재 확인된 필드:

  • 총 검색 건수
  • totalPage 자바스크립트 변수
  • 상품명
  • 규격
  • 제조사
  • 가격
  • 재고

주의:

  • 응답 상단에 "<-schWord<br>" 같은 디버그성 문자열이 섞여 있을 수 있어
    파싱 시 본문 시작 전 잡음을 제거하는 방어 코드가 필요합니다.

D. 상품 추가 페이지 조회

  • GET /mobiledowa/inc/asp/search_product_list.asp
  • 파라미터:
    • p_market: 매장 코드
    • p_schWord: 상품 검색어
    • page: 페이지 번호

예시:

curl -sS --get 'https://company.lottemart.com/mobiledowa/inc/asp/search_product_list.asp' \
  --data-urlencode 'p_market=2301' \
  --data-urlencode 'p_schWord=콜라' \
  --data-urlencode 'page=2'

응답은 <li>...</li> 조각 HTML입니다.

4. 매장 범위 확인 결과

메인 페이지 지역 선택지는 총 16개입니다.

  • 서울
  • 경기
  • 인천
  • 강원
  • 충북
  • 충남
  • 대전
  • 경북
  • 경남
  • 대구
  • 부산
  • 울산
  • 전북
  • 전남
  • 광주
  • 기타(제주)

search_market_list.asp?p_type=2 기준 실측 개수:

  • 서울 24
  • 경기 38
  • 인천 12
  • 강원 4
  • 충북 7
  • 충남 6
  • 대전 4
  • 경북 4
  • 경남 13
  • 대구 3
  • 부산 11
  • 울산 3
  • 전북 8
  • 전남 7
  • 광주 6
  • 기타 2

합계는 현재 실측 기준 152개입니다.

주의:

  • 롯데마트 외에도 토이저러스, 맥스, 보틀벙커, Mealguru, 그랑그로서리가 함께 노출됩니다.
  • 서비스 스키마에서 storeType 또는 brandVariant를 분리해두는 편이 안전합니다.

5. 주변 매장 기능 설계

현재 제약

search_shop.aspdetail_shop.asp에는 좌표(lat/lng)가 직접 포함되지 않았습니다.

즉, 이 프로젝트의 findNearbyStores 스타일 기능을 만들려면 아래 두 단계가 필요합니다.

  1. 롯데마트 HTML에서 주소를 수집
  2. 주소를 좌표로 변환 후 캐시

권장 구현 방식

기존 GS25, CU와 유사하게 지오코딩 보완 방식을 사용합니다.

  • 1차 데이터 원본:
    • search_shop.asp에서 수집한 주소
  • 2차 좌표 보완:
    • googleMapsApiKey가 있을 때만 Google Geocoding API 사용
  • 캐시:
    • 주소 -> 좌표 결과를 메모리 캐시 또는 서비스별 캐시에 저장

도구 설계 제안

  • lottemart_find_nearby_stores

    • 입력:
      • latitude
      • longitude
      • keyword 선택
      • area 선택
      • limit
      • googleMapsApiKey 선택
    • 동작:
      • 지역별 매장 목록 수집
      • 주소 지오코딩
      • 거리 계산
      • 가까운 순 정렬
  • lottemart_search_products

    • 입력:
      • query
      • storeCode 또는 storeName
      • area
      • pageLimit
    • 동작:
      • 초기 search_product.asp 호출
      • totalPage 파싱
      • 필요 시 search_product_list.asp 추가 호출

6. 프로젝트 반영 계획

1단계. 문서화 및 캡처 고정

  • docs/lottemart-mobile-scraping-replay-plan.md 유지
  • 필요 시 다음 보조 문서 추가
    • docs/lottemart-network-analysis-result.md
    • captures/lottemart-YYYYMMDD/

2단계. 서비스 뼈대 추가

예상 구조:

src/services/lottemart/
├── index.ts
├── client.ts
├── types.ts
├── api.ts
└── tools/
    ├── findNearbyStores.ts
    └── searchProducts.ts

3단계. client.ts 역할

  • 지역 목록 상수 관리
  • fetchLotteMartMarketOptions(area, type)
  • fetchLotteMartStoreDetails(area, storeCode?)
  • fetchLotteMartProducts(area, storeCode, query)
  • fetchLotteMartProductPage(storeCode, query, page)
  • HTML 파서 유틸리티 제공
  • 주소 지오코딩 보조 함수 제공

4단계. HTML 파싱 방식

현재 응답이 JSON이 아니므로 파싱 전략을 명확히 고정해야 합니다.

  • 옵션 목록:
    • <option value="...">이름</option> 정규식 파싱 가능
  • 매장 상세:
    • <li> 블록 단위 분리 후 라벨 기반 파싱
  • 상품 목록:
    • .prod-name, 규격, 제조사, 가격, 재고를 블록 기반 추출

권장:

  • 파서는 느슨한 CSS 선택자보다 라벨 문자열 기반 보완 로직을 같이 둡니다.
  • 마크업 변경 시 깨지기 쉬운 단일 정규식 하나에 의존하지 않습니다.

5단계. 캐시 전략

  • 매장 목록은 지역별 캐시
  • 매장 상세는 지역별 캐시
  • 주소 지오코딩은 주소 단위 캐시
  • 상품 검색은 짧은 TTL 캐시

이유:

  • 매장 정보는 변동이 적음
  • 상품 재고/가격은 변동 가능성이 높음

7. 테스트 계획

테스트는 기존 서비스와 같은 수준으로 분리합니다.

  • tests/services/lottemart/client.test.ts

    • 옵션 목록 파싱
    • 매장 상세 파싱
    • 상품 검색 파싱
    • totalPage 추출
    • 디버그 문자열 혼입 대응
    • 지오코딩 fallback
  • tests/services/lottemart/tools/findNearbyStores.test.ts

    • 거리순 정렬
    • 좌표 없음 처리
    • 브랜드 변형 포함 여부
  • tests/services/lottemart/tools/searchProducts.test.ts

    • 단일 페이지 검색
    • 다중 페이지 병합
    • 매장 미선택 에러 처리
  • API 핸들러를 붙일 경우:

    • tests/api/lottemart-handlers.test.ts
    • tests/app/app-api-lottemart.test.ts

8. 리스크와 대응

리스크 1. 좌표 부재

가장 큰 제약입니다.

대응:

  • 주소 지오코딩을 옵션 기능으로 추가
  • API 키가 없을 때는 거리 계산 없이 목록 조회만 허용

리스크 2. HTML 구조 변경

JSON API보다 마크업 변경에 취약합니다.

대응:

  • 파서를 라벨 기반으로 작성
  • 테스트 fixture를 충분히 확보

리스크 3. 브랜드 혼합

토이저러스, 맥스 등이 함께 내려오므로
사용자가 기대하는 “롯데마트만” 결과와 다를 수 있습니다.

대응:

  • brandVariant 필드 추가
  • 기본값은 전체 노출, 필요 시 필터 옵션 제공

9. 최종 판정

현재 기준으로 롯데마트 모바일 도와센터는
이 프로젝트에 다음 순서로 편입하는 것이 가장 효율적입니다.

  1. lottemart_find_nearby_stores 먼저 구현
  2. 주소 지오코딩 캐시로 좌표 보완
  3. 이후 lottemart_search_products 추가
  4. 필요 시 입점업체/층별안내(detail_shop.asp, search_store_list.asp) 확장

즉시 구현 우선순위는 아래와 같습니다.

  • 1순위: 지역별 매장 수집
  • 2순위: 주소 지오코딩 기반 nearby
  • 3순위: 상품 검색 HTML 파싱
  • 4순위: 층별안내/입점업체 확장
docs/megabox-network-analysis-result.md 5,338 bytes

메가박스 네트워크 분석 결과 (실측 기반)

작성일: 2026-03-03 (KST)
실측 도구: Playwright MCP, curl
대상:

  • https://www.megabox.co.kr/booking
  • https://www.megabox.co.kr/on/oh/ohb/SimpleBooking/simpleBookingPage.do
  • https://www.megabox.co.kr/on/oh/ohz/PcntSeatChoi/selectPcntSeatChoi.do
  • https://www.megabox.co.kr/theater?brchNo=1372

결론 요약

  • 근처 영화관 조회: 가능 (극장 정보 API에서 주소 + 위경도 추출 가능)
  • 상영중 영화/상영회차 목록: 가능 (selectBokdList.do 응답에 회차 필드 포함)
  • 좌석 남은량(회차별): 가능 (selectBokdList.dorestSeatCnt)
  • 좌석 선점 가능 여부 체크: 조건부 가능
    • selectOccupSeat.doplaySchdlNo 단독 호출 실패
    • seatOccupText 포함 시 정상 응답 확인

1) Playwright 기준 동작 방식 이해

A. 진입 구조

  • /booking 본문에서 데이터가 바로 내려오지 않음
  • 실제 예매 데이터는 iframe에서 동작
    • iframe URL: /on/oh/ohb/SimpleBooking/simpleBookingPage.do

B. 사용자 동작별 호출 흐름

  1. 영화 선택
    -> POST /on/oh/ohb/SimpleBooking/selectBokdList.do

  2. 극장 선택
    -> POST /on/oh/ohb/SimpleBooking/selectBrchBokdUnablePopup.do
    -> POST /on/oh/ohb/SimpleBooking/selectBokdList.do

  3. 좌석 페이지 로딩

    • 좌석 iframe: /on/oh/ohz/PcntSeatChoi/selectPcntSeatChoi.do
    • 좌석 조회: POST /on/oh/ohz/PcntSeatChoi/selectSeatList.do
  4. 좌석 선택 후 다음 단계

    • 좌석 페이지에서 options 생성 후 parent.fn_goNextPagePcntSeatChoi(options) 호출
    • parent에서 POST /on/oh/ohb/BokdMain/selectOccupSeat.do 호출

2) 핵심 엔드포인트 실측

2.1 POST /on/oh/ohb/SimpleBooking/selectBokdList.do

  • 목적:
    • 영화/극장/날짜 조합 기반 상영목록 조회
    • 영화 목록, 극장 목록, 상영 회차, 잔여좌석 동시 수신
  • 주요 요청 필드 예시:
    • arrMovieNo, playDe
    • brchNoListCnt, brchNo1, areaCd1, spclbYn1, theabKindCd1
    • sellChnlCd (ONLINE)
  • 주요 응답 필드:
    • areaBrchList[].brchNo, brchNm
    • movieList[].movieNo, movieNm, movieStatCdNm
    • movieFormList[].playSchdlNo
    • movieFormList[].playStartTime, playEndTime
    • movieFormList[].restSeatCnt, totSeatCnt

2.2 POST /on/oh/ohb/SimpleBooking/selectBrchBokdUnablePopup.do

  • 목적: 극장 선택 시 예매 가능/팝업 상태 확인
  • 응답 예시:
    • brchBokdUnablePopup.bokdAbleAt (Y/N)

2.3 POST /on/oh/ohz/PcntSeatChoi/selectSeatList.do

  • 목적: 특정 회차 좌석 맵/좌석 메타 조회
  • 주요 응답:
    • seatListSD01[].seatUniqNo
    • seatListSD01[].seatZoneCd, seatClassCd
    • seatTicketAmtList[].ticketKindCd (예: TKA, TKY)
    • seatTicketAmtList[].clsReclineAmt 등 좌석등급별 요금

2.4 POST /on/oh/ohb/BokdMain/selectOccupSeat.do

  • 목적: 좌석 선점 충돌(이미 판매 진행중 여부) 체크
  • 중요 포인트:
    • playSchdlNo만 전달하면 실패(500)
    • seatOccupText 포함하면 정상 응답 가능

3) 리플레이 검증 결과

검증일: 2026-03-03 (KST)

A. 상영/잔여좌석 조회 리플레이

  • 요청:
    • playDe=20260304
    • arrMovieNo=25104500
    • brchNo1=1372 (강남)
  • 결과:
    • HTTP 200
    • 응답에서 playSchdlNo 37개, restSeatCnt 37개 확인
    • 회차별 시작/종료 시각 + 남은좌석 수집 가능

B. 좌석 선점 체크 리플레이

  1. 실패 케이스
  • payload: {"playSchdlNo":"2603041372011"}
  • 결과: HTTP 500, "좌석 정보가 없습니다."
  1. 성공 케이스
  • payload 예시:
{
  "playSchdlNo": "2603041372011",
  "brchNo": "1372",
  "bokdCnt": 1,
  "seatOccupText": "00100201,TKA,17000,MBX,GERN_ZONE,RECLINE_CLS,1",
  "totalAmt": 17000,
  "entrpMbCd": "",
  "tkeYn": "N"
}
  • 결과: HTTP 200, resultMap.occupSeatAt = "N"

4) 근처 영화관(위치 기반) 가능성

확인 근거

  • POST /on/oh/ohc/Brch/infoPage.do HTML 내 길찾기 링크에 좌표 포함:
    • 예: ...map.naver.com...lng=127.0264086&lat=37.498214...
  • 동일 HTML에 도로명주소 포함

구현 메모

  • selectBokdListareaBrchList에서 brchNo를 확보
  • infoPage.do를 통해 극장별 좌표를 수집/캐싱
  • 사용자 좌표와 하버사인 거리 계산으로 근접순 정렬

5) 구현 판정

즉시 구현 가능

  • megabox_find_nearby_theaters
    • 데이터: selectBokdList.do + infoPage.do
  • megabox_list_now_showing
    • 데이터: selectBokdList.do (movieList, movieFormList)
  • megabox_get_remaining_seats
    • 데이터: selectBokdList.do (movieFormList[].restSeatCnt)

조건부 구현

  • megabox_check_seat_occupancy
    • seatOccupText를 정확히 생성해야 함
    • 좌석맵(selectSeatList)과 요금/권종 매핑 로직 필요

6) 다음 실측/구현 권장 순서

  1. selectBokdList.do 기반 도구 2종 우선 구현
    • 상영목록/잔여좌석
  2. infoPage.do 좌표 캐시 설계 후 근처극장 도구 구현
  3. 좌석 선점 체크는 별도 단계로 분리
    • seatOccupText 생성 유틸을 먼저 고정
    • 실패/재시도 케이스(이미 선점됨) 메시지 표준화
docs/mitmproxy-guide.md 4,568 bytes

mitmproxy 가이드 (iOS + macOS, HTTPS 포함)

작성일: 2026-03-08 (KST)
대상: iOS 앱(예: 포켓CU) 트래픽 실측

1. 목적

이 문서는 macOS에서 mitmproxy를 사용해 iOS 앱 트래픽을 수집하고, HTTPS 요청/응답을 확인하는 방법을 설명합니다.

2. 사전 준비

  • Mac과 iPhone이 같은 Wi-Fi 네트워크에 연결되어 있어야 합니다.
  • macOS에 Homebrew가 설치되어 있어야 합니다.
  • 분석 대상 앱이 최신 버전인지 확인합니다.

3. macOS에 mitmproxy 설치 및 실행

brew install --cask mitmproxy
mitmweb --listen-host 0.0.0.0 --listen-port 8080
  • mitmweb 실행 후 브라우저 UI(기본 http://127.0.0.1:8081)에서 캡처 로그를 확인할 수 있습니다.
  • Mac의 로컬 IP를 확인합니다.
ipconfig getifaddr en0

참고: 유선/다른 인터페이스 사용 시 IP 조회 명령을 환경에 맞게 조정하세요.

4. iOS에서 프록시 설정

  1. iPhone에서 설정 > Wi‑Fi > 현재 연결된 네트워크 > 프록시 구성으로 이동합니다.
  2. 수동 선택 후 아래 입력:
  • 서버: Mac의 로컬 IP (예: 192.168.0.10)
  • 포트: 8080
  1. 저장 후 Safari에서 http://mitm.it 접속
  2. iOS용 인증서 프로파일을 설치
  3. 설정 > 일반 > 정보 > 인증서 신뢰 설정에서 mitmproxy 인증서를 신뢰로 활성화

주의: 5단계를 하지 않으면 HTTPS 복호화가 정상 동작하지 않습니다.

5. 앱 기반 트래픽 수집 절차

다음 순서로 수집하면 재현성이 높습니다.

  1. mitmweb 실행 상태 확인
  2. iOS 프록시/인증서 신뢰 상태 확인
  3. 대상 앱 완전 종료 후 재실행
  4. 앱 내 실제 사용자 시나리오 수행
  • 예: 포켓CU에서 점포 선택 -> 상품 검색 -> 재고 조회 버튼 실행
  1. mitmweb에서 도메인/경로 기준으로 요청 필터링
  2. 핵심 요청을 저장
  • URL, Method, Headers(민감정보 마스킹), Body, Response 샘플

권장 필터 예시:

  • ~u cu.bgfretail.com
  • ~m POST
  • ~u inventory|stock|product|store

6. 캡처 데이터 저장 방법

A. mitmproxy 흐름 파일 저장

mitmdump -w cu-ios-capture-20260308.mitm

수집 후 Ctrl+C로 종료하면 파일이 저장됩니다.

B. 저장 파일 다시 열기

mitmweb -r cu-ios-capture-20260308.mitm

7. 분석 시 체크리스트

  • 재고 API 후보 URL/경로가 식별되는가?
  • 필수 헤더(Authorization, 앱 버전, 디바이스 식별값 등)가 있는가?
  • 로그인/비로그인 상태에서 응답 차이가 있는가?
  • 점포 코드/상품 코드/수량 필드가 응답에 존재하는가?
  • Cloudflare Worker에서 재현 가능한 요청 구조인가?

8. 자주 발생하는 실패 원인

1) HTTPS 요청이 안 보이거나 TLS 에러 발생

  • iOS 인증서 신뢰 설정이 누락되었을 가능성이 큽니다.
  • 프록시 IP/포트 설정 오타 여부를 확인하세요.

2) 특정 앱 요청만 복호화되지 않음

  • 앱이 certificate pinning을 사용하는 경우가 많습니다.
  • 이 경우 일반 MITM 프록시만으로는 복호화가 제한될 수 있습니다.

3) 요청 자체가 거의 안 보임

  • 앱이 프록시 우회 경로 또는 비표준 네트워크 스택을 쓸 수 있습니다.
  • 앱 재설치/재로그인 후 시나리오를 다시 수행해 비교하세요.

9. 보안 및 운영 주의사항

  • 개인 계정 토큰, 전화번호, 주소 등 민감정보는 문서화 시 반드시 마스킹하세요.
  • 실사용 계정 대신 테스트 계정 사용을 권장합니다.
  • 사내/공용망 정책에 따라 트래픽 캡처가 제한될 수 있으니 정책을 확인하세요.

10. CU 재고 분석에 바로 적용하는 최소 실행안

  1. mitmweb 실행
  2. iOS 프록시 + 인증서 신뢰 완료
  3. 포켓CU에서 점포 선택 -> 상품 검색 -> 재고 조회 수행
  4. cu.bgfretail.com 또는 앱 API 도메인 요청 추출
  5. 재고 관련 엔드포인트/헤더/파라미터를 docs/cu-network-analysis-result.md에 후속 기록

11. 전달 가능한 산출물 자동 생성

앱 트래픽을 분석자에게 바로 전달하려면 아래 문서를 사용하세요.

  • docs/cu-app-request-capture-guide.md

위 문서에는 mitmdump 실행 명령과 함께, 아래 산출물을 자동 생성하는 방법이 포함되어 있습니다.

  • raw.mitm (원본)
  • requests.jsonl (마스킹된 요청/응답)
  • summary.json (건수/시나리오 요약)
docs/oliveyoung-lightpanda-validation.md 2,264 bytes

올리브영 x Lightpanda 검증 기록

작성일: 2026-03-03 (KST)

목적

  • https://github.com/lightpanda-io/browser 기반으로 올리브영 데이터 수집이 가능한지 실측 검증
  • 대상 API:
    • POST /oystore/api/storeFinder/find-store
    • POST /oystore/api/stock/product-search-v3

검증 환경

검증 방법

  1. @lightpanda/browser 설치 후 CDP 서버 실행
  2. Playwright connectOverCDP로 Lightpanda 제어
  3. 올리브영 메인 접속 후 챌린지 페이지 여부 확인
  4. 브라우저 컨텍스트에서 API 2종 직접 호출
  5. 동일 시나리오 3회 반복
  6. lightpanda.fetch() 단독 호출 결과도 확인

실행 명령

  • node /tmp/lightpanda-test/test-oliveyoung-loop.mjs
  • node -e "import('@lightpanda/browser')...lightpanda.fetch('https://www.oliveyoung.co.kr/')..."

결과 요약

1) CDP + Playwright 반복 3회

  • 3회 모두 동일 결과
  • navigator.userAgent: Lightpanda/1.0
  • 본문에 아래 차단 문구 확인:
    • Just a moment...
    • Enable JavaScript and cookies to continue
    • Browser not supported
    • 안전하고 원활한 올리브영 이용을 위해 접속 정보를 확인 중
  • API 호출 결과:
    • find-store: HTTP 403
    • product-search-v3: HTTP 403

2) lightpanda.fetch() 단독 호출

  • 챌린지/차단 키워드 모두 포함 확인
  • 정상 서비스 페이지가 아닌 보안 검증 페이지 반환

추가 관찰

  • Lightpanda CDP + Playwright 조합에서 컨텍스트 확장 시 Duplicate target 오류가 간헐적으로 발생
  • 현재 패키지 옵션만으로는 UA를 Chrome 계열로 안정적으로 위장/전환하기 어려움

판정

  • 현 시점 판정: Lightpanda 단독으로 올리브영 데이터 수집은 사실상 불가
  • 근거:
    • 초기 진입에서 보안 검증/브라우저 미지원 페이지로 고정
    • 핵심 API 2종이 반복적으로 403 반환

권장 방향

  • 올리브영 수집은 기존 실측 성공 경로(Playwright + 일반 Chromium 세션) 유지
  • Lightpanda는 올리브영이 아닌, 차단 정책이 약한 대상에서만 후보로 검토
docs/oliveyoung-network-analysis-result.md 9,001 bytes

올리브영 네트워크 분석 결과 및 구현 판정

작성일: 2026-03-02 (KST)

결론 요약

  • 재고 관련 요청 리플레이 성공: 성공
    • POST /oystore/api/stock/product-search-v3
  • 매장 검색 요청 리플레이 성공: 성공
    • POST /oystore/api/storeFinder/find-store
  • 쿠키/세션 의존성: 낮음 (현재 실측 기준)
  • 분류: A 유형에 가까움
    • 쿠키 불필요 + JSON Body 기반 직접 호출 가능

엔드포인트 판정

  1. POST /oystore/api/storeFinder/find-store
  • 목적: 매장 검색(위치/키워드)
  • 필수 Body 필드(실측 기준):
    • lat, lon, pageIdx, searchWords, pogKeys, serviceKeys, mapLat, mapLon
  • 실패 패턴:
    • 필드 축약 시 400 Bad Request
    • GET405 Method Not Allowed
  1. POST /oystore/api/stock/product-search-v3
  • 목적: 매장 맥락 상품/재고 리스트
  • 필수 Body 필드(실측 기준):
    • includeSoldOut, keyword, page, sort, size
  • 실패 패턴:
    • Body 단순화(keyword만 전송) 시 500 Internal Server Error
    • GET405 Method Not Allowed

안정성/리스크

  • API 응답 성공률 자체는 높음(실측 3회 이상 연속 성공)
  • 다만 product-search-v3는 특정 시점에 질의어와 결과 불일치가 관찰됨
    • 예: 샴푸 요청 직후 립밤 결과 재노출
    • 가설: 프론트 상태/응답 경합/캐시 관여
  • 권장 방어:
    • 응답 검증(keyword 포함도, 상위 상품명 토큰 점검)
    • 필요 시 재시도 1회

MCP 도구 스키마 초안

1) oliveyoung_find_stores

입력:

{
  "searchWords": "명동",
  "pageIdx": 1,
  "lat": 37.56409158001314,
  "lon": 126.98517710459745,
  "pogKeys": "",
  "serviceKeys": "",
  "mapLat": 37.56409158001314,
  "mapLon": 126.98517710459745
}

출력:

{
  "status": "SUCCESS",
  "totalCount": 9,
  "stores": [
    {
      "storeCode": "D176",
      "storeName": "올리브영 명동 타운",
      "address": "서울특별시 중구 명동길 53 1~2층",
      "latitude": 37.56409158001314,
      "longitude": 126.98517710459745,
      "pickupYn": false,
      "o2oRemainQuantity": 0
    }
  ]
}

2) oliveyoung_search_stock_products

입력:

{
  "keyword": "선크림",
  "page": 1,
  "size": 20,
  "sort": "01",
  "includeSoldOut": false
}

출력:

{
  "status": "SUCCESS",
  "totalCount": 429,
  "nextPage": true,
  "products": [
    {
      "goodsNumber": "A000000200614",
      "goodsName": "[7일특가/3월 올영픽] 달바 퍼플 톤업 선크림 듀오 기획 (50ml+50ml)",
      "priceToPay": 32130,
      "originalPrice": 51000,
      "discountRate": 37,
      "o2oStockFlag": true,
      "o2oRemainQuantity": 0
    }
  ]
}

추가 메모

  • Cloudflare 챌린지로 인해 초기 접속 실패 가능
  • 실측 시 UA/브라우저 세션 상태 영향 있음
  • 세션 테스트 스크립트는 --mode=browser에서 실측 성공 확인
    • 예: npx tsx docs/oliveyoung-replay-session-test.ts --mode=browser --loop=1 --headless=false
  • 서버 구현 시 공식 API 정책/약관 준수 필요

Zyte API 검증 (추가)

검증일: 2026-03-03 (KST)

  • 검증 스크립트: docs/oliveyoung-zyte-replay-test.ts
  • 인증: .envZYTE_API_KEY 사용 (저장소에는 .env.example만 커밋)
  • 실행:
    • npx tsx docs/oliveyoung-zyte-replay-test.ts --loop=3 --keyword=선크림 --store=명동
  • 결과:
    • 홈페이지 렌더(browserHtml) 기준 Cloudflare 챌린지 미검출
    • POST /oystore/api/storeFinder/find-store: 3/3 성공
    • POST /oystore/api/stock/product-search-v3: 3/3 성공

구현 판정 업데이트

  • 판정: 가능
  • 근거:
    • Zyte extract API에서 커스텀 HTTP 요청(httpRequestMethod, httpRequestText, customHttpRequestHeaders)으로
      올리브영 API 2종을 안정적으로 호출 성공
    • 응답은 statusCode=200, 본문 status=SUCCESS, totalCount 정상 확인

Zyte 전송량/비용 실측 (추가)

측정일: 2026-03-03 (KST)
측정 스크립트: docs/oliveyoung-zyte-bandwidth-test.ts
실행: npx tsx docs/oliveyoung-zyte-bandwidth-test.ts --loop=10 --keyword=선크림 --store=명동

실측 평균 (10회)

  • find-store 1회 왕복: 10,350 bytes (약 10.11 KB)
  • product-search-v3 1회 왕복: 18,314 bytes (약 17.88 KB)
  • 단일 API 호출 평균: 14,332 bytes (약 13.99 KB)
  • 수집 1건(find-store + product-search-v3) 왕복: 28,664 bytes (약 27.99 KB)

수집량 기준 트래픽 (캐시 미적용)

  • 1천건: 약 27.34 MB (요청 2,000)
  • 1만건: 약 273.36 MB (요청 20,000)
  • 10만건: 약 2.67 GB (요청 200,000)

비용 계산 기준

  • Zyte 과금은 바이트가 아니라 성공 요청 수 기준
  • 단가 범위(HTTP PAYG, 티어별): $0.00013 ~ $0.00127 / 요청
  • 수집 1건이 API 2콜이면: 총요청수 = 수집건수 x 2

캐싱 설계 시 비용 절감 추정

가정:

  • find-storeproduct-search-v3 모두 동일 적중률 h
  • 유효 요청 수: 총요청수 = 수집건수 x 2 x (1 - h)

1천건 기준

  • 캐시 없음(h=0%): 2,000 요청, $0.26 ~ $2.54
  • h=50%: 1,000 요청, $0.13 ~ $1.27
  • h=80%: 400 요청, $0.05 ~ $0.51
  • h=90%: 200 요청, $0.03 ~ $0.25
  • h=95%: 100 요청, $0.01 ~ $0.13

1만건 기준

  • 캐시 없음(h=0%): 20,000 요청, $2.60 ~ $25.40
  • h=50%: 10,000 요청, $1.30 ~ $12.70
  • h=80%: 4,000 요청, $0.52 ~ $5.08
  • h=90%: 2,000 요청, $0.26 ~ $2.54
  • h=95%: 1,000 요청, $0.13 ~ $1.27

10만건 기준

  • 캐시 없음(h=0%): 200,000 요청, $26.00 ~ $254.00
  • h=50%: 100,000 요청, $13.00 ~ $127.00
  • h=80%: 40,000 요청, $5.20 ~ $50.80
  • h=90%: 20,000 요청, $2.60 ~ $25.40
  • h=95%: 10,000 요청, $1.30 ~ $12.70

초기 캐시 아키텍처 권장안

1) 매장 검색 캐시 (find-store)

  • 캐시 키: region-grid(위경도 반올림)+searchWords+pageIdx
  • TTL: 24h (매장 정보 변동이 상대적으로 낮음)
  • 기대 효과: 동일 지역 반복 조회에서 높은 적중률

2) 상품 검색 캐시 (product-search-v3)

  • 캐시 키: keyword+page+size+sort+includeSoldOut
  • TTL: 10~30분 (재고/노출 변동 고려)
  • 기대 효과: 인기 키워드 반복 조회에서 비용 대폭 절감

3) 캐시 정책

  • stale-while-revalidate 적용: 응답 지연 없이 백그라운드 갱신
  • 빈 결과도 짧은 TTL(예: 3분)로 캐시하여 중복 조회 방지
  • 에러 응답(4xx/5xx)은 캐시하지 않음

4) 현실적인 목표치

  • 초기에 h=60~80%만 달성해도 비용은 약 40~80% 절감
  • 키워드 편중이 큰 트래픽이면 h=90%도 가능

재고 라이브성 우선 캐시 정책 (설계안)

목표:

  • 비용 절감은 유지하되, 재고 정확도를 우선 보장
  • 재고 관련 오판(있다고 보여주나 실제 없음) 최소화

분리 캐시 전략

  • store metadata (매장명/주소/좌표 등): TTL 24h
  • product metadata (상품명/가격/이미지 등): TTL 10~30분
  • inventory fields (o2oStockFlag, o2oRemainQuantity): TTL 30~90초

핵심:

  • 동일 API 응답이라도 저장 시 메타 데이터와 재고 필드를 분리 캐시
  • 응답 조합 시 meta cache + inventory cache를 합성

재고 TTL 가변 규칙

  • remain <= 3: TTL 20~30초
  • 4 <= remain <= 20: TTL 45~60초
  • remain > 20: TTL 60~120초
  • o2oStockFlag=false: TTL 20~45초 (품절 변동 감지 위해 짧게)

조회/갱신 정책

  • 기본: cache-first + 짧은 stale-while-revalidate
  • 재고 stale 허용 상한: 15~30초
  • stale 상한 초과 시: 원본 API 강제 조회
  • 4xx/5xx 응답: 캐시 저장 금지
  • 빈 결과(검색 0건): negative cache TTL 2~3분

사용자/클라이언트 투명성

응답 필드에 아래 메타를 포함:

  • fetchedAt: 원본 조회 시각(ISO)
  • cacheAgeSec: 캐시 경과 시간
  • inventoryFreshSec: 재고 필드 신선도(초)
  • dataSource: origin|cache|cache_swr

강제 신선 조회 경로

  • 결제/픽업 직전에는 forceRefresh=true 옵션으로 원본 재조회
  • 해당 경로는 캐시 우회 후 최신 재고를 반환

모니터링 지표 (SLO)

  • inventory_mismatch_rate (표시 재고 vs 후속 검증 불일치율)
  • inventory_cache_hit_rate
  • inventory_p95_age_sec
  • force_refresh_ratio

권장 초기 목표:

  • inventory_mismatch_rate < 1%
  • inventory_p95_age_sec < 60

구현 스케치

  • 캐시 키 예시:
    • 메타: oy:stock:meta:{keyword}:{page}:{size}:{sort}:{includeSoldOut}
    • 재고: oy:stock:inv:{keyword}:{page}:{size}:{sort}:{includeSoldOut}
  • 처리 순서:
    1. 메타/재고 캐시 조회
    2. 재고 캐시가 stale 상한 이내면 즉시 응답
    3. stale 상한 초과면 원본 조회 후 재고 캐시 갱신
    4. 메타는 긴 TTL로 별도 갱신
docs/oliveyoung-playwright-mcp-onboarding.md 3,252 bytes

올리브영 Playwright MCP 연동 및 실측 진행 가이드

문서 목적

  • Playwright MCP 연동 이후, 올리브영 실측 분석을 어떤 순서로 진행할지 정의합니다.
  • 목표는 Puppeteer 없이 리플레이 가능한 요청 조합을 찾는 것입니다.

1) Playwright MCP 연동 확인

필수 파일

  • .mcp.json에 Playwright MCP 서버 등록
  • .claude/settings.local.jsonenableAllProjectMcpServers: true 설정

체크리스트

  • Claude Code 세션 재시작 완료
  • 도구 목록에 Playwright 관련 MCP 도구 노출 확인
  • 대상 URL 접속 가능 여부 확인: https://www.oliveyoung.co.kr/

2) 실측 분석 기준 (반드시 준수)

  • 브라우저 기반 실측만 사용 (단순 추정 금지)
  • 최소 3회 반복 측정 (요청/응답 일관성 확인)
  • 각 요청별 아래 항목 기록
    • URL, Method, Query, Body
    • 필수 헤더 (referer, origin, content-type, 기타 커스텀 헤더)
    • 쿠키 필요 여부
    • 응답 형식(JSON/HTML), 핵심 필드
    • 실패 시 에러 코드/차단 패턴 (예: Cloudflare challenge)

3) 분석 시나리오

시나리오 A: 매장 검색

  • 페이지 진입
  • 위치 기반/키워드 기반 매장 검색 동작 수행
  • 네트워크 요청 중 매장 목록 API 후보 추출

시나리오 B: 상품 검색

  • 검색어 입력 후 결과 로딩 요청 추출
  • 페이지네이션/정렬/필터 요청 파라미터 변화 관찰

시나리오 C: 재고 조회

  • 특정 상품 선택 후 재고 조회 동작 수행
  • 매장별 재고 수량, 품절 상태 필드 구조 파악

4) 리플레이 검증 절차 (핵심)

1차 검증

  • 캡처된 요청을 curl로 그대로 재현
  • 응답이 정상 데이터인지 확인

2차 최소화 검증

  • 헤더/쿠키를 하나씩 제거하여 최소 조건 도출
  • 최종적으로 아래 중 어디에 해당하는지 분류
    • A. 쿠키 불필요 + 필수 헤더만 필요
    • B. 세션 쿠키 필요
    • C. 토큰/동적 파라미터 필요
    • D. 브라우저 런타임 의존(리플레이 난이도 높음)

3차 안정성 검증

  • 동일 요청 10회 반복 시 성공률 확인
  • 성공률 90% 미만이면 원인(차단/만료/레이스) 기록

5) 구현 결정 규칙

  • A 유형: Cloudflare Workers fetch 기반 구현
  • B/C 유형: 세션/토큰 획득 단계 포함한 2-step 설계
  • D 유형: 서버 배포형 자동화 지양, 대안 경로(공식 API/제휴 API) 검토

6) 산출물 (docs/에 추가할 파일)

  • docs/oliveyoung-playwright-network-analysis.md
    • 실측 로그 요약, 엔드포인트 표, 필수 헤더/쿠키 표
  • docs/oliveyoung-network-analysis-result.md
    • 리플레이 성공/실패 결론, 구현 가능성 판정
  • docs/oliveyoung-replay-session-test.ts
    • 재현 스크립트
  • docs/oliveyoung-replay-session-test.html (선택)
    • 브라우저 수동 검증용 페이지

7) 완료 조건 (Definition of Done)

  • 최소 1개 이상 재고 관련 요청 리플레이 성공
  • 성공 요청의 최소 헤더/쿠키 조건 문서화 완료
  • 실패 케이스 및 차단 패턴 문서화 완료
  • MCP 도구 설계에 필요한 입력/출력 스키마 초안 작성 완료
README.md 17,407 bytes

Daiso MCP Server

다이소(제품/매장/재고), 롯데마트(매장/상품), GS25(매장/상품/재고), 세븐일레븐(상품/매장/재고/인기검색어/카탈로그), CU(매장/재고), 이마트24(매장/상품/재고), 올리브영(매장/재고), 메가박스(지점/영화/시간표/좌석), 롯데시네마(지점/영화/좌석), CGV(극장/영화/시간표) 조회 기능을 AI에 연결합니다.

License: MIT
Cloudflare Workers
MCP
Code Coverage
Coverage

기준 워커: daiso-mcp · 마지막 갱신: 2026-04-22 00:49 KST

  


AI 앱에서 MCP 연결하기

ChatGPT, Claude, Grok 같은 AI 앱에서 바로 연결해 사용할 수 있습니다.
아래 앱별 가이드에서 먼저 연동한 뒤 검색/재고/영화 조회를 요청하세요.

ChatGPT

MCP 연동이 어렵다는 피드백이 있어 바로 사용 가능한 GPT 앱을 추가했습니다.
아래 링크로 모바일에서도 간편하게 이용 가능합니다!

Daiso MCP GPT 앱 바로가기

빠른 사용 예시:

다이소 mcp로 수납박스 검색해줘
올리브영 mcp로 명동 근처 매장 찾아줘
이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘
롯데마트 mcp로 잠실 근처 매장 찾아줘
롯데마트 mcp로 강변점에서 콜라 재고 알려줘
GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
세븐일레븐 mcp로 삼각김밥 검색해줘
세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘
세븐일레븐 mcp로 안산 중앙역 근처 세븐일레븐에서 핫식스 재고 알려줘
세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘
메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘
롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘
롯데시네마 mcp로 월드타워 잔여 좌석 알려줘
CGV mcp로 강남 상영 영화와 시간표 알려줘

Claude

Pro / Max / Team / Enterprise 플랜 필요 · 웹에서 설정 시 모바일 앱에서도 사용 가능

  1. claude.ai에서 SettingsConnectors 이동
  2. Add custom connector 클릭
  3. 원격 MCP 서버 URL 입력: https://mcp.aka.page
  4. Add 클릭하여 완료
  5. 대화창에서 + 버튼 → Connectors → 토글로 활성화

사용 예시:

다이소 mcp를 사용해서 수납박스 검색해줘
다이소 mcp를 사용해서 강남역 근처 매장 찾아줘
올리브영 mcp를 사용해서 명동 근처 매장 찾아줘
올리브영 mcp를 사용해서 선크림 재고 확인해줘
이마트24 mcp를 사용해서 강남 매장 찾고 두바이 재고 확인해줘
롯데마트 mcp를 사용해서 잠실 근처 매장 찾아줘
롯데마트 mcp를 사용해서 강변점에서 콜라 검색해줘
GS25 mcp를 사용해서 강남 매장 찾고 오감자 재고 확인해줘
세븐일레븐 mcp를 사용해서 안산 중앙역 근처 매장 찾고 핫식스 재고 확인해줘
메가박스 mcp를 사용해서 강남역 근처 지점 찾아줘
메가박스 mcp를 사용해서 강남점 영화 목록이랑 잔여 좌석 확인해줘
롯데시네마 mcp를 사용해서 잠실 근처 지점 찾아줘
롯데시네마 mcp를 사용해서 월드타워 영화 목록이랑 잔여 좌석 확인해줘
CGV mcp를 사용해서 서울 지역 극장 목록 찾아줘
CGV mcp를 사용해서 강남 CGV 영화랑 시간표 확인해줘

참고: Claude Remote MCP 가이드

Claude Code

Claude Code CLI에서 MCP 서버 추가

claude mcp add daiso-mcp https://mcp.aka.page --transport sse

Grok

웹 및 모바일 앱 모두 지원

프롬프트 페이지 URL:

https://mcp.aka.page/prompt

사용 방법:

  1. Grok 모바일 앱에서 https://mcp.aka.page/prompt 페이지를 읽어달라고 요청
  2. 에이전트가 API 사용법을 이해하고 GET 요청으로 기능 실행

예시 대화:

사용자: https://mcp.aka.page/prompt 를 읽어줘
AI: (페이지를 읽고 API 사용법 이해)

사용자: 수납박스 검색해줘
AI: (https://mcp.aka.page/api/daiso/products?q=수납박스 호출 후 결과 제공)

사용자: 안산 중앙역 근처 메가박스 지점 찾아줘
AI: (https://mcp.aka.page/api/megabox/theaters?keyword=안산%20중앙역 호출 후 결과 제공)

사용자: 잠실 근처 롯데시네마 지점 찾아줘
AI: (https://mcp.aka.page/api/lottecinema/theaters?keyword=%EC%9E%A0%EC%8B%A4 호출 후 결과 제공)

사용자: 강남 CGV 시간표 알려줘
AI: (https://mcp.aka.page/api/cgv/timetable?playDate=20260304&theaterCode=0056 호출 후 결과 제공)

사용자: 안산 중앙역 근처 CGV 찾아서 오늘 영화랑 시간표 알려줘
AI: (https://mcp.aka.page/api/cgv/theaters?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공)
AI: (https://mcp.aka.page/api/cgv/movies?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공)
AI: (https://mcp.aka.page/api/cgv/timetable?playDate=20260315&keyword=안산%20중앙역 호출 후 결과 제공)

MCP 서버 URL / CLI (고급)

AI 앱 대신 직접 연결하거나 스크립트에서 사용할 때만 참고하세요.

MCP 서버 URL:

https://mcp.aka.page

CLI (npx):

# 인터랙티브 모드 (추천)
npx daiso

# 인터랙티브 비활성화 (CI/스크립트)
npx daiso --non-interactive

# 명령형 모드
npx daiso help
npx daiso help products
npx daiso url
npx daiso health
npx daiso claude

# AI 없이 직접 조회
npx daiso products 수납박스
npx daiso product 1034604
npx daiso stores 강남역
npx daiso inventory 1034604 --keyword 강남역
npx daiso display-location 1034604 04515
npx daiso cu-stores 강남
npx daiso cu-inventory 과자 --storeKeyword 강남
npx daiso lottemart-stores 잠실 --area 서울 --limit 10
npx daiso lottemart-products 콜라 --storeName 강변점 --area 서울
npx daiso emart24-stores 강남 --service24h true
npx daiso emart24-products 두바이 --pageSize 20
npx daiso emart24-inventory 8800244010504 --bizNoArr 28339,05015
npx daiso gs25-stores 강남 --limit 10
npx daiso gs25-products 오감자 --limit 20
npx daiso gs25-inventory 오감자 --storeKeyword 강남 --storeLimit 10
npx daiso seveneleven-products 삼각김밥 --size 20
npx daiso seveneleven-stores 안산 중앙역 --limit 10
npx daiso get /api/seveneleven/inventory --keyword 핫식스 --storeKeyword 안산%20중앙역 --storeLimit 10
npx daiso seveneleven-popwords --label home
npx daiso seveneleven-catalog --includeIssues true --includeExhibition true --limit 10
npx daiso get /api/cgv/movies --playDate 20260307 --theaterCode 0056

# 원본 JSON 필요 시
npx daiso products 수납박스 --json

OpenAPI 스펙

  • OpenAI 챗봇 등록용 축약 스펙: https://mcp.aka.page/openapi.json
  • OpenAI 챗봇 등록용 YAML: https://mcp.aka.page/openapi.yaml
  • 전체 개별 엔드포인트 스펙(JSON): https://mcp.aka.page/openapi-full.json
  • 전체 개별 엔드포인트 스펙(YAML): https://mcp.aka.page/openapi-full.yaml

기본 openapi.json은 OpenAI Actions import 제한에 맞추기 위해 GET /api/actions/query 단일 facade만 노출합니다.
기존 서비스별 GET API는 유지되며, 자세한 배경은 OpenAPI Actions Facade 문서에 정리했습니다.

인터랙티브 예시:

$ npx daiso
daiso 인터랙티브 모드

[서비스 선택]
1. 다이소
2. 올리브영
3. CU
서비스 번호를 선택하세요 (0: 종료): 1

매장 검색 키워드를 입력하세요: 강남

[매장 선택]
1. 다이소 강남점 | 서울 강남구 ...
2. 다이소 강남역점 | 서울 강남구 ...
입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색
선택: /역점
선택: 1

[선택한 매장 정보]
- 매장명: 다이소 강남역점
- 주소: 서울 강남구 ...
- 전화: 02-...

찾을 상품 키워드를 입력하세요: 수납박스

[상품 선택]
1. 손잡이 수납박스 (2000원, ID: 1034604)
2. 접이식 수납박스 (3000원, ID: 1034605)
입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 취소
선택: 1

[재고 결과]
- 상품: 손잡이 수납박스
- 매장: 다이소 강남역점
- 재고 수량: 7

[다음 동작]
1. 같은 매장에서 다른 상품 찾기
2. 다른 매장/서비스 다시 선택하기
3. 종료하기
번호를 선택하세요: 3
인터랙티브 모드를 종료합니다.

미지원 서비스

서비스 상태
Gemini Google Gemini ❌ 미지원
Copilot GitHub Copilot ❌ 미지원

Special Thanks

이 프로젝트에 도움 주신 분들께 감사드립니다.

  • @thecats1105: 다이소 진열 위치 조회 도구(daiso_get_display_location) 구현 및 API/테스트 연동
  • @betterthanhajin: CGV 서비스 프로바이더 구현(극장/영화/시간표 도구, 라우트·스펙·테스트 추가)
  • 제로초님: 프로젝트 홍보 도움

상세 문서

Special Thanks 이후에 있던 상세 설명은 별도 문서로 분리했습니다.


docs 문서

공통 가이드

다이소

CU

이마트24

롯데마트

공통

올리브영

영화관

GS25


MIT License


신규 MCP 기능 추가 시 유의사항

새로운 서비스나 도구를 추가할 때는 구현만 끝내지 말고 아래 반영 범위를 함께 확인해야 합니다.

  • MCP: src/index.ts 서비스 등록, 루트 서비스/도구 목록, 관련 테스트 반영
  • HTTPS: GET API 핸들러/라우트, 프롬프트 페이지(src/pages/prompt.ts), 앱 통합 테스트 반영
  • CLI: src/cli.ts, src/cliHelp.ts, CLI 테스트 반영
  • AI instruction: ai-instruction.md 사용 규칙/워크플로우 반영
  • README: 지원 서비스 설명, 예시, 문서 링크 반영
  • OpenAPI: 기본 /openapi.json facade 스펙, /openapi-full.json 전체 스펙, 관련 테스트 반영

기능 추가 후 최소 검증 기준:

  • npm run typecheck
  • npm test