diff --git a/tests/operator_gui/test_static_workbench.py b/tests/operator_gui/test_static_workbench.py
index 9dd79f5..d933895 100644
--- a/tests/operator_gui/test_static_workbench.py
+++ b/tests/operator_gui/test_static_workbench.py
@@ -788,3 +788,18 @@ def test_visual_assets_are_referenced_for_review_images():
"match-web.svg",
]:
assert not (assets_dir / orphan_asset).exists(), f"{orphan_asset} is unused; do not reintroduce it"
+
+
+def test_suggested_queries_support_one_click_and_batch_execution():
+ script = _read(APP_JS)
+ styles = _read(STYLES)
+
+ assert "executeSuggestedQueries" in script
+ assert "data-run-suggested-query" in script
+ assert 'id="run-all-suggested-queries"' in script
+ assert "모두 실행" in script
+ assert "바로 실행" in script
+ assert "suggested-query-run-status" in script
+ # 기존 "입력칸 채우기" 버튼은 수정용으로 유지된다.
+ assert "data-suggested-query" in script
+ assert ".suggested-query-item" in styles
diff --git a/web/operator-gui/app.js b/web/operator-gui/app.js
index f4b4fa4..9038cc5 100644
--- a/web/operator-gui/app.js
+++ b/web/operator-gui/app.js
@@ -27,6 +27,8 @@ const knowledgeEntries = [];
const searchCoverage = {};
+let suggestedQueryRunSummary = null; // { caseId, message } — 현재 케이스의 마지막 추천 쿼리 실행 결과
+
const DEFAULT_COVERAGE_THRESHOLDS = Object.freeze({
coverageGoodRate: 70,
@@ -888,59 +890,47 @@ function suggestedEvidenceQueries(submission) {
function renderEvidenceNextActions(submission) {
+ const summary =
+ suggestedQueryRunSummary && suggestedQueryRunSummary.caseId === submission.id
+ ? `
${escapeHtml(suggestedQueryRunSummary.message)}
`
+ : "";
- if (!evidenceNeedsFollowup(submission)) return "";
+ if (!evidenceNeedsFollowup(submission)) return summary;
const queries = suggestedEvidenceQueries(submission);
-
const reasons = evidenceFollowupReasons(submission);
-
- if (!queries.length) return "";
+ if (!queries.length) return summary;
return `
-
-
-
근거 보강 추천
-
- 현재 근거가 부족합니다. 추천 쿼리를 선택하면 수동 검색 입력칸에만 채워집니다.
-
+ 추천 쿼리를 바로 실행하거나, 쿼리 본문을 눌러 수동 검색 입력칸에서 수정할 수 있습니다.
-
-
${reasons.map((reason) => `- ${escapeHtml(reason)}
`).join("")}
-
-
-
${queries
-
.map(
-
(query) => `
-
-
-
+
+
+
+
`,
-
)
-
.join("")}
-
-
+
+
+
+
+ ${summary}
-
`;
-
}
@@ -968,6 +958,39 @@ function applySuggestedQuery(button) {
}
+async function executeSuggestedQueries(queries) {
+ const submission = getSelectedCase();
+ if (!submission || !queries.length) return;
+
+ const statusTarget = document.getElementById("suggested-query-run-status");
+ const naver = providers.find((provider) => provider.id === "naver");
+ if (!naver || !naver.enabled) {
+ if (statusTarget) statusTarget.textContent = "네이버 외부 검색 tool이 비활성입니다.";
+ return;
+ }
+
+ const results = [];
+ for (const [index, query] of queries.entries()) {
+ if (statusTarget) statusTarget.textContent = `"${query}" 실행 중… (${index + 1}/${queries.length})`;
+ try {
+ await apiJson("/api/search/manual", {
+ method: "POST",
+ body: JSON.stringify({ submission_id: submission.id, provider: "naver", query }),
+ });
+ results.push(`"${query}" 완료`);
+ } catch (errorValue) {
+ results.push(`"${query}" 실패: ${errorValue.message}`);
+ }
+ }
+
+ suggestedQueryRunSummary = {
+ caseId: submission.id,
+ message: `추천 쿼리 실행 — ${results.join(" · ")}`,
+ };
+ await refreshFromApi();
+}
+
+
function filteredSubmissions() {
const query = state.filters.query.trim().toLowerCase();
@@ -2129,6 +2152,8 @@ function selectCase(caseId) {
state.selectedCaseId = caseId;
+ suggestedQueryRunSummary = null;
+
switchView("workbench");
switchWorkbenchTab("evidence");
@@ -3547,6 +3572,18 @@ function bindEvents() {
+ const runSuggestedButton = event.target.closest("[data-run-suggested-query]");
+ if (runSuggestedButton) {
+ void executeSuggestedQueries([runSuggestedButton.dataset.runSuggestedQuery]);
+ return;
+ }
+
+ const runAllSuggestedButton = event.target.closest("#run-all-suggested-queries");
+ if (runAllSuggestedButton) {
+ void executeSuggestedQueries(suggestedEvidenceQueries(getSelectedCase()));
+ return;
+ }
+
const rerunQueryButton = event.target.closest("[data-rerun-query]");
if (rerunQueryButton) {
diff --git a/web/operator-gui/styles.css b/web/operator-gui/styles.css
index 4e7644c..11f43a6 100644
--- a/web/operator-gui/styles.css
+++ b/web/operator-gui/styles.css
@@ -2758,3 +2758,13 @@ tbody tr.selected-row,
grid-column: auto;
}
}
+
+.suggested-query-item {
+ display: inline-flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.suggested-query-actions {
+ margin-top: 8px;
+}