feat: get_issues에 사이트 검사 이슈 집계 지원
- site_inspection_id를 받으면 모든 페이지의 이슈를 자동 수집/집계 - page_url 파라미터로 특정 페이지 필터링 가능 - 페이지별 이슈 수 테이블 + 심각도순 상위 이슈 15개 표시 - i18n 설명 업데이트 (en/ko) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { ApiClient } from "../api-client.js";
|
||||
import { ApiClient, type Issue } from "../api-client.js";
|
||||
import { t } from "../i18n/index.js";
|
||||
|
||||
export async function getIssues(
|
||||
@ -7,16 +7,155 @@ export async function getIssues(
|
||||
lang: "en" | "ko",
|
||||
category?: string,
|
||||
severity?: string,
|
||||
pageUrl?: string,
|
||||
): Promise<string> {
|
||||
const result = await client.getIssues(id, category, severity);
|
||||
// Try single-page first
|
||||
try {
|
||||
const result = await client.getIssues(id, category, severity);
|
||||
return formatSingleIssues(result.inspection_id, result.issues, result.total, result.filters, lang);
|
||||
} catch {
|
||||
// Might be a site inspection ID — aggregate issues from all pages
|
||||
}
|
||||
|
||||
try {
|
||||
return await formatSiteIssues(client, id, lang, category, severity, pageUrl);
|
||||
} catch {
|
||||
return t("get_inspection.not_found", lang, { id });
|
||||
}
|
||||
}
|
||||
|
||||
async function formatSiteIssues(
|
||||
client: ApiClient,
|
||||
siteId: string,
|
||||
lang: "en" | "ko",
|
||||
category?: string,
|
||||
severity?: string,
|
||||
pageUrl?: string,
|
||||
): Promise<string> {
|
||||
const site = await client.getSiteInspection(siteId);
|
||||
|
||||
// Collect pages with inspection results
|
||||
let pages = site.discovered_pages.filter(
|
||||
(p) => p.inspection_id && p.status === "completed",
|
||||
);
|
||||
|
||||
// Filter by page URL if specified
|
||||
if (pageUrl) {
|
||||
pages = pages.filter((p) => p.url.includes(pageUrl));
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
const lines = [
|
||||
`# ${t("get_issues.title", lang)} (Site)`,
|
||||
`**Site**: ${site.root_url}`,
|
||||
"",
|
||||
"No completed page inspections found.",
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Fetch issues from each page (parallel, max 10 concurrent)
|
||||
type PageIssues = { url: string; issues: Issue[] };
|
||||
const pageIssues: PageIssues[] = [];
|
||||
|
||||
const chunks = chunkArray(pages, 10);
|
||||
for (const chunk of chunks) {
|
||||
const results = await Promise.allSettled(
|
||||
chunk.map(async (p) => {
|
||||
const r = await client.getIssues(p.inspection_id!, category, severity);
|
||||
return { url: p.url, issues: r.issues } as PageIssues;
|
||||
}),
|
||||
);
|
||||
for (const r of results) {
|
||||
if (r.status === "fulfilled" && r.value.issues.length > 0) {
|
||||
pageIssues.push(r.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate all issues
|
||||
const allIssues = pageIssues.flatMap((p) =>
|
||||
p.issues.map((i) => ({ ...i, _pageUrl: p.url })),
|
||||
);
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# ${t("get_issues.title", lang)}`);
|
||||
lines.push(`**Inspection ID**: ${result.inspection_id}`);
|
||||
lines.push(`**${t("get_issues.total", lang)}**: ${result.total}`);
|
||||
lines.push(`# ${t("get_issues.title", lang)} (Site)`);
|
||||
lines.push(`**Site**: ${site.root_url} (${site.domain})`);
|
||||
lines.push(`**Pages**: ${pages.length}`);
|
||||
lines.push(`**${t("get_issues.total", lang)}**: ${allIssues.length}`);
|
||||
|
||||
// Show applied filters
|
||||
const activeFilters = Object.entries(result.filters)
|
||||
const filters: string[] = [];
|
||||
if (category) filters.push(`category: ${category}`);
|
||||
if (severity) filters.push(`severity: ${severity}`);
|
||||
if (pageUrl) filters.push(`page: ${pageUrl}`);
|
||||
if (filters.length > 0) {
|
||||
lines.push(`**${t("get_issues.filters", lang)}**: ${filters.join(", ")}`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
if (allIssues.length === 0) {
|
||||
lines.push("No issues found.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Summary by severity
|
||||
const sevCounts = { critical: 0, major: 0, minor: 0, info: 0 };
|
||||
for (const i of allIssues) sevCounts[i.severity]++;
|
||||
lines.push(
|
||||
`**Summary**: Critical: ${sevCounts.critical}, Major: ${sevCounts.major}, Minor: ${sevCounts.minor}, Info: ${sevCounts.info}`,
|
||||
);
|
||||
lines.push("");
|
||||
|
||||
// Per-page breakdown table
|
||||
lines.push("## Pages");
|
||||
lines.push("| Page | Issues | Critical | Major |");
|
||||
lines.push("|---|---|---|---|");
|
||||
for (const pi of pageIssues) {
|
||||
const crit = pi.issues.filter((i) => i.severity === "critical").length;
|
||||
const maj = pi.issues.filter((i) => i.severity === "major").length;
|
||||
const shortUrl = pi.url.length > 60 ? pi.url.slice(0, 57) + "..." : pi.url;
|
||||
lines.push(`| ${shortUrl} | ${pi.issues.length} | ${crit} | ${maj} |`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
// Top issues across all pages (critical + major first, max 15)
|
||||
const sorted = allIssues
|
||||
.sort((a, b) => sevOrder(a.severity) - sevOrder(b.severity))
|
||||
.slice(0, 15);
|
||||
|
||||
lines.push("## Top Issues");
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const issue = sorted[i];
|
||||
const shortUrl = issue._pageUrl.replace(site.root_url, "") || "/";
|
||||
lines.push(
|
||||
`### ${i + 1}. [${issue.severity.toUpperCase()}] ${issue.code} — ${shortUrl}`,
|
||||
);
|
||||
lines.push(`- **${t("result.message", lang)}**: ${issue.message}`);
|
||||
if (issue.element) {
|
||||
const el = issue.element.length > 120 ? issue.element.slice(0, 117) + "..." : issue.element;
|
||||
lines.push(`- **${t("result.element", lang)}**: \`${el}\``);
|
||||
}
|
||||
lines.push(`- **${t("result.suggestion", lang)}**: ${issue.suggestion}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatSingleIssues(
|
||||
inspectionId: string,
|
||||
issues: Issue[],
|
||||
total: number,
|
||||
filters: Record<string, string | null>,
|
||||
lang: "en" | "ko",
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`# ${t("get_issues.title", lang)}`);
|
||||
lines.push(`**Inspection ID**: ${inspectionId}`);
|
||||
lines.push(`**${t("get_issues.total", lang)}**: ${total}`);
|
||||
|
||||
const activeFilters = Object.entries(filters)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(", ");
|
||||
@ -25,7 +164,7 @@ export async function getIssues(
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
if (result.issues.length === 0) {
|
||||
if (issues.length === 0) {
|
||||
lines.push("No issues found.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
@ -36,8 +175,8 @@ export async function getIssues(
|
||||
);
|
||||
lines.push("|---|---|---|---|---|");
|
||||
|
||||
for (let i = 0; i < result.issues.length; i++) {
|
||||
const issue = result.issues[i];
|
||||
for (let i = 0; i < issues.length; i++) {
|
||||
const issue = issues[i];
|
||||
const msg =
|
||||
issue.message.length > 80
|
||||
? issue.message.slice(0, 77) + "..."
|
||||
@ -49,10 +188,10 @@ export async function getIssues(
|
||||
lines.push("");
|
||||
|
||||
// Detailed view for first 10
|
||||
const detailCount = Math.min(result.issues.length, 10);
|
||||
const detailCount = Math.min(issues.length, 10);
|
||||
lines.push("## Details");
|
||||
for (let i = 0; i < detailCount; i++) {
|
||||
const issue = result.issues[i];
|
||||
const issue = issues[i];
|
||||
lines.push(`### ${i + 1}. [${issue.severity.toUpperCase()}] ${issue.code}`);
|
||||
lines.push(`- **${t("result.message", lang)}**: ${issue.message}`);
|
||||
if (issue.element) {
|
||||
@ -69,3 +208,15 @@ export async function getIssues(
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function sevOrder(s: string): number {
|
||||
return { critical: 0, major: 1, minor: 2, info: 3 }[s] ?? 4;
|
||||
}
|
||||
|
||||
function chunkArray<T>(arr: T[], size: number): T[][] {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user