8000 fix: 공식 인기 검색어 빈 응답 구분 보정 by hmmhmmhm · Pull Request #302 · 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
2 changes: 1 addition & 1 deletion src/services/seveneleven/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export async function fetchSevenElevenSearchPopwords(
options,
);

return parseSevenElevenPopwords(response.data);
return parseSevenElevenPopwords(response);
}

export async function fetchSevenElevenStockProductMeta(
Expand Down
17 changes: 13 additions & 4 deletions src/services/seveneleven/popwords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ const entry = z.union([
z.object({ name: text }).transform((v) => v.name),
]);
const words = z.array(entry);
const response = z.union([
const dataSchema = z.union([
words,
z
.object({})
.strict()
.transform(() => []),
z
.object({
keywords: words.optional(),
Expand All @@ -23,14 +27,19 @@ const response = z.union([
.refine((lists) => lists.length > 0)
.transform((lists) => lists.find((list) => list.length > 0) ?? []),
]);
export function parseSevenElevenPopwords(data: unknown): string[] {
const parsed = response.safeParse(data);
const response = z.object({
success: z.literal(true).optional(),
code: z.number().int().min(200).max(299).optional(),
data: dataSchema,
});
export function parseSevenElevenPopwords(envelope: unknown): string[] {
const parsed = response.safeParse(envelope);
if (!parsed.success)
throw new ServiceError(
'SEVENELEVEN_INVALID_RESPONSE',
'세븐일레븐 인기 검색어 응답 형식을 확인할 수 없습니다.',
502,
true,
);
return parsed.data;
return parsed.data.data;
}
17 changes: 13 additions & 4 deletions src/services/seveneleven/tools/getSearchPopwords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ async function getSearchPopwords(
available: keywords.length > 0,
count: keywords.length,
keywords,
...(keywords.length === 0 ? { reason: 'upstream_empty' } : {}),
note:
keywords.length === 0
? '현재 응답에서 인기 검색어 목록을 찾지 못했습니다.'
? '공식 API가 빈 목록을 반환했습니다. 현재 제공되는 인기 검색어를 찾지 못했습니다.'
: '홈 인기 검색어를 조회했습니다.',
},
null,
Expand All @@ -48,17 +49,25 @@ async function getSearchPopwords(
};
}

export function createGetSearchPopwordsTool(zyteApiKey?: string, transport: ConvenienceTransportOptions = {}): ToolRegistration {
export function createGetSearchPopwordsTool(
zyteApiKey?: string,
transport: ConvenienceTransportOptions = {},
): ToolRegistration {
return {
name: 'seveneleven_get_search_popwords',
metadata: {
title: '세븐일레븐 인기 검색어 조회',
description: '세븐일레븐 인기 검색어 목록을 조회합니다.',
inputSchema: {
label: z.string().optional().default('home').describe('조회 라벨 (기본값: home)'),
timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
timeoutMs: z
.number()
.optional()
.default(15000)
.describe('요청 제한 시간(ms, 기본값: 15000)'),
},
},
handler: (args: unknown) => getSearchPopwords(args as GetSearchPopwordsArgs, zyteApiKey, transport),
handler: (args: unknown) =>
getSearchPopwords(args as GetSearchPopwordsArgs, zyteApiKey, transport),
};
}
2 changes: 1 addition & 1 deletion tests/app/app-api-seveneleven.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('GET /api/seveneleven/popwords', () => {
});

it('형식이 잘못된 인기 검색어 원본은 빈 성공 대신 502를 반환한다', async () => {
mockFetch.mockResolvedValue(Response.json({ success: true, data: {} }));
mockFetch.mockResolvedValue(Response.json({ success: true, data: { unexpected: true } }));
const res = await app.request('/api/seveneleven/popwords?label=home');
expect(res.status).toBe(502);
expect((await res.json()).error.code).toBe('SEVENELEVEN_INVALID_RESPONSE');
Expand Down
15 changes: 12 additions & 3 deletions tests/services/seveneleven/popwords.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,27 @@ it.each([
[{ wordList: ['커피'] }, ['커피']],
[{ list: ['커피'] }, ['커피']],
[[], []],
[{}, []],
[{ keywords: [], list: ['커피'] }, ['커피']],
[{ keywords: [], popwords: [], wordList: [], list: [] }, []],
])('공개 검색어 배열/이름을 명시적으로 해석한다: %j', (input, output) => {
expect(parseSevenElevenPopwords(input)).toEqual(output);
expect(parseSevenElevenPopwords({ success: true, code: 200, data: input })).toEqual(output);
});
it.each([
{},
null,
{ surprise: [] },
{ list: [null] },
{ keywords: [''] },
{ list: [{ name: 4 }] },
])('모르는 형식을 빈 목록으로 위장하지 않는다: %j', (input) => {
expect(() => parseSevenElevenPopwords(input)).toThrow('인기 검색어 응답');
expect(() => parseSevenElevenPopwords({ success: true, code: 200, data: input })).toThrow(
'인기 검색어 응답',
);
});

it.each([{ success: false, data: {} }, { success: true, code: 501, data: {} }, {}])(
'실패 envelope와 누락된 data를 거절한다: %j',
(input) => {
expect(() => parseSevenElevenPopwords(input)).toThrow('인기 검색어 응답');
},
);
22 changes: 21 additions & 1 deletion tests/services/seveneleven/tools/getSearchPopwords.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,34 @@ describe('createGetSearchPopwordsTool', () => {
expect(parsed.note).toContain('찾지 못했습니다');
});

it.each([{}, { unexpected: true }, null, { keywords: [null] }])(
it.each([{ unexpected: true }, null, { keywords: [null] }])(
'해석할 수 없는 원본은 빈 성공으로 바꾸지 않는다: %j',
async (data) => {
mockFetch.mockResolvedValue(Response.json({ success: true, data }));
await expect(createGetSearchPopwordsTool().handler({})).rejects.toThrow('인기 검색어 응답');
},
);

it('실제 원본의 성공 code200/data{}는 제공 목록 없음으로 구분한다', async () => {
mockFetch.mockResolvedValue(
Response.json({ success: true, data: {}, message: '성공', code: 200 }),
);
const result = await createGetSearchPopwordsTool().handler({});
expect(JSON.parse(result.content[0].text)).toMatchObject({
available: false,
count: 0,
keywords: [],
reason: 'upstream_empty',
});
});
it.each([
{ success: false, data: {} },
{ success: true, code: 501, data: {} },
])('원본 실패를 빈 성공으로 바꾸지 않는다: %j', async (response) => {
mockFetch.mockResolvedValue(Response.json(response));
await expect(createGetSearchPopwordsTool().handler({})).rejects.toThrow('인기 검색어 응답');
});

it('키가 있어도 차단된 인기 검색어 요청에 유료 호출을 보내지 않는다', async () => {
mockFetch.mockResolvedValueOnce(new Response('blocked', { status: 403 }));
await expect(
Expand Down
Loading
0