daiso 1.0.10 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -5
- package/dist/api/actionsProxy.js +3 -0
- package/dist/api/cgvHandlers.js +15 -3
- package/dist/api/configStatus.js +2 -1
- package/dist/api/dtryxHandlers.js +113 -0
- package/dist/api/emart24Handlers.js +6 -2
- package/dist/api/gs25Handlers.js +22 -19
- package/dist/api/handlers.js +13 -5
- package/dist/api/healthCheckDefinitions.js +46 -16
- package/dist/api/healthCheckShape.js +24 -8
- package/dist/api/healthChecks.js +39 -20
- package/dist/api/operationalAuth.js +32 -0
- package/dist/api/routes/dtryxRoutes.js +22 -0
- package/dist/api/routes/healthRoutes.js +19 -11
- package/dist/api/routes/rateLimitStatsRoutes.js +274 -0
- package/dist/api/routes/registerServiceRoutes.js +37 -0
- package/dist/api/sevenelevenHandlers.js +7 -3
- package/dist/cli/commandOptions.js +24 -0
- package/dist/cli/commands/cinema.js +27 -0
- package/dist/cli.js +7 -1
- package/dist/cliHelp.js +3 -337
- package/dist/cliHelpDefinitions.js +369 -0
- package/dist/cliInteractive.js +8 -5
- package/dist/core/errors.js +29 -2
- package/dist/core/registry.js +13 -15
- package/dist/durableObjects/dailyRateLimiter.js +185 -0
- package/dist/durableObjects/rateLimitMetricsStore.js +144 -0
- package/dist/index.js +18 -31
- package/dist/middleware/dailyRateLimit.js +176 -0
- package/dist/pages/openapiFullSpec.js +2 -0
- package/dist/pages/openapiSpecActionParameters.js +429 -0
- package/dist/pages/openapiSpecActions.js +1 -391
- package/dist/pages/openapiSpecComponentsLottecinema.js +2 -2
- package/dist/pages/openapiSpecPathsDtryx.js +148 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +10 -2
- package/dist/pages/promptRetailCinemaText.js +9 -1
- package/dist/services/cgv/errors.js +13 -0
- package/dist/services/cgv/transport.js +5 -22
- package/dist/services/cu/client.js +44 -30
- package/dist/services/cu/index.js +13 -3
- package/dist/services/cu/tools/checkInventory.js +65 -45
- package/dist/services/cu/tools/findNearbyStores.js +4 -3
- package/dist/services/cu/upstreamError.js +11 -0
- package/dist/services/dtryx/api.js +25 -0
- package/dist/services/dtryx/client.js +133 -0
- package/dist/services/dtryx/index.js +24 -0
- package/dist/services/dtryx/location.js +216 -0
- package/dist/services/dtryx/tools/getRemainingSeats.js +117 -0
- package/dist/services/dtryx/tools/listCinemas.js +46 -0
- package/dist/services/dtryx/tools/listNowShowing.js +77 -0
- package/dist/services/dtryx/types.js +4 -0
- package/dist/services/dtryx/validation.js +21 -0
- package/dist/services/feedback/tools/submitDeveloperRequest.js +15 -1
- package/dist/services/gs25/api.js +2 -2
- package/dist/services/gs25/client.js +16 -123
- package/dist/services/gs25/errors.js +12 -0
- package/dist/services/gs25/index.js +2 -2
- package/dist/services/gs25/productSearch.js +2 -14
- package/dist/services/gs25/storeStockTransport.js +30 -0
- package/dist/services/gs25/tools/checkInventory.js +7 -2
- package/dist/services/gs25/tools/findNearbyStores.js +20 -5
- package/dist/services/lottecinema/client.js +4 -2
- package/dist/services/lottemart/config.js +1 -1
- package/dist/services/lottemart/session.js +33 -201
- package/dist/services/lottemart/socketTransport.js +149 -0
- package/dist/services/oliveyoung/client.js +7 -39
- package/dist/services/oliveyoung/index.js +4 -4
- package/dist/services/oliveyoung/tools/checkInventory.js +6 -3
- package/dist/services/oliveyoung/tools/findNearbyStores.js +4 -3
- package/dist/services/oliveyoung/tools/searchProducts.js +4 -3
- package/dist/services/oliveyoung/transport.js +46 -0
- package/dist/services/seveneleven/client.js +20 -9
- package/dist/services/seveneleven/index.js +11 -7
- package/dist/services/seveneleven/inventory.js +5 -2
- package/dist/services/seveneleven/productKeyword.js +3 -2
- package/dist/services/seveneleven/readCache.js +49 -0
- package/dist/services/seveneleven/tools/checkInventory.js +4 -3
- package/dist/services/seveneleven/tools/getCatalogSnapshot.js +4 -3
- package/dist/services/seveneleven/tools/getSearchPopwords.js +4 -3
- package/dist/services/seveneleven/tools/searchProducts.js +4 -3
- package/dist/services/seveneleven/tools/searchStores.js +4 -3
- package/dist/utils/format.js +10 -3
- package/dist/utils/http.js +40 -12
- package/dist/utils/zyte.js +4 -101
- package/dist/utils/zyteJsonFallback.js +28 -0
- package/openapi.json +51 -3
- package/openapi.yaml +36 -3
- package/package.json +28 -27
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 디트릭스 극장 레지스트리 및 극장 해석 보조 모듈
|
|
3
|
+
*
|
|
4
|
+
* 다른 영화관 서비스의 `location.ts` 와 같은 자리이지만, 디트릭스 응답에는
|
|
5
|
+
* 극장 좌표가 없어 좌표 기반 근접 검색 대신 극장명 매칭만 제공합니다.
|
|
6
|
+
*
|
|
7
|
+
* 디트릭스는 브랜드/극장 목록을 반환하는 공개 엔드포인트를 제공하지 않습니다.
|
|
8
|
+
* 그래서 조회로 확인된 극장을 상수로 관리합니다.
|
|
9
|
+
*
|
|
10
|
+
* 아래 목록은 상영시간표 응답의 `CinemaNm` 으로 확인한 값만 담았습니다.
|
|
11
|
+
* `indieart/000068`, `etc/000086` 두 코드는 상영 가능 날짜는 있으나
|
|
12
|
+
* 아직 편성 회차가 없어 극장명을 확인하지 못해 제외했습니다.
|
|
13
|
+
*
|
|
14
|
+
* `BrandCd` 는 조회 시 반드시 일치해야 하며, 값이 다르면 오류 없이 0건이 반환됩니다.
|
|
15
|
+
*/
|
|
16
|
+
export const DTRYX_CINEMAS = [
|
|
17
|
+
{
|
|
18
|
+
brandCode: 'indieart',
|
|
19
|
+
cinemaCode: '000053',
|
|
20
|
+
cinemaName: '씨네아트 리좀',
|
|
21
|
+
region: '경남',
|
|
22
|
+
address: '경상남도 창원시 마산합포구 동서북14길 24 (동성동)',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
brandCode: 'indieart',
|
|
26
|
+
cinemaCode: '000057',
|
|
27
|
+
cinemaName: '인디플러스포항',
|
|
28
|
+
region: '경북',
|
|
29
|
+
address: '경상북도 포항시 남구 희망대로 850, 1층 (대도동, 포항문화예술회관)',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
brandCode: 'indieart',
|
|
33
|
+
cinemaCode: '000059',
|
|
34
|
+
cinemaName: '오오극장',
|
|
35
|
+
region: '대구',
|
|
36
|
+
address: '대구광역시 중구 국채보상로 537 (수동)',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
brandCode: 'indieart',
|
|
40
|
+
cinemaCode: '000061',
|
|
41
|
+
cinemaName: '전주디지털독립영화관',
|
|
42
|
+
region: '전북',
|
|
43
|
+
address: '전라북도 전주시 완산구 전주객사3길 22, 2층 (고사동, 전주영화제작소)',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
brandCode: 'indieart',
|
|
47
|
+
cinemaCode: '000065',
|
|
48
|
+
cinemaName: '더숲아트시네마',
|
|
49
|
+
region: '서울',
|
|
50
|
+
address: '서울특별시 노원구 노해로 480, 지하1층 (상계동, 조광빌딩)',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
brandCode: 'indieart',
|
|
54
|
+
cinemaCode: '000066',
|
|
55
|
+
cinemaName: '광주극장',
|
|
56
|
+
region: '광주',
|
|
57
|
+
address: '광주광역시 동구 충장로46번길 10 (충장로5가)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
brandCode: 'indieart',
|
|
61
|
+
cinemaCode: '000067',
|
|
62
|
+
cinemaName: '아트하우스모모',
|
|
63
|
+
region: '서울',
|
|
64
|
+
address: '서울특별시 서대문구 이화여대길 52, 지하4층 (대현동, 이화여대 ECC)',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
brandCode: 'indieart',
|
|
68
|
+
cinemaCode: '000069',
|
|
69
|
+
cinemaName: '에무시네마',
|
|
70
|
+
region: '서울',
|
|
71
|
+
address: '서울특별시 종로구 경희궁1가길 7, 1~3층·지하1층 (신문로2가)',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
brandCode: 'indieart',
|
|
75
|
+
cinemaCode: '000071',
|
|
76
|
+
cinemaName: '헤이리시네마',
|
|
77
|
+
region: '경기',
|
|
78
|
+
address: '경기도 파주시 탄현면 헤이리마을길 93-119, 지상3층',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
brandCode: 'spacedog',
|
|
82
|
+
cinemaCode: '000072',
|
|
83
|
+
cinemaName: '라이카시네마',
|
|
84
|
+
region: '서울',
|
|
85
|
+
address: '서울특별시 서대문구 연희로8길 18 (연희동)',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
brandCode: 'etc',
|
|
89
|
+
cinemaCode: '000083',
|
|
90
|
+
cinemaName: '천안인생극장',
|
|
91
|
+
region: '충남',
|
|
92
|
+
address: '충청남도 천안시 동남구 버들로 41',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
brandCode: 'etc',
|
|
96
|
+
cinemaCode: '000084',
|
|
97
|
+
cinemaName: '광주자동차극장',
|
|
98
|
+
region: '광주',
|
|
99
|
+
address: '광주광역시 북구 우치로 649',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
brandCode: 'etc',
|
|
103
|
+
cinemaCode: '000088',
|
|
104
|
+
cinemaName: '아리랑시네센터',
|
|
105
|
+
region: '서울',
|
|
106
|
+
address: '서울특별시 성북구 아리랑로12길 4',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
brandCode: 'etc',
|
|
110
|
+
cinemaCode: '000092',
|
|
111
|
+
cinemaName: '밀양시네마',
|
|
112
|
+
region: '경남',
|
|
113
|
+
address: '경상남도 밀양시 백민로 75-1',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
brandCode: 'etc',
|
|
117
|
+
cinemaCode: '000097',
|
|
118
|
+
cinemaName: '모퉁이극장',
|
|
119
|
+
region: '부산',
|
|
120
|
+
address: '부산광역시 중구 광복중앙로 13, 4층',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
brandCode: 'etc',
|
|
124
|
+
cinemaCode: '000098',
|
|
125
|
+
cinemaName: '씨네인디U',
|
|
126
|
+
region: '대전',
|
|
127
|
+
address: '대전광역시 중구 계백로 1712 (문화동, 기독교연합봉사회관 1층)',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
brandCode: 'etc',
|
|
131
|
+
cinemaCode: '000100',
|
|
132
|
+
cinemaName: '애관극장',
|
|
133
|
+
region: '인천',
|
|
134
|
+
address: '인천광역시 중구 경동 238',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
brandCode: 'etc',
|
|
138
|
+
cinemaCode: '000107',
|
|
139
|
+
cinemaName: '금성시네마',
|
|
140
|
+
region: '충남',
|
|
141
|
+
address: '충청남도 부여군 부여읍 사비로100번길 12',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
brandCode: 'etc',
|
|
145
|
+
cinemaCode: '000113',
|
|
146
|
+
cinemaName: '낭만극장',
|
|
147
|
+
region: '서울',
|
|
148
|
+
address: '서울특별시 종로구 삼일대로 428, 4층',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
brandCode: 'etc',
|
|
152
|
+
cinemaCode: '000115',
|
|
153
|
+
cinemaName: '허리우드클래식',
|
|
154
|
+
region: '서울',
|
|
155
|
+
address: '서울특별시 종로구 삼일대로 428, 4층',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
brandCode: 'etc',
|
|
159
|
+
cinemaCode: '000116',
|
|
160
|
+
cinemaName: '명화극장',
|
|
161
|
+
region: '경기',
|
|
162
|
+
address: '경기도 안산시 단원구 중앙대로 921 (고잔동, 동서코아빌딩 지하2층)',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
brandCode: 'etc',
|
|
166
|
+
cinemaCode: '000117',
|
|
167
|
+
cinemaName: '자유로자동차극장',
|
|
168
|
+
region: '경기',
|
|
169
|
+
address: '경기도 파주시 탄현면 필승로 432',
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
/** 극장명 부분 일치로 카탈로그를 검색합니다. */
|
|
173
|
+
export function findCinemasByKeyword(keyword) {
|
|
174
|
+
const normalized = keyword.replace(/\s+/g, '').toLowerCase();
|
|
175
|
+
if (!normalized) {
|
|
176
|
+
return [...DTRYX_CINEMAS];
|
|
177
|
+
}
|
|
178
|
+
return DTRYX_CINEMAS.filter((cinema) => cinema.cinemaName.replace(/\s+/g, '').toLowerCase().includes(normalized));
|
|
179
|
+
}
|
|
180
|
+
/** 극장 코드로 카탈로그 항목을 찾습니다. */
|
|
181
|
+
export function findCinemaByCode(cinemaCode) {
|
|
182
|
+
return DTRYX_CINEMAS.find((cinema) => cinema.cinemaCode === cinemaCode);
|
|
183
|
+
}
|
|
184
|
+
/** 광역 지역명으로 극장을 찾습니다. 부분 일치를 허용합니다. */
|
|
185
|
+
export function findCinemasByRegion(region) {
|
|
186
|
+
const normalized = region.replace(/\s+/g, '');
|
|
187
|
+
if (!normalized) {
|
|
188
|
+
return [...DTRYX_CINEMAS];
|
|
189
|
+
}
|
|
190
|
+
return DTRYX_CINEMAS.filter((cinema) => cinema.region.includes(normalized));
|
|
191
|
+
}
|
|
192
|
+
/** 극장 코드와 검색 필터를 함께 적용하며, 미등록 코드는 브랜드와 함께 허용합니다. */
|
|
193
|
+
export function resolveDtryxCinemas(args) {
|
|
194
|
+
let cinemas = [...DTRYX_CINEMAS];
|
|
195
|
+
if (args.cinemaCode) {
|
|
196
|
+
const known = findCinemaByCode(args.cinemaCode);
|
|
197
|
+
cinemas = known
|
|
198
|
+
? [known]
|
|
199
|
+
: args.brandCode
|
|
200
|
+
? [
|
|
201
|
+
{
|
|
202
|
+
cinemaCode: args.cinemaCode,
|
|
203
|
+
brandCode: args.brandCode,
|
|
204
|
+
cinemaName: '',
|
|
205
|
+
region: '',
|
|
206
|
+
address: '',
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
: [];
|
|
210
|
+
}
|
|
211
|
+
const keywordMatches = args.keyword ? findCinemasByKeyword(args.keyword) : undefined;
|
|
212
|
+
const regionMatches = args.region ? findCinemasByRegion(args.region) : undefined;
|
|
213
|
+
return cinemas.filter((cinema) => (!keywordMatches || keywordMatches.includes(cinema)) &&
|
|
214
|
+
(!regionMatches || regionMatches.includes(cinema)) &&
|
|
215
|
+
(!args.brandCode || cinema.brandCode === args.brandCode));
|
|
216
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 디트릭스 상영시간표·잔여 좌석 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { DTRYX_INVALID_INPUT, hasInvalidDtryxOptions, dtryxLimitSchema, dtryxTimeoutSchema, dtryxDateSchema, } from '../validation.js';
|
|
6
|
+
import { resolveDtryxCinemas } from '../location.js';
|
|
7
|
+
import { fetchDtryxTimetable, toYyyymmdd } from '../client.js';
|
|
8
|
+
function matchesMovie(showtime, movieName) {
|
|
9
|
+
if (!movieName) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const normalize = (value) => value.replace(/\s+/g, '').toLowerCase();
|
|
13
|
+
return normalize(showtime.movieName).includes(normalize(movieName));
|
|
14
|
+
}
|
|
15
|
+
async function getRemainingSeats(args) {
|
|
16
|
+
if (hasInvalidDtryxOptions(args)) {
|
|
17
|
+
return { isError: true, content: [{ type: 'text', text: DTRYX_INVALID_INPUT }] };
|
|
18
|
+
}
|
|
19
|
+
const { playDate = toYyyymmdd(), movieName, limit = 50, timeoutMs = 15000 } = args;
|
|
20
|
+
const cinemas = resolveDtryxCinemas(args);
|
|
21
|
+
if (cinemas.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: JSON.stringify({
|
|
27
|
+
error: '극장을 찾을 수 없습니다. 카탈로그에 없는 극장 코드는 brandCode 와 함께 지정해야 합니다. 목록은 dtryx_list_cinemas 로 확인할 수 있습니다.',
|
|
28
|
+
filters: { cinemaCode: args.cinemaCode || null, keyword: args.keyword || null },
|
|
29
|
+
}, null, 2),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const settled = await Promise.allSettled(cinemas.map((cinema) => fetchDtryxTimetable({
|
|
36
|
+
brandCode: cinema.brandCode,
|
|
37
|
+
cinemaCode: cinema.cinemaCode,
|
|
38
|
+
playDate,
|
|
39
|
+
timeout: timeoutMs,
|
|
40
|
+
})));
|
|
41
|
+
const failedCinemas = cinemas
|
|
42
|
+
.filter((_, index) => settled[index]?.status === 'rejected')
|
|
43
|
+
.map((cinema) => cinema.cinemaName || cinema.cinemaCode);
|
|
44
|
+
if (failedCinemas.length === cinemas.length) {
|
|
45
|
+
return {
|
|
46
|
+
isError: true,
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: JSON.stringify({ error: '모든 극장 조회에 실패했습니다.', failedCinemas }),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const showtimes = settled
|
|
56
|
+
.flatMap((result) => (result.status === 'fulfilled' ? result.value : []))
|
|
57
|
+
.filter((showtime) => matchesMovie(showtime, movieName))
|
|
58
|
+
.sort((a, b) => {
|
|
59
|
+
if (a.startTime === b.startTime) {
|
|
60
|
+
return a.cinemaName.localeCompare(b.cinemaName);
|
|
61
|
+
}
|
|
62
|
+
return a.startTime.localeCompare(b.startTime);
|
|
63
|
+
})
|
|
64
|
+
.slice(0, limit);
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: JSON.stringify({
|
|
70
|
+
playDate,
|
|
71
|
+
filters: {
|
|
72
|
+
cinemaCode: args.cinemaCode || null,
|
|
73
|
+
keyword: args.keyword || null,
|
|
74
|
+
region: args.region || null,
|
|
75
|
+
movieName: movieName || null,
|
|
76
|
+
limit,
|
|
77
|
+
},
|
|
78
|
+
searchedCinemaCount: cinemas.length,
|
|
79
|
+
failedCinemas,
|
|
80
|
+
count: showtimes.length,
|
|
81
|
+
showtimes,
|
|
82
|
+
}, null, 2),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function createGetRemainingSeatsTool() {
|
|
88
|
+
return {
|
|
89
|
+
name: 'dtryx_get_remaining_seats',
|
|
90
|
+
metadata: {
|
|
91
|
+
title: '디트릭스 상영시간표·잔여 좌석 조회',
|
|
92
|
+
description: '디트릭스 독립·예술영화관의 상영 회차와 잔여 좌석을 조회합니다. 극장을 지정하지 않으면 등록된 전체 극장을 조회하므로 특정 영화의 상영관을 찾을 때 movieName 을 함께 쓸 수 있습니다.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
playDate: dtryxDateSchema
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('조회 날짜(YYYYMMDD 또는 YYYY-MM-DD, 기본값: 오늘)'),
|
|
97
|
+
cinemaCode: z.string().optional().describe('극장 코드 (예: 000067)'),
|
|
98
|
+
keyword: z.string().optional().describe('극장명 키워드 (예: 모모, 광주)'),
|
|
99
|
+
region: z
|
|
100
|
+
.string()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('광역 지역명으로 조회 범위를 좁힙니다 (예: 서울, 경기)'),
|
|
103
|
+
brandCode: z
|
|
104
|
+
.string()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe('브랜드 코드. 카탈로그에 없는 극장 코드를 직접 조회할 때 함께 지정합니다.'),
|
|
107
|
+
movieName: z.string().optional().describe('영화명 부분 일치 필터 (예: 경멸)'),
|
|
108
|
+
limit: dtryxLimitSchema.optional().default(50).describe('반환할 최대 회차 수 (기본값: 50)'),
|
|
109
|
+
timeoutMs: dtryxTimeoutSchema
|
|
110
|
+
.optional()
|
|
111
|
+
.default(15000)
|
|
112
|
+
.describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
handler: ((args) => getRemainingSeats(args)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 디트릭스 극장 목록 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { DTRYX_INVALID_INPUT, hasInvalidDtryxOptions, dtryxLimitSchema } from '../validation.js';
|
|
6
|
+
import { resolveDtryxCinemas } from '../location.js';
|
|
7
|
+
async function listCinemas(args) {
|
|
8
|
+
if (hasInvalidDtryxOptions(args)) {
|
|
9
|
+
return { isError: true, content: [{ type: 'text', text: DTRYX_INVALID_INPUT }] };
|
|
10
|
+
}
|
|
11
|
+
const { keyword, region, brandCode, limit = 50 } = args;
|
|
12
|
+
const matched = resolveDtryxCinemas(args).slice(0, limit);
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
filters: {
|
|
19
|
+
keyword: keyword || null,
|
|
20
|
+
region: region || null,
|
|
21
|
+
brandCode: brandCode || null,
|
|
22
|
+
limit,
|
|
23
|
+
},
|
|
24
|
+
count: matched.length,
|
|
25
|
+
cinemas: matched,
|
|
26
|
+
}, null, 2),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function createListCinemasTool() {
|
|
32
|
+
return {
|
|
33
|
+
name: 'dtryx_list_cinemas',
|
|
34
|
+
metadata: {
|
|
35
|
+
title: '디트릭스 극장 목록 조회',
|
|
36
|
+
description: '디트릭스 예매 플랫폼을 사용하는 독립·예술영화관 목록을 조회합니다. 극장명 키워드나 브랜드 코드로 걸러낼 수 있습니다.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
keyword: z.string().optional().describe('극장명 키워드 (예: 모모, 광주)'),
|
|
39
|
+
region: z.string().optional().describe('광역 지역명 (예: 서울, 경기, 부산)'),
|
|
40
|
+
brandCode: z.string().optional().describe('브랜드 코드 (indieart, etc, spacedog 중 하나)'),
|
|
41
|
+
limit: dtryxLimitSchema.optional().default(50).describe('반환할 최대 극장 수 (기본값: 50)'),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
handler: ((args) => listCinemas(args)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 디트릭스 현재 상영작 조회 도구
|
|
3
|
+
*/
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { DTRYX_INVALID_INPUT, hasInvalidDtryxOptions, dtryxTimeoutSchema } from '../validation.js';
|
|
6
|
+
import { resolveDtryxCinemas } from '../location.js';
|
|
7
|
+
import { fetchDtryxNowShowing, fetchDtryxPlayDates } from '../client.js';
|
|
8
|
+
async function listNowShowing(args) {
|
|
9
|
+
if (hasInvalidDtryxOptions(args)) {
|
|
10
|
+
return { isError: true, content: [{ type: 'text', text: DTRYX_INVALID_INPUT }] };
|
|
11
|
+
}
|
|
12
|
+
const { includePlayDates = false, timeoutMs = 15000 } = args;
|
|
13
|
+
const cinema = args.cinemaCode || args.keyword ? resolveDtryxCinemas(args)[0] : undefined;
|
|
14
|
+
if (!cinema) {
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'text',
|
|
19
|
+
text: JSON.stringify({
|
|
20
|
+
error: '극장을 찾을 수 없습니다. cinemaCode 를 직접 지정하거나 keyword 로 극장을 지정하세요. 목록은 dtryx_list_cinemas 로 확인할 수 있습니다.',
|
|
21
|
+
filters: { cinemaCode: args.cinemaCode || null, keyword: args.keyword || null },
|
|
22
|
+
}, null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const request = {
|
|
29
|
+
brandCode: cinema.brandCode,
|
|
30
|
+
cinemaCode: cinema.cinemaCode,
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
};
|
|
33
|
+
const [movies, playDates] = await Promise.all([
|
|
34
|
+
fetchDtryxNowShowing(request),
|
|
35
|
+
includePlayDates ? fetchDtryxPlayDates(request) : Promise.resolve([]),
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
cinema,
|
|
43
|
+
count: movies.length,
|
|
44
|
+
movies,
|
|
45
|
+
playDates: includePlayDates ? playDates : undefined,
|
|
46
|
+
}, null, 2),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createListNowShowingTool() {
|
|
52
|
+
return {
|
|
53
|
+
name: 'dtryx_list_now_showing',
|
|
54
|
+
metadata: {
|
|
55
|
+
title: '디트릭스 현재 상영작 조회',
|
|
56
|
+
description: '디트릭스 독립·예술영화관의 현재 상영작을 조회합니다. 필요하면 예매 가능한 상영 날짜 목록도 함께 반환합니다.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
cinemaCode: z.string().optional().describe('극장 코드 (예: 000067)'),
|
|
59
|
+
keyword: z.string().optional().describe('극장명 키워드 (예: 모모, 라이카)'),
|
|
60
|
+
brandCode: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('브랜드 코드. 카탈로그에 없는 극장 코드를 직접 조회할 때 함께 지정합니다.'),
|
|
64
|
+
includePlayDates: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.optional()
|
|
67
|
+
.default(false)
|
|
68
|
+
.describe('예매 가능한 상영 날짜 목록 포함 여부 (기본값: false)'),
|
|
69
|
+
timeoutMs: dtryxTimeoutSchema
|
|
70
|
+
.optional()
|
|
71
|
+
.default(15000)
|
|
72
|
+
.describe('요청 제한 시간(ms, 기본값: 15000)'),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
handler: ((args) => listNowShowing(args)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** API와 MCP에서 공통으로 사용하는 디트릭스 입력 검증 */
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
export const dtryxLimitSchema = z.number().int().min(1).max(200);
|
|
4
|
+
export const dtryxTimeoutSchema = z.number().int().min(1).max(60000);
|
|
5
|
+
export const dtryxDateSchema = z.string().refine((value) => {
|
|
6
|
+
if (!/^(\d{8}|\d{4}-\d{2}-\d{2})$/.test(value))
|
|
7
|
+
return false;
|
|
8
|
+
const compact = value.replace(/-/g, '');
|
|
9
|
+
const dashed = `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`;
|
|
10
|
+
const date = new Date(`${dashed}T00:00:00Z`);
|
|
11
|
+
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === dashed;
|
|
12
|
+
});
|
|
13
|
+
const optionsSchema = z.object({
|
|
14
|
+
limit: dtryxLimitSchema.optional(),
|
|
15
|
+
timeoutMs: dtryxTimeoutSchema.optional(),
|
|
16
|
+
playDate: dtryxDateSchema.optional(),
|
|
17
|
+
});
|
|
18
|
+
export const DTRYX_INVALID_INPUT = 'limit은 1~200, timeoutMs는 1~60000 사이의 정수이며 playDate는 유효한 YYYYMMDD 또는 YYYY-MM-DD 날짜여야 합니다.';
|
|
19
|
+
export function hasInvalidDtryxOptions(options) {
|
|
20
|
+
return !optionsSchema.safeParse(options).success;
|
|
21
|
+
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { submitDeveloperRequest } from '../client.js';
|
|
3
|
+
const jsonValueSchema = z.lazy(() => z.union([
|
|
4
|
+
z.string(),
|
|
5
|
+
z.int(),
|
|
6
|
+
z.number(),
|
|
7
|
+
z.boolean(),
|
|
8
|
+
z.null(),
|
|
9
|
+
z.array(jsonValueSchema),
|
|
10
|
+
// JSON 객체의 키는 항상 문자열이므로 HA가 지원하지 않는 중복 제약만 생략한다.
|
|
11
|
+
z.record(z.string(), jsonValueSchema).meta({ propertyNames: undefined }),
|
|
12
|
+
]));
|
|
3
13
|
export function createSubmitDeveloperRequestTool(config) {
|
|
4
14
|
return {
|
|
5
15
|
name: 'submit_developer_request',
|
|
@@ -29,7 +39,11 @@ export function createSubmitDeveloperRequestTool(config) {
|
|
|
29
39
|
.optional()
|
|
30
40
|
.default('mcp')
|
|
31
41
|
.describe('요청 출처'),
|
|
32
|
-
userContext: z
|
|
42
|
+
userContext: z
|
|
43
|
+
.record(z.string(), jsonValueSchema)
|
|
44
|
+
.meta({ propertyNames: undefined })
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('추가 컨텍스트 JSON'),
|
|
33
47
|
},
|
|
34
48
|
outputSchema: {
|
|
35
49
|
id: z.string().optional(),
|
|
@@ -6,7 +6,7 @@ export const GS25_API = {
|
|
|
6
6
|
STORE_STOCK_PATH: '/api/bff/v2/store/stock',
|
|
7
7
|
APIGW_BASE_URL: 'https://b2c-apigw.woodongs.com',
|
|
8
8
|
TOTAL_SEARCH_PATH: '/search/v3/totalSearch',
|
|
9
|
-
WEB_BASE_URL: 'https://
|
|
9
|
+
WEB_BASE_URL: 'https://www.gsretail.com',
|
|
10
10
|
WEB_LOCATIONS_PATH: '/gscvs/ko/store-services/locations',
|
|
11
|
-
WEB_LOCATION_LIST_PATH: '/
|
|
11
|
+
WEB_LOCATION_LIST_PATH: '/api/homepage/brand/storeSearch/selectGs25Stores',
|
|
12
12
|
};
|