내 주변 다이소/올리브영 매장을 찾고, 재고를 확인하는 기능을 AI에게 부여합니다. (+메가박스, CGV, 롯데시네마, CU편의점, 이마트24 편의점)
MCP Configs (3)
에이전트 개발 규칙
이 문서는 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()에 민감한 정보가 출력되지 않는가? - 주석에 실제 계정 정보가 포함되어 있지 않는가?
- 테스트 데이터가 실제 개인정보를 포함하지 않는가?
파일 구조
플러그인 기반 아키텍처
이 프로젝트는 확장 가능한 플러그인 아키텍처로 설계되었습니다. 다이소 외에 편의점, 백화점, 영화관 등 다양한 서비스를 쉽게 추가할 수 있습니다.
핵심 설계 원칙
- ServiceProvider 인터페이스: 모든 서비스가 구현해야 하는 표준 계약
- ServiceRegistry: 서비스를 동적으로 등록하고 MCP 서버에 연결
- 도구 이름 네임스페이스: 서비스별 접두사 사용 (예:
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 사용 가이드 업데이트
참고 자료
이 규칙을 따라 일관되고 안전하며 유지보수 가능한 코드를 작성해주세요.
에이전트 개발 규칙
이 문서는 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()에 민감한 정보가 출력되지 않는가? - 주석에 실제 계정 정보가 포함되어 있지 않는가?
- 테스트 데이터가 실제 개인정보를 포함하지 않는가?
파일 구조
플러그인 기반 아키텍처
이 프로젝트는 확장 가능한 플러그인 아키텍처로 설계되었습니다. 다이소 외에 편의점, 백화점, 영화관 등 다양한 서비스를 쉽게 추가할 수 있습니다.
핵심 설계 원칙
- ServiceProvider 인터페이스: 모든 서비스가 구현해야 하는 표준 계약
- ServiceRegistry: 서비스를 동적으로 등록하고 MCP 서버에 연결
- 도구 이름 네임스페이스: 서비스별 접두사 사용 (예:
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 잔여 좌석 조회: 날짜/지점/영화 기준 회차별 잔여 좌석 조회
워크플로우
- 사용자가 브랜드(다이소/롯데마트/GS25/세븐일레븐/CU/이마트24/올리브영/메가박스/롯데시네마/CGV)를 명시하면 품목상 어울리지 않아 보여도 먼저 해당 서비스 도구로 실제 조회를 수행
- 금지 예시: "핫식스는 다이소가 아니라 편의점 상품이라서 다이소 조회를 생략하겠습니다."
- 올바른 예시: "먼저 다이소 기준으로 검색해보고, 없으면 그때 다른 브랜드를 제안하겠습니다."
- 예시 해석: "안산 중앙역 주변 다이소 찾아주시고 핫식스 재고 찾아주세요"는 뒤의 핫식스 재고 요청도 다이소 기준으로 처리
- 브랜드를 명시하지 않으면 먼저 어느 브랜드를 원하는지 짧게 확인
- 재고/좌석 확인 요청 시:
- 다이소: 먼저 제품 검색 후
productId로 재고 확인 - 다이소 진열 위치:
productId+storeCode로daiso_get_display_location호출 - GS25 (필수 2단계 검색):
- 반드시 먼저
gs25_search_products로 상품 검색 (바로gs25_check_inventory호출 금지) - 검색 결과를 사용자에게 보여주고 선택 유도 (예: "1. 핫식스250ML / 2. 핫식스더킹")
- 사용자가 선택한 상품의
itemCode로gs25_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로 잔여 좌석 조회
- 결과는 핵심 정보 중심으로 보기 쉽게 정리
- 다이소/롯데마트/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 기준으로 처리
- 규칙 1: MCP/REST 응답에
주의사항
- 다이소 재고 확인 전에는 반드시 제품 검색으로 ID를 확인
- 다이소 재고 조회는
productId+keyword또는 위치(lat,lng)만으로 가능하며storeCode가 필요하지 않음 - 상품/재고 응답에
imageUrl이 있으면 답변에서 반드시 마크다운 이미지로 함께 표시함 - 상품/재고 이미지가 여러 개면 첫 번째만 남기지 말고 전부 표시함
- 답변 길이, 대표 이미지 관행, 임의 판단을 이유로 이미지를 일부만 생략하지 않음
- 다이소
storeCode는 매장 검색 결과가 아니라 재고 조회 결과에서 확보하는 값으로 취급 - 다이소
안산 중앙역같은 역명 키워드가 비면안산중앙역,안산중앙,고잔등 변형 키워드로 재시도 - 다이소 진열 위치 조회는
productId와storeCode가 모두 필요 - 다이소 매장 검색 시
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.krhttps://api.cgv.co.krhttps://oidc.cgv.co.kr
결론 요약
- Playwright(로컬 브라우저) 직접 접속:
실패- 차단 페이지 노출(Cloudflare / 비정상 접속 안내)
- 비브라우저 직접 호출(서버 IP):
실패https://api.cgv.co.kr에서 403 차단
- Zyte 프록시 + 신 API + 서명 헤더:
성공- 극장/영화/시간표 모두 실데이터 수신 확인
스크래핑 플레이북 기준 판정
- Playwright MCP로 브라우저 동작 재현
- 결과: 차단 재현(실패)
- 브라우저 요청 체인 분석
- 기존
m.cgv.co.kr/WebAPP/ReservationV5/*경로는 현재 실효성 없음 - 신 프론트 번들에서
api.cgv.co.kr티켓 API 확인
- 비브라우저 재현 시도
- 직접 호출은 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/jsonAccept-Language: ko-KRX-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
구현 전략 (우선순위)
- 브라우저 기반 성공 경로 확보
- Playwright는 차단되므로 Zyte 프록시를 브라우저 대체 경로로 사용
- 비브라우저 직접 호출 우선
- 직접 호출 시도 후 403이면 fallback
- 불가피 시 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.ts의fetchCgvTimetable을 수정해
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. 사전 준비
- Mac/iPhone 동일 Wi-Fi 연결
- iOS 프록시 및 인증서 신뢰 설정 완료
mitmproxy설치- 대상 시나리오 정의
권장 시나리오 예시:
- 로그인 상태에서 점포 선택
- 상품 검색
- 재고조회 버튼 실행
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. 실패 시 점검
- HTTPS가 안 보임
- iOS 인증서 신뢰 설정 재확인
- 트래픽이 거의 없음
- 앱 완전 종료 후 재실행
- 프록시 IP/포트 재확인
- 특정 요청만 안 보임
- 앱 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/jsonx-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. 실패 대응
403/5xx또는 빈 응답
- 앱/웹 정책 변경 가능성. 최신 캡처로 파라미터 재동기화
- DNS/연결 실패
- 실행 환경 네트워크 정책 확인(샌드박스/사내망)
- 결과는 오는데 필드 누락
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=storehttps://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.mitmcaptures/cu-20260308/requests.jsonlcaptures/cu-20260308/summary.jsoncaptures/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:
{} - 응답 키 예시:
areaCateListareaItemListareaListcuconListproductList
B. 재고 검색 핵심 API
- Endpoint:
POST /api/search/rest/stock/main - 샘플 요청:
{
"searchWord": "과자",
"prevSearchWord": "두바이",
"spellModifyUseYn": "Y",
"offset": 0,
"limit": 8,
"searchSort": "recom"
}
- 응답 필드 예시:
data.stockResult.result.total_countrows[].fields.item_cdrows[].fields.item_nmrows[].fields.hyun_maegarows[].fields.pickup_ynrows[].fields.deliv_ynrows[].fields.reserv_yn
C. 점포 조회 API (앱)
- Endpoint:
POST /api/store - 주요 입력:
latVal,longValtabIdfilterSvcListitemCd,onItemNo(상황별)
- 응답 필드 예시:
totalCntstoreList[].storeCdstoreList[].storeNm
3) 재현성 검증 (curl)
2026-03-08 기준, 아래 API는 최소 헤더로 curl 재현 성공:
POST https://www.pocketcu.co.kr/api/search/display/stockPOST https://www.pocketcu.co.kr/api/search/rest/stock/mainPOST https://www.pocketcu.co.kr/api/store
공통 최소 헤더:
content-type: application/jsonx-requested-with: XMLHttpRequest
상세 재현 명령은 docs/cu-app-scraping-replay-guide.md 참고.
4) 구현 권장안
즉시 구현
-
cu_find_nearby_stores- 1순위: 웹
list_Ajax.do파싱 - 2순위: 앱
api/store기반 좌표 조회
- 1순위: 웹
-
cu_check_inventory- 흐름:
api/search/display/stock초기 데이터api/search/rest/stock/main검색- 필요 시
item_cd기반 후속 상세 조회
- 흐름:
주의사항
- 민감정보(
Cookie, 토큰)는 저장/로그에서 마스킹 유지 - 앱 버전 변화로 파라미터가 변경될 수 있으므로, 배포 전 재실측 권장
5) 관련 문서
docs/mitmproxy-guide.mddocs/cu-app-request-capture-guide.mddocs/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 구현 가능 ✅
-
HTTP 클라이언트만으로 충분
- Puppeteer/Playwright 불필요
- fetch API로 직접 호출 가능
-
HTML 파싱 필요
- 매장 검색 응답은 HTML
- cheerio 또는 정규식으로 파싱 권장
-
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(/"/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로 리플레이 | ✅ 성공 |
| 온라인 재고 조회 | ✅ 성공 |
| 매장별 재고 조회 | ✅ 성공 |
다음 단계
- ✅ 매장 검색 API 분석 완료
- ✅ 재고 조회 API 분석 완료
- ⏳ HTML 파싱 유틸리티 구현
- ⏳ MCP 도구 통합
- ⏳ 테스트 작성
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단계: 요청 정보 분석
확인 사항:
-
✅ 쿠키 필요 여부
- 어떤 쿠키가 설정되는가?
- PHPSESSID, JSESSIONID 등?
- 쿠키 없이 요청 가능한가?
-
✅ 필수 헤더
- X-Requested-With: XMLHttpRequest
- Referer: https://www.daiso.co.kr/cs/shop
- Content-Type
- 기타 커스텀 헤더
-
✅ CSRF 토큰
- 폼에 숨겨진 토큰이 있는가?
- 헤더에 CSRF 토큰이 있는가?
-
✅ 응답 형식
- 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 호출
다음 단계
-
세션 재시작
# Claude Code 종료 후 재시작 # Playwright MCP 도구 활성화 확인 -
Playwright로 페이지 열기
다이소 매장 검색 페이지 방문 네트워크 캡처 활성화 -
검색 실행 및 분석
매장 검색 수행 네트워크 요청 분석 필요한 헤더/쿠키 파악 -
리플레이 테스트
curl로 요청 재현 성공 여부 확인 -
결과 문서화
성공 시: 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. 사전 준비
- Mac/iPhone 동일 Wi-Fi 연결
- iOS 프록시 수동 설정 및 mitm 인증서 신뢰 완료
mitmproxy설치 확인- 이마트24 앱 최신 버전 설치 및 로그인 상태 준비
- 캡처 시나리오 사전 정의
권장 시나리오:
- 앱 실행
- 점포 선택 또는 내 주변 매장 진입
- 상품 검색
- 예약픽업/오늘픽업/재고 표시 화면 진입
- 상품 상세에서 수량/재고 관련 텍스트 확인
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. 실패 시 점검
- HTTPS 요청이 거의 안 보임
- iOS 인증서 신뢰 설정 재확인
- 프록시 IP/포트 오타 확인
- 앱 핵심 요청만 누락
- certificate pinning 가능성 점검
- Android 동일 시나리오 비교 캡처
- 요청은 보이는데 의미 있는 데이터가 없음
- 시나리오를 더 구체화해 재수집(상품 상세, 장바구니, 픽업 확정 직전 단계)
9. 후속 문서화 규칙
실측 후 아래 순서로 문서를 업데이트합니다.
docs/emart24-network-analysis-result.md에 "앱 실측 섹션" 추가- 재현 가능한 엔드포인트를
curl예시와 함께 기록 - 구현 판정을 A/B/C로 업데이트
- 필요 시
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. 실패 대응
403/401발생
- 세션 의존 요청일 수 있으므로 앱 최신 캡처 기준으로 헤더/쿠키 재검증
storeGoodsQty비어 있음
searchPluCode와bizNoArr조합을 캡처값으로 다시 맞춰 재시도
- DNS/네트워크 실패
- 실행 환경 네트워크 정책(샌드박스/사내망) 확인
docs/emart24-network-analysis-result.md
5,607 bytes
이마트24 네트워크 분석 결과 (실측 기반)
작성일: 2026-03-03 (KST)
실측 도구: curl, 정적 JS 분석
대상:
https://emart24.co.kr/storehttps://emart24.co.kr/libs/FindStore.jshttps://emart24.co.kr/api1/areahttps://emart24.co.kr/api1/storehttps://emart24.co.kr/api1/goodshttps://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:5669GET /api1/store?page=1&search=강남->error:0,count:71GET /api1/store?page=1&search=강남&SVR_24=1->error:0,count:12GET /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.js의 reqData 기준 주요 파라미터:
- 기본:
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.js의loadGoods()에서url: "/api1/goods"
- 실측:
GET /api1/goods->error:0,count:2862GET /api1/goods?page=1&search=도시락->error:0,count:13GET /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
- iOS
판정:
- 매장 단위 재고는 웹 공개 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단계:
emart24_find_nearby_stores먼저 출시 - 2단계:
emart24_search_products(카탈로그 조회) 별도 도구로 분리 - 3단계: 앱 트래픽 실측 후
emart24_check_inventory구현 여부 재판정
주의:
- 2단계 도구를 "재고"로 표기하면 오해 소지가 있으므로
명확히 "상품 목록/행사 정보"로 구분하는 것이 안전합니다.
7) 다음 실측 작업
- Android/iOS
emart24앱에서 예약픽업/재고조회 시나리오 캡처 - 앱 API 엔드포인트, 인증 헤더/토큰, 필수 파라미터 확인
- 비로그인/로그인 상태별 재현성 비교
- 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. 핵심 교훈
성공 요인
- 정적 분석 우선: 네트워크 캡처 실패 시 앱 바이너리 분석으로 전환
- blutter 활용: Flutter AOT 특화 도구로 Dart 심볼/오프셋 추출
- 앱 레이어 후킹: TLS가 아닌 암복호화 함수 직접 후킹
- 오프셋 기반 접근: 클래스명이 아닌 메모리 오프셋으로 정확한 후킹
- 인증 검증: 캡처된 인증 정보 없이도 API 호출 테스트 → 인증 불필요 발견
주요 발견
앱 암호화 ≠ 서버 인증
앱 내부에서 요청/응답을 암호화하지만, 실제 서버는 인증 없이 호출 가능합니다.
이는 앱 레이어 암호화가 난독화/리버스 엔지니어링 방지 목적임을 의미합니다.
재고 조회는 2단계 프로세스
totalSearchAPI: 키워드 → itemCode 변환store/stockAPI: 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¢erPositionXCoordination=126.841342¢erPositionYCoordination=37.317730&radiusCondition=500&pickupStoreYn=N&realTimeStockYn=Y"
발견 방법: Frida v4 스크립트로 앱의 실제 요청 파라미터 평문 캡처
- 상세 가이드:
docs/gs25-frida-plaintext-capture-guide.md
9. 완료된 항목
토큰 갱신: 재고 조회 API는 인증 불필요로 확인됨 ✅MCP 도구 구현:gs25_check_inventory구현 완료 ✅좌표 파라미터 수정: 올바른 파라미터명으로 수정 ✅
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 구조와 영문/숫자 데이터는 정상적으로 캡처됩니다.
주의사항
- 오프셋 변경: 앱 업데이트 시 libapp.so 오프셋이 변경될 수 있음. blutter로 재분석 필요.
- 앱 버전: 테스트된 버전 -
5.3.35(build 1854) - 기기: 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/Ticketinghttps://www.lottecinema.co.kr/LCWS/Ticketing/TicketingData.aspxhttps://www.lottecinema.co.kr/LCWS/Common/MainData.aspx
결론 요약
- Playwright 없이 터미널 리플레이:
성공curl --form-string 'paramList=...'만으로 핵심 API 재현 성공- 별도 로그인, 쿠키, 서명 헤더 없이 응답 수신 확인
- 근처 영화관 조회:
가능GetTicketingPageTOBE응답에 극장별Latitude,Longitude,CinemaAddrSummary가 포함됨
- 상영중 영화/상영 회차 목록 조회:
가능GetTicketingPageTOBE에서 전체 영화/극장 기본 목록 확보GetPlaySequence에서 날짜/극장/영화 조합별 회차 실데이터 확보
- 남은 좌석 수 조회:
가능GetPlaySequence의TotalSeatCount,BookingSeatCount로 계산 가능- 실측 기준
remainingSeats = TotalSeatCount - BookingSeatCount
- 좌석 맵 상세 조회:
조건부 가능GetSeats로 좌석 맵은 조회되나,BookingSeats/ScreenSeatInfo.BookingCount와
GetPlaySequence.BookingSeatCount의 의미 차이가 있어
"잔여 좌석 수 소스"로는 즉시 채택하지 않는 편이 안전함
1) Playwright 기준 동작 방식 이해
A. 진입 구조
- 예매 화면은 별도 iframe 없이
/NLCHS/Ticketing단일 페이지에서 동작 - 프론트 번들:
Scripts/Dist/TicketingIndex.bundle.jsScripts/common/Common.js
- 공통 URL 매핑 함수:
GetLcwsUrls('ticket') -> /LCWS/Ticketing/TicketingData.aspxGetLcwsUrls('main') -> /LCWS/Common/MainData.aspx
B. 공통 요청 규칙
- 대부분
POST /LCWS/Ticketing/TicketingData.aspx - body 형식:
FormData- 키:
paramList - 값: JSON 문자열
- 공통 필드:
MethodNamechannelType: "HO"osType: "W"osVersion: navigator.userAgent
C. 브라우저 클릭 시 실제 호출 순서
-
예매 페이지 최초 로딩
->GetTicketingPageTOBE -
극장 선택
->GetInvisibleMoviePlayInfo
->GetPlaySequence
->GetPopupMessageOnLine -
영화 선택 후 재조회
->GetInvisibleMoviePlayInfo
->GetPlaySequence -
특정 회차 선택 후 좌석 단계 진입
->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-10cinemaID=1|0001|1016representationMovieCode=23816
- 결과:
GetSeats- 결과:
IsOK=true,ResultMessage=SUCCESS - 조건:
cinemaId=1016screenId=1201playDate=2026-03-10playSequence=1screenDivisionCode=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[]
주요 응답 필드
- 헤더/그룹:
CinemaNameKRMovieNameKRScreenDivisionNameKRBrandNm_KR
- 회차:
CinemaIDRepresentationMovieCodeMovieCodeScreenIDPlaySequencePlayDtStartTimeEndTimeScreenNameKRTotalSeatCountBookingSeatCountIsBookingYN
2026-03-10 실측 샘플
- 조건:
cinemaID=1|0001|1016(월드타워)representationMovieCode=23816(왕과 사는 남자)
- 결과:
PlaySeqsHeader.Items.length = 12PlaySeqs.Items.length = 50
- 첫 회차:
ScreenID=1201PlaySequence=1StartTime=10:40EndTime=12:47ScreenNameKR=1관 샤롯데TotalSeatCount=32BookingSeatCount=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].TotalSeatCountScreenSeatInfo.Items[0].BookingCount
주의점
- 동일 조건에서:
GetPlaySequence.BookingSeatCount = 28GetSeats.ScreenSeatInfo.BookingCount = 4
- 따라서 현재 시점에서는
GetSeats의BookingSeats/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) 정규화 매핑 제안
극장 목록
- 입력:
CinemaIDCinemaNameKRDivisionCodeDetailDivisionCodeLatitudeLongitudeCinemaAddrSummary
- 출력:
theaterIdtheaterNameregionCoderegionDetailCodelatitudelongitudeaddress
영화 목록
- 입력:
RepresentationMovieCodeMovieNameKRViewGradeNameKRPlayTimeReleaseDate
- 출력:
movieIdmovieNameratingdurationMinutesreleaseDate
상영 회차
- 입력:
CinemaIDCinemaNameKRRepresentationMovieCodeMovieNameKRScreenIDScreenNameKRPlaySequencePlayDtStartTimeEndTimeTotalSeatCountBookingSeatCount
- 출력:
scheduleIdtheaterIdtheaterNamemovieIdmovieNamescreenIdscreenNameplayDatestartTimeendTimetotalSeatsbookedSeatsremainingSeats
5) 권장 코드 구조
기존 cgv, megabox 서비스와 동일한 패턴으로 구성하는 것이 가장 자연스럽습니다.
src/services/lottecinema/
├── index.ts
├── api.ts
├── client.ts
├── types.ts
└── tools/
├── findNearbyTheaters.ts
├── listNowShowing.ts
└── getRemainingSeats.ts
파일별 역할
-
api.tsLOTTECINEMA_API.BASE_URLLOTTECINEMA_API.TICKETING_PATHLOTTECINEMA_API.MAIN_PATH- 메서드 이름 상수
GET_TICKETING_PAGEGET_PLAY_SEQUENCEGET_INVISIBLE_MOVIE_PLAY_INFOGET_SEATS
-
client.tsfetchLotteCinemaTicketingPage()fetchLotteCinemaPlaySequence()- 필요 시
fetchLotteCinemaSeats() - 공통 body 생성:
channelType: 'HO'osType: 'W'osVersion
cinemaID복합 문자열 변환 유틸 포함
-
types.ts- LCWS 응답 타입 정의
- 극장/영화/회차 정규화 타입 정의
-
tools/findNearbyTheaters.tsGetTicketingPageTOBE응답의 좌표로 거리 계산
-
tools/listNowShowing.ts- 날짜 + 극장 + 영화 조건으로
GetPlaySequence결과 반환
- 날짜 + 극장 + 영화 조건으로
-
tools/getRemainingSeats.tsGetPlaySequence의 회차별 좌석 수를 정규화해 반환
추가로 필요한 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) 다음 구현 순서 권장
GetTicketingPageTOBE기반 클라이언트 구현GetPlaySequence기반 회차/잔여좌석 구현findNearbyTheaters,listNowShowing,getRemainingSeats도구 추가- API 핸들러/라우트 연결
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.comorigin은 간헐적으로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.asp는m_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.asp와 detail_shop.asp에는 좌표(lat/lng)가 직접 포함되지 않았습니다.
즉, 이 프로젝트의 findNearbyStores 스타일 기능을 만들려면 아래 두 단계가 필요합니다.
- 롯데마트 HTML에서 주소를 수집
- 주소를 좌표로 변환 후 캐시
권장 구현 방식
기존 GS25, CU와 유사하게 지오코딩 보완 방식을 사용합니다.
- 1차 데이터 원본:
search_shop.asp에서 수집한 주소
- 2차 좌표 보완:
googleMapsApiKey가 있을 때만 Google Geocoding API 사용
- 캐시:
주소 -> 좌표결과를 메모리 캐시 또는 서비스별 캐시에 저장
도구 설계 제안
-
lottemart_find_nearby_stores- 입력:
latitudelongitudekeyword선택area선택limitgoogleMapsApiKey선택
- 동작:
- 지역별 매장 목록 수집
- 주소 지오코딩
- 거리 계산
- 가까운 순 정렬
- 입력:
-
lottemart_search_products- 입력:
querystoreCode또는storeNameareapageLimit
- 동작:
- 초기
search_product.asp호출 totalPage파싱- 필요 시
search_product_list.asp추가 호출
- 초기
- 입력:
6. 프로젝트 반영 계획
1단계. 문서화 및 캡처 고정
docs/lottemart-mobile-scraping-replay-plan.md유지- 필요 시 다음 보조 문서 추가
docs/lottemart-network-analysis-result.mdcaptures/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.tstests/app/app-api-lottemart.test.ts
8. 리스크와 대응
리스크 1. 좌표 부재
가장 큰 제약입니다.
대응:
- 주소 지오코딩을 옵션 기능으로 추가
- API 키가 없을 때는 거리 계산 없이 목록 조회만 허용
리스크 2. HTML 구조 변경
JSON API보다 마크업 변경에 취약합니다.
대응:
- 파서를 라벨 기반으로 작성
- 테스트 fixture를 충분히 확보
리스크 3. 브랜드 혼합
토이저러스, 맥스 등이 함께 내려오므로
사용자가 기대하는 “롯데마트만” 결과와 다를 수 있습니다.
대응:
brandVariant필드 추가- 기본값은 전체 노출, 필요 시 필터 옵션 제공
9. 최종 판정
현재 기준으로 롯데마트 모바일 도와센터는
이 프로젝트에 다음 순서로 편입하는 것이 가장 효율적입니다.
lottemart_find_nearby_stores먼저 구현- 주소 지오코딩 캐시로 좌표 보완
- 이후
lottemart_search_products추가 - 필요 시 입점업체/층별안내(
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/bookinghttps://www.megabox.co.kr/on/oh/ohb/SimpleBooking/simpleBookingPage.dohttps://www.megabox.co.kr/on/oh/ohz/PcntSeatChoi/selectPcntSeatChoi.dohttps://www.megabox.co.kr/theater?brchNo=1372
결론 요약
- 근처 영화관 조회:
가능(극장 정보 API에서 주소 + 위경도 추출 가능) - 상영중 영화/상영회차 목록:
가능(selectBokdList.do응답에 회차 필드 포함) - 좌석 남은량(회차별):
가능(selectBokdList.do의restSeatCnt) - 좌석 선점 가능 여부 체크:
조건부 가능selectOccupSeat.do는playSchdlNo단독 호출 실패seatOccupText포함 시 정상 응답 확인
1) Playwright 기준 동작 방식 이해
A. 진입 구조
/booking본문에서 데이터가 바로 내려오지 않음- 실제 예매 데이터는 iframe에서 동작
- iframe URL:
/on/oh/ohb/SimpleBooking/simpleBookingPage.do
- iframe URL:
B. 사용자 동작별 호출 흐름
-
영화 선택
->POST /on/oh/ohb/SimpleBooking/selectBokdList.do -
극장 선택
->POST /on/oh/ohb/SimpleBooking/selectBrchBokdUnablePopup.do
->POST /on/oh/ohb/SimpleBooking/selectBokdList.do -
좌석 페이지 로딩
- 좌석 iframe:
/on/oh/ohz/PcntSeatChoi/selectPcntSeatChoi.do - 좌석 조회:
POST /on/oh/ohz/PcntSeatChoi/selectSeatList.do
- 좌석 iframe:
-
좌석 선택 후 다음 단계
- 좌석 페이지에서
options생성 후parent.fn_goNextPagePcntSeatChoi(options)호출 - parent에서
POST /on/oh/ohb/BokdMain/selectOccupSeat.do호출
- 좌석 페이지에서
2) 핵심 엔드포인트 실측
2.1 POST /on/oh/ohb/SimpleBooking/selectBokdList.do
- 목적:
- 영화/극장/날짜 조합 기반 상영목록 조회
- 영화 목록, 극장 목록, 상영 회차, 잔여좌석 동시 수신
- 주요 요청 필드 예시:
arrMovieNo,playDebrchNoListCnt,brchNo1,areaCd1,spclbYn1,theabKindCd1sellChnlCd(ONLINE)
- 주요 응답 필드:
areaBrchList[].brchNo,brchNmmovieList[].movieNo,movieNm,movieStatCdNmmovieFormList[].playSchdlNomovieFormList[].playStartTime,playEndTimemovieFormList[].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[].seatUniqNoseatListSD01[].seatZoneCd,seatClassCdseatTicketAmtList[].ticketKindCd(예:TKA,TKY)seatTicketAmtList[].clsReclineAmt등 좌석등급별 요금
2.4 POST /on/oh/ohb/BokdMain/selectOccupSeat.do
- 목적: 좌석 선점 충돌(이미 판매 진행중 여부) 체크
- 중요 포인트:
playSchdlNo만 전달하면 실패(500)seatOccupText포함하면 정상 응답 가능
3) 리플레이 검증 결과
검증일: 2026-03-03 (KST)
A. 상영/잔여좌석 조회 리플레이
- 요청:
playDe=20260304arrMovieNo=25104500brchNo1=1372(강남)
- 결과:
- HTTP 200
- 응답에서
playSchdlNo37개,restSeatCnt37개 확인 - 회차별 시작/종료 시각 + 남은좌석 수집 가능
B. 좌석 선점 체크 리플레이
- 실패 케이스
- payload:
{"playSchdlNo":"2603041372011"} - 결과: HTTP 500,
"좌석 정보가 없습니다."
- 성공 케이스
- 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.doHTML 내 길찾기 링크에 좌표 포함:- 예:
...map.naver.com...lng=127.0264086&lat=37.498214...
- 예:
- 동일 HTML에 도로명주소 포함
구현 메모
selectBokdList의areaBrchList에서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_occupancyseatOccupText를 정확히 생성해야 함- 좌석맵(
selectSeatList)과 요금/권종 매핑 로직 필요
6) 다음 실측/구현 권장 순서
selectBokdList.do기반 도구 2종 우선 구현- 상영목록/잔여좌석
infoPage.do좌표 캐시 설계 후 근처극장 도구 구현- 좌석 선점 체크는 별도 단계로 분리
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에서 프록시 설정
- iPhone에서
설정 > Wi‑Fi > 현재 연결된 네트워크 > 프록시 구성으로 이동합니다. 수동선택 후 아래 입력:
- 서버: Mac의 로컬 IP (예:
192.168.0.10) - 포트:
8080
- 저장 후 Safari에서
http://mitm.it접속 - iOS용 인증서 프로파일을 설치
설정 > 일반 > 정보 > 인증서 신뢰 설정에서 mitmproxy 인증서를 신뢰로 활성화
주의: 5단계를 하지 않으면 HTTPS 복호화가 정상 동작하지 않습니다.
5. 앱 기반 트래픽 수집 절차
다음 순서로 수집하면 재현성이 높습니다.
mitmweb실행 상태 확인- iOS 프록시/인증서 신뢰 상태 확인
- 대상 앱 완전 종료 후 재실행
- 앱 내 실제 사용자 시나리오 수행
- 예: 포켓CU에서 점포 선택 -> 상품 검색 -> 재고 조회 버튼 실행
mitmweb에서 도메인/경로 기준으로 요청 필터링- 핵심 요청을 저장
- 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 재고 분석에 바로 적용하는 최소 실행안
mitmweb실행- iOS 프록시 + 인증서 신뢰 완료
- 포켓CU에서
점포 선택 -> 상품 검색 -> 재고 조회수행 cu.bgfretail.com또는 앱 API 도메인 요청 추출- 재고 관련 엔드포인트/헤더/파라미터를
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-storePOST /oystore/api/stock/product-search-v3
검증 환경
- OS: macOS (Darwin arm64)
- Node.js: v25.2.1
- 패키지:
@lightpanda/[email protected][email protected]
검증 방법
@lightpanda/browser설치 후 CDP 서버 실행- Playwright
connectOverCDP로 Lightpanda 제어 - 올리브영 메인 접속 후 챌린지 페이지 여부 확인
- 브라우저 컨텍스트에서 API 2종 직접 호출
- 동일 시나리오 3회 반복
lightpanda.fetch()단독 호출 결과도 확인
실행 명령
node /tmp/lightpanda-test/test-oliveyoung-loop.mjsnode -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 continueBrowser not supported안전하고 원활한 올리브영 이용을 위해 접속 정보를 확인 중
- API 호출 결과:
find-store: HTTP403product-search-v3: HTTP403
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 기반 직접 호출 가능
엔드포인트 판정
POST /oystore/api/storeFinder/find-store
- 목적: 매장 검색(위치/키워드)
- 필수 Body 필드(실측 기준):
lat,lon,pageIdx,searchWords,pogKeys,serviceKeys,mapLat,mapLon
- 실패 패턴:
- 필드 축약 시
400 Bad Request GET시405 Method Not Allowed
- 필드 축약 시
POST /oystore/api/stock/product-search-v3
- 목적: 매장 맥락 상품/재고 리스트
- 필수 Body 필드(실측 기준):
includeSoldOut,keyword,page,sort,size
- 실패 패턴:
- Body 단순화(
keyword만 전송) 시500 Internal Server Error GET시405 Method Not Allowed
- Body 단순화(
안정성/리스크
- 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 - 인증:
.env의ZYTE_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
extractAPI에서 커스텀 HTTP 요청(httpRequestMethod,httpRequestText,customHttpRequestHeaders)으로
올리브영 API 2종을 안정적으로 호출 성공 - 응답은
statusCode=200, 본문status=SUCCESS,totalCount정상 확인
- Zyte
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-store1회 왕복:10,350 bytes(약10.11 KB)product-search-v31회 왕복: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-store와product-search-v3모두 동일 적중률h- 유효 요청 수:
총요청수 = 수집건수 x 2 x (1 - h)
1천건 기준
- 캐시 없음(
h=0%):2,000요청,$0.26 ~ $2.54 h=50%:1,000요청,$0.13 ~ $1.27h=80%:400요청,$0.05 ~ $0.51h=90%:200요청,$0.03 ~ $0.25h=95%:100요청,$0.01 ~ $0.13
1만건 기준
- 캐시 없음(
h=0%):20,000요청,$2.60 ~ $25.40 h=50%:10,000요청,$1.30 ~ $12.70h=80%:4,000요청,$0.52 ~ $5.08h=90%:2,000요청,$0.26 ~ $2.54h=95%:1,000요청,$0.13 ~ $1.27
10만건 기준
- 캐시 없음(
h=0%):200,000요청,$26.00 ~ $254.00 h=50%:100,000요청,$13.00 ~ $127.00h=80%:40,000요청,$5.20 ~ $50.80h=90%:20,000요청,$2.60 ~ $25.40h=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(매장명/주소/좌표 등): TTL24hproduct metadata(상품명/가격/이미지 등): TTL10~30분inventory fields(o2oStockFlag,o2oRemainQuantity): TTL30~90초
핵심:
- 동일 API 응답이라도 저장 시 메타 데이터와 재고 필드를 분리 캐시
- 응답 조합 시
meta cache + inventory cache를 합성
재고 TTL 가변 규칙
remain <= 3: TTL20~30초4 <= remain <= 20: TTL45~60초remain > 20: TTL60~120초o2oStockFlag=false: TTL20~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_rateinventory_p95_age_secforce_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}
- 메타:
- 처리 순서:
- 메타/재고 캐시 조회
- 재고 캐시가 stale 상한 이내면 즉시 응답
- stale 상한 초과면 원본 조회 후 재고 캐시 갱신
- 메타는 긴 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.json에enableAllProjectMcpServers: 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에 연결합니다.
기준 워커: daiso-mcp · 마지막 갱신: 2026-04-22 00:49 KST
AI 앱에서 MCP 연결하기
ChatGPT, Claude, Grok 같은 AI 앱에서 바로 연결해 사용할 수 있습니다.
아래 앱별 가이드에서 먼저 연동한 뒤 검색/재고/영화 조회를 요청하세요.
MCP 연동이 어렵다는 피드백이 있어 바로 사용 가능한 GPT 앱을 추가했습니다.
아래 링크로 모바일에서도 간편하게 이용 가능합니다!
빠른 사용 예시:
다이소 mcp로 수납박스 검색해줘
올리브영 mcp로 명동 근처 매장 찾아줘
이마트24 mcp로 강남 근처 매장과 두바이 재고 알려줘
롯데마트 mcp로 잠실 근처 매장 찾아줘
롯데마트 mcp로 강변점에서 콜라 재고 알려줘
GS25 mcp로 강남 근처 매장과 오감자 재고 알려줘
세븐일레븐 mcp로 삼각김밥 검색해줘
세븐일레븐 mcp로 안산 중앙역 근처 매장 찾아줘
세븐일레븐 mcp로 안산 중앙역 근처 세븐일레븐에서 핫식스 재고 알려줘
세븐일레븐 mcp로 인기 검색어와 카탈로그 요약 알려줘
메가박스 mcp로 강남점 영화와 잔여 좌석 알려줘
롯데시네마 mcp로 월드타워 근처 지점과 상영 영화 알려줘
롯데시네마 mcp로 월드타워 잔여 좌석 알려줘
CGV mcp로 강남 상영 영화와 시간표 알려줘
Pro / Max / Team / Enterprise 플랜 필요 · 웹에서 설정 시 모바일 앱에서도 사용 가능
- claude.ai에서 Settings → Connectors 이동
- Add custom connector 클릭
- 원격 MCP 서버 URL 입력:
https://mcp.aka.page - Add 클릭하여 완료
- 대화창에서 + 버튼 → Connectors → 토글로 활성화
사용 예시:
다이소 mcp를 사용해서 수납박스 검색해줘
다이소 mcp를 사용해서 강남역 근처 매장 찾아줘
올리브영 mcp를 사용해서 명동 근처 매장 찾아줘
올리브영 mcp를 사용해서 선크림 재고 확인해줘
이마트24 mcp를 사용해서 강남 매장 찾고 두바이 재고 확인해줘
롯데마트 mcp를 사용해서 잠실 근처 매장 찾아줘
롯데마트 mcp를 사용해서 강변점에서 콜라 검색해줘
GS25 mcp를 사용해서 강남 매장 찾고 오감자 재고 확인해줘
세븐일레븐 mcp를 사용해서 안산 중앙역 근처 매장 찾고 핫식스 재고 확인해줘
메가박스 mcp를 사용해서 강남역 근처 지점 찾아줘
메가박스 mcp를 사용해서 강남점 영화 목록이랑 잔여 좌석 확인해줘
롯데시네마 mcp를 사용해서 잠실 근처 지점 찾아줘
롯데시네마 mcp를 사용해서 월드타워 영화 목록이랑 잔여 좌석 확인해줘
CGV mcp를 사용해서 서울 지역 극장 목록 찾아줘
CGV mcp를 사용해서 강남 CGV 영화랑 시간표 확인해줘
Claude Code CLI에서 MCP 서버 추가
claude mcp add daiso-mcp https://mcp.aka.page --transport sse
웹 및 모바일 앱 모두 지원
프롬프트 페이지 URL:
https://mcp.aka.page/prompt
사용 방법:
- Grok 모바일 앱에서
https://mcp.aka.page/prompt페이지를 읽어달라고 요청 - 에이전트가 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
인터랙티브 모드를 종료합니다.
미지원 서비스
| 서비스 | 상태 |
|---|---|
| ❌ 미지원 | |
| ❌ 미지원 |
Special Thanks
이 프로젝트에 도움 주신 분들께 감사드립니다.
- @thecats1105: 다이소 진열 위치 조회 도구(
daiso_get_display_location) 구현 및 API/테스트 연동 - @betterthanhajin: CGV 서비스 프로바이더 구현(극장/영화/시간표 도구, 라우트·스펙·테스트 추가)
- 제로초님: 프로젝트 홍보 도움
상세 문서
Special Thanks 이후에 있던 상세 설명은 별도 문서로 분리했습니다.
docs 문서
공통 가이드
다이소
CU
이마트24
롯데마트
공통
올리브영
- 올리브영 네트워크 분석 결과
- 올리브영 Playwright MCP 온보딩
- 올리브영 Playwright 네트워크 분석
- 올리브영 Lightpanda 검증
- 올리브영 리플레이 세션 테스트 스크립트
- 올리브영 Zyte 대역폭 테스트
- 올리브영 Zyte 리플레이 테스트
영화관
GS25
- GS25 API 리플레이 방법론 (최종)
- GS25 네트워크 분석 결과 (아카이브)
- GS25 안드로이드 우회 캡처 가이드 (아카이브)
- GS25 앱 캡처 시도 로그 (2026-03-08, 아카이브)
- GS25 앱 스크래핑 준비 가이드 (아카이브)
- GS25 세션 인계 문서 (2026-03-09, 아카이브)
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.jsonfacade 스펙,/openapi-full.json전체 스펙, 관련 테스트 반영
기능 추가 후 최소 검증 기준:
npm run typechecknpm test