8000 fix: 롯데시네마 비 JSON 원본 응답 오류 분류 by hmmhmmhm · Pull Request #305 · hmmhmmhm/daiso-mcp · GitHub
[go: up one dir, main page]

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
11 changes: 10 additions & 1 deletion src/api/lottecinemaHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServiceError } from '../core/errors.js';
import { hasInvalidCinemaQuery, CINEMA_INVALID_INPUT } from '../services/cgv/validation.js';
import { geocodeBindings } from '../utils/geocode.js';
/**
Expand All @@ -9,7 +10,12 @@ import {
fetchLotteCinemaNearbyTheaters,
resolveLotteCinemaNearestTheater,
} from '../services/lottecinema/location.js';
import { type ApiContext, errorResponse, successResponse } from './response.js';
import {
type ApiContext,
errorResponse,
serviceErrorResponse,
successResponse,
} from './response.js';

function parseOptionalNumber(value: string | undefined): number | undefined {
return value === undefined ? undefined : Number(value);
Expand Down Expand Up @@ -44,6 +50,7 @@ export async function handleLotteCinemaFindNearbyTheaters(c: ApiContext) {

return successResponse(c, result, { total: result.count, pageSize: limit });
} catch (error) {
if (error instanceof ServiceError) return serviceErrorResponse(c, error, 'lottecinema_request');
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
8000 return errorResponse(c, 'LOTTECINEMA_THEATER_SEARCH_FAILED', message, 500);
}
Expand Down Expand Up @@ -111,6 +118,7 @@ export async function handleLotteCinemaListNowShowing(c: ApiContext) {
{ total: showtimes.length },
);
} catch (error) {
if (error instanceof ServiceError) return serviceErrorResponse(c, error, 'lottecinema_request');
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
return errorResponse(c, 'LOTTECINEMA_MOVIE_LIST_FAILED', message, 500);
}
Expand Down Expand Up @@ -188,6 +196,7 @@ export async function handleLotteCinemaGetRemainingSeats(c: ApiContext) {
{ total: seats.length, pageSize: limit },
);
} catch (error) {
if (error instanceof ServiceError) return serviceErrorResponse(c, error, 'lottecinema_request');
const message = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
return errorResponse(c, 'LOTTECINEMA_SEAT_LIST_FAILED', message, 500);
}
Expand Down
18 changes: 14 additions & 4 deletions src/services/lottecinema/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServiceError } from '../../core/errors.js';
/**
* 롯데시네마 API 클라이언트
*/
Expand Down Expand Up @@ -81,7 +82,18 @@ async function requestLotteCinema<T>(
throw new Error(`롯데시네마 API 호출 실패: ${response.status}`);
}

const parsed = (await response.json()) as { IsOK?: boolean; ResultMessage?: string } & T;
const parsed = (await response.json().catch((error: unknown) => {
if (error instanceof SyntaxError) {
throw new ServiceError(
'LOTTECINEMA_INVALID_RESPONSE',
'롯데시네마 원본 응답을 읽을 수 없습니다. 잠시 후 다시 조회해주세요.',
502,
true,
response.status,
);
}
throw error;
})) as { IsOK?: boolean; ResultMessage?: string } & T;
if (parsed.IsOK === false) {
throw new Error(`롯데시네마 API 응답 실패: ${parsed.ResultMessage || 'UNKNOWN'}`);
}
Expand Down Expand Up @@ -207,9 +219,7 @@ async function fetchPlaySequenceByPair(
});
}

export async function fetchLotteCinemaNowShowing(
params: CommonFetchParams,
): Promise<{
export async function fetchLotteCinemaNowShowing(params: CommonFetchParams): Promise<{
theaters: LotteCinemaTheater[];
movies: LotteCinemaMovie[];
showtimes: LotteCinemaShowtime[];
Expand Down
23 changes: 23 additions & 0 deletions tests/api/lottecinema-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,26 @@ describe('handleLotteCinemaGetRemainingSeats', () => {
expect(ctx.json).toHaveBeenCalledWith(expect.objectContaining({ success: false, error: expect.objectContaining({ message: expect.stringContaining('위치를 좌표로 변환하지 못했습니다') }) }), 500);
});
});

it.each([
['theaters', handleLotteCinemaFindNearbyTheaters],
['movies', handleLotteCinemaListNowShowing],
['seats', handleLotteCinemaGetRemainingSeats],
])('비 JSON 원본을 %s API의 내부 오류나 빈 결과로 바꾸지 않는다', async (_name, handler) => {
mockFetch.mockResolvedValue(
new Response('<!DOCTYPE html>private upstream content', { status: 200 }),
);
const ctx = createMockContext({ lat: '37.5132941', lng: '127.104215' });
await handler(ctx);
expect(ctx.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: {
code: 'LOTTECINEMA_INVALID_RESPONSE',
message: '롯데시네마 원본 응답을 읽을 수 없습니다. 잠시 후 다시 조회해주세요.',
},
diagnostics: expect.objectContaining({ upstreamStatus: 200, retryable: true }),
}),
502,
);
});
8000
32 changes: 30 additions & 2 deletions tests/services/lottecinema/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ describe('fetchLotteCinemaNowShowing', () => {
),
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ IsOK: true, ResultMessage: 'SUCCESS', PlaySeqs: { Items: [] } })),
new Response(
JSON.stringify({ IsOK: true, ResultMessage: 'SUCCESS', PlaySeqs: { Items: [] } }),
),
);

const result = await fetchLotteCinemaNowShowing({ playDate: '20260310', theaterId: '1016' });
Expand Down Expand Up @@ -344,7 +346,9 @@ describe('fetchLotteCinemaNowShowing', () => {
),
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ IsOK: true, ResultMessage: 'SUCCESS', PlaySeqs: { Items: [] } })),
new Response(
JSON.stringify({ IsOK: true, ResultMessage: 'SUCCESS', PlaySeqs: { Items: [] } }),
),
);

const result = await fetchLotteCinemaNowShowing({ playDate: '2026-03-10', theaterId: '1016' });
Expand All @@ -367,3 +371,27 @@ describe('toYyyymmdd', () => {
expect(toYyyymmdd(new Date('2026-03-10T00:00:00.000Z'))).toBe('20260310');
});
});

it.each(['<!DOCTYPE html><html>private upstream content</html>', '{invalid'])(
'비 JSON 원본 응답을 안전한 외부 오류로 분류한다: %s',
async (body) => {
mockFetch.mockResolvedValue(new Response(body, { status: 200 }));
await expect(fetchLotteCinemaTicketingPage()).rejects.toMatchObject({
code: 'LOTTECINEMA_INVALID_RESPONSE',
status: 502,
upstreamStatus: 200,
retryable: true,
message: '롯데시네마 원본 응답을 읽을 수 없습니다. 잠시 후 다시 조회해주세요.',
});
},
);

it('JSON 본문 읽기 중 취소는 원본 형식 오류 대신 기존 시간 초과로 유지한다', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => {
throw new DOMException('aborted', 'AbortError');
},
});
await expect(fetchLotteCinemaTicketingPage()).rejects.toThrow('롯데시네마 API 호출 시간 초과');
});
Loading
0