feat: one-click and batch execution for suggested evidence queries

This commit is contained in:
유창욱 2026-06-12 17:44:48 +09:00
parent 63bbf0d755
commit 4abb837aaa
3 changed files with 92 additions and 30 deletions

View file

@ -788,3 +788,18 @@ def test_visual_assets_are_referenced_for_review_images():
"match-web.svg", "match-web.svg",
]: ]:
assert not (assets_dir / orphan_asset).exists(), f"{orphan_asset} is unused; do not reintroduce it" 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

View file

@ -27,6 +27,8 @@ const knowledgeEntries = [];
const searchCoverage = {}; const searchCoverage = {};
let suggestedQueryRunSummary = null; // { caseId, message } — 현재 케이스의 마지막 추천 쿼리 실행 결과
const DEFAULT_COVERAGE_THRESHOLDS = Object.freeze({ const DEFAULT_COVERAGE_THRESHOLDS = Object.freeze({
coverageGoodRate: 70, coverageGoodRate: 70,
@ -888,59 +890,47 @@ function suggestedEvidenceQueries(submission) {
function renderEvidenceNextActions(submission) { function renderEvidenceNextActions(submission) {
const summary =
suggestedQueryRunSummary && suggestedQueryRunSummary.caseId === submission.id
? `<div class="inline-status" id="suggested-query-run-result">${escapeHtml(suggestedQueryRunSummary.message)}</div>`
: "";
if (!evidenceNeedsFollowup(submission)) return ""; if (!evidenceNeedsFollowup(submission)) return summary;
const queries = suggestedEvidenceQueries(submission); const queries = suggestedEvidenceQueries(submission);
const reasons = evidenceFollowupReasons(submission); const reasons = evidenceFollowupReasons(submission);
if (!queries.length) return summary;
if (!queries.length) return "";
return ` return `
<section class="evidence-next-action-panel" aria-label="근거 보강 추천"> <section class="evidence-next-action-panel" aria-label="근거 보강 추천">
<div> <div>
<strong>근거 보강 추천</strong> <strong>근거 보강 추천</strong>
<span>추천 쿼리를 바로 실행하거나, 쿼리 본문을 눌러 수동 검색 입력칸에서 수정할 있습니다.</span>
<span>현재 근거가 부족합니다. 추천 쿼리를 선택하면 수동 검색 입력칸에만 채워집니다.</span>
</div> </div>
<ul class="evidence-followup-reasons"> <ul class="evidence-followup-reasons">
${reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")} ${reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")}
</ul> </ul>
<div class="suggested-query-list"> <div class="suggested-query-list">
${queries ${queries
.map( .map(
(query) => ` (query) => `
<span class="suggested-query-item">
<button class="row-action" type="button" data-suggested-query="${escapeHtml(query)}" data-suggested-provider="naver"> <button class="row-action" type="button" data-suggested-query="${escapeHtml(query)}" data-suggested-provider="naver">
${escapeHtml(query)} ${escapeHtml(query)}
</button> </button>
<button class="row-action" type="button" data-run-suggested-query="${escapeHtml(query)}">바로 실행</button>
</span>
`, `,
) )
.join("")} .join("")}
</div> </div>
<div class="suggested-query-actions">
<button class="row-action" type="button" id="run-all-suggested-queries">모두 실행</button>
</div>
<div id="suggested-query-run-status" class="inline-status" aria-live="polite"></div>
${summary}
</section> </section>
`; `;
} }
@ -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() { function filteredSubmissions() {
const query = state.filters.query.trim().toLowerCase(); const query = state.filters.query.trim().toLowerCase();
@ -2129,6 +2152,8 @@ function selectCase(caseId) {
state.selectedCaseId = caseId; state.selectedCaseId = caseId;
suggestedQueryRunSummary = null;
switchView("workbench"); switchView("workbench");
switchWorkbenchTab("evidence"); 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]"); const rerunQueryButton = event.target.closest("[data-rerun-query]");
if (rerunQueryButton) { if (rerunQueryButton) {

View file

@ -2758,3 +2758,13 @@ tbody tr.selected-row,
grid-column: auto; grid-column: auto;
} }
} }
.suggested-query-item {
display: inline-flex;
gap: 4px;
align-items: center;
}
.suggested-query-actions {
margin-top: 8px;
}