Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/cgv-network-analysis-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CGV 네트워크 분석 결과 (1차 구현용)

작성일: 2026-03-04 (KST)
대상: `https://m.cgv.co.kr`

## 목적

플러그인 아키텍처에 CGV 서비스를 1차 추가하기 위한 최소 엔드포인트를 정리합니다.

## 1차 구현 기준 엔드포인트

- `POST /WebAPP/ReservationV5/Reservation.aspx/GetTheaterList`
- 사용 목적: 상영 가능 극장 목록 조회
- 입력 필드(예시): `PlayYMD`, `AreaCd`

- `POST /WebAPP/ReservationV5/Reservation.aspx/GetMovieList`
- 사용 목적: 날짜/극장 기준 영화 목록 조회
- 입력 필드(예시): `PlayYMD`, `TheaterCd`

- `POST /WebAPP/ReservationV5/Reservation.aspx/GetTimeTableList`
- 사용 목적: 날짜/극장/영화 기준 상영 시간표 조회
- 입력 필드(예시): `PlayYMD`, `TheaterCd`, `MovieCd`

## 응답 정규화 규칙

### 극장 목록

- 입력 필드: `TheaterCd`, `TheaterName`, `AreaCd`
- 정규화: `theaterCode`, `theaterName`, `regionCode`

### 영화 목록

- 입력 필드: `MovieCd`, `MovieName`, `Grade`
- 정규화: `movieCode`, `movieName`, `rating`

### 시간표

- 입력 필드:
- `ScheduleNo`, `MovieCd`, `MovieName`
- `TheaterCd`, `TheaterName`
- `PlayYmd`, `StartTime`, `EndTime`
- `TotalSeat`, `RemainSeat`
- 정규화:
- `scheduleId`, `movieCode`, `movieName`
- `theaterCode`, `theaterName`
- `playDate`, `startTime`, `endTime`
- `totalSeats`, `remainingSeats`

## 도구 매핑

- `cgv_find_theaters`: 극장 목록 조회
- `cgv_search_movies`: 영화 목록 조회
- `cgv_get_timetable`: 시간표/잔여좌석 조회

## 1차 구현 범위

- MCP 도구 3종 등록
- GET API 3종 제공 (`/api/cgv/theaters`, `/api/cgv/movies`, `/api/cgv/timetable`)
- OpenAPI/프롬프트 문서 반영
- 단위 테스트 및 통합 테스트 추가

## 후속 개선 항목

- 지역 코드 매핑 테이블 고도화
- 상영 포맷(2D/IMAX/4DX) 상세 필드 정규화
- 실좌석 선택 단계 API 분석 및 좌석맵 제공
123 changes: 123 additions & 0 deletions src/api/cgvHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* CGV GET API 핸들러
*/

import { fetchCgvMovies, fetchCgvTheaters, fetchCgvTimetable, toYyyymmdd } from '../services/cgv/client.js';
import { type ApiContext, errorResponse, successResponse } from './response.js';

/**
* CGV 극장 목록 조회 API 핸들러
* GET /api/cgv/theaters?playDate={YYYYMMDD}&regionCode={지역코드}
*/
export async function handleCgvFindTheaters(c: ApiContext) {
const playDate = c.req.query('playDate') || toYyyymmdd();
const regionCode = c.req.query('regionCode') || undefined;
const limit = parseInt(c.req.query('limit') || '30');
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');

try {
const theaters = await fetchCgvTheaters({
playDate,
regionCode,
timeout: timeoutMs,
});

const sliced = theaters.slice(0, limit);

return successResponse(
c,
{
playDate,
filters: {
regionCode: regionCode || null,
},
theaters: sliced,
},
{ total: sliced.length, pageSize: limit },
);
} catch (error) {
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
return errorResponse(c, 'CGV_THEATER_SEARCH_FAILED', message, 500);
}
}

/**
* CGV 영화 목록 조회 API 핸들러
* GET /api/cgv/movies?playDate={YYYYMMDD}&theaterCode={극장코드}
*/
export async function handleCgvSearchMovies(c: ApiContext) {
const playDate = c.req.query('playDate') || toYyyymmdd();
const theaterCode = c.req.query('theaterCode') || undefined;
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');

try {
const movies = await fetchCgvMovies({
playDate,
theaterCode,
timeout: timeoutMs,
});

return successResponse(
c,
{
playDate,
filters: {
theaterCode: theaterCode || null,
},
movies,
},
{ total: movies.length },
);
} catch (error) {
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
return errorResponse(c, 'CGV_MOVIE_SEARCH_FAILED', message, 500);
}
}

/**
* CGV 시간표 조회 API 핸들러
* GET /api/cgv/timetable?playDate={YYYYMMDD}&theaterCode={극장코드}&movieCode={영화코드}
*/
export async function handleCgvGetTimetable(c: ApiContext) {
const playDate = c.req.query('playDate') || toYyyymmdd();
const theaterCode = c.req.query('theaterCode') || undefined;
const movieCode = c.req.query('movieCode') || undefined;
const limit = parseInt(c.req.query('limit') || '50');
const timeoutMs = parseInt(c.req.query('timeoutMs') || '15000');

try {
const timetable = await fetchCgvTimetable({
playDate,
theaterCode,
movieCode,
timeout: timeoutMs,
});

const filtered = timetable
.filter((item) => (theaterCode ? item.theaterCode === theaterCode : true))
.filter((item) => (movieCode ? item.movieCode === movieCode : true))
.sort((a, b) => {
if (a.startTime === b.startTime) {
return a.theaterName.localeCompare(b.theaterName);
}
return a.startTime.localeCompare(b.startTime);
})
.slice(0, limit);

return successResponse(
c,
{
playDate,
filters: {
theaterCode: theaterCode || null,
movieCode: movieCode || null,
},
timetable: filtered,
},
{ total: filtered.length, pageSize: limit },
);
} catch (error) {
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
return errorResponse(c, 'CGV_TIMETABLE_FETCH_FAILED', message, 500);
}
}
46 changes: 46 additions & 0 deletions src/api/routes/cgvRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* CGV GET API 라우트 등록
*/

import type { Hono } from 'hono';
import { withEdgeCache } from '../../utils/cache.js';
import { handleCgvFindTheaters, handleCgvGetTimetable, handleCgvSearchMovies } from '../cgvHandlers.js';
import type { AppBindings } from '../response.js';

export function registerCgvRoutes(app: Hono<{ Bindings: AppBindings }>): void {
app.get('/api/cgv/theaters', async (c) =>
withEdgeCache(
c.req.url,
{
ttlSeconds: 60 * 60 * 24,
staleWhileRevalidateSeconds: 60 * 5,
keyPrefix: 'cgv-theaters-v1',
},
() => handleCgvFindTheaters(c),
),
);

app.get('/api/cgv/movies', async (c) =>
withEdgeCache(
c.req.url,
{
ttlSeconds: 60 * 10,
staleWhileRevalidateSeconds: 60,
keyPrefix: 'cgv-movies-v1',
},
() => handleCgvSearchMovies(c),
),
);

app.get('/api/cgv/timetable', async (c) =>
withEdgeCache(
c.req.url,
{
ttlSeconds: 60 * 3,
staleWhileRevalidateSeconds: 30,
keyPrefix: 'cgv-timetable-v1',
},
() => handleCgvGetTimetable(c),
),
);
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import { ServiceRegistry } from './core/registry.js';
import { createDaisoService } from './services/daiso/index.js';
import { createOliveyoungService } from './services/oliveyoung/index.js';
import { createMegaboxService } from './services/megabox/index.js';
import { createCgvService } from './services/cgv/index.js';
import { createPromptResponse } from './pages/prompt.js';
import { createOpenApiJsonResponse, createOpenApiYamlResponse } from './pages/openapi.js';
import { createPrivacyResponse } from './pages/privacy.js';
import type { AppBindings } from './api/response.js';
import { registerDaisoRoutes } from './api/routes/daisoRoutes.js';
import { registerOliveyoungRoutes } from './api/routes/oliveyoungRoutes.js';
import { registerMegaboxRoutes } from './api/routes/megaboxRoutes.js';
import { registerCgvRoutes } from './api/routes/cgvRoutes.js';

// 서버 메타데이터
const SERVER_NAME = 'multi-service-mcp';
Expand All @@ -35,6 +37,7 @@ const createRegistry = (bindings?: AppBindings) => {
registry.registerAll([
createDaisoService,
createMegaboxService,
createCgvService,
() =>
createOliveyoungService({
zyteApiKey: bindings?.ZYTE_API_KEY,
Expand Down Expand Up @@ -132,6 +135,7 @@ app.get('/privacy', (c) => {
registerDaisoRoutes(app);
registerOliveyoungRoutes(app);
registerMegaboxRoutes(app);
registerCgvRoutes(app);

// MCP 엔드포인트
app.all('/mcp', async (c) => {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/openapiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { OPENAPI_PATHS_DAISO_OLIVEYOUNG } from './openapiSpecPathsDaisoOliveyoung.js';
import { OPENAPI_PATHS_MEGABOX } from './openapiSpecPathsMegabox.js';
import { OPENAPI_PATHS_CGV } from './openapiSpecPathsCgv.js';
import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';

/**
Expand All @@ -21,6 +22,7 @@ export function generateOpenApiSpec(baseUrl: string): object {
- 🏪 **매장 찾기**: 지역/키워드로 매장 검색
- 📦 **재고 확인**: 온라인 및 오프라인 매장 재고 조회
- 🎬 **메가박스 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
- 🎥 **CGV 지점/영화 조회**: 극장 목록, 영화 목록, 시간표 조회

## 사용 팁
1. 한글 검색어는 URL 인코딩이 자동 처리됩니다
Expand All @@ -36,6 +38,7 @@ export function generateOpenApiSpec(baseUrl: string): object {
paths: {
...OPENAPI_PATHS_DAISO_OLIVEYOUNG,
...OPENAPI_PATHS_MEGABOX,
...OPENAPI_PATHS_CGV,
},
components: OPENAPI_COMPONENTS,
};
Expand Down
Loading
Loading