// Part 13 of 15 of the operator console app script. // // Collection candidate search, dismissal, selection, and promotion. // // Load order is significant: these are classic (non-module) scripts sharing one // global scope, and index.html must keep them in the order listed above/below. // Splitting is a pure line-range extraction of the former single app.js. function clearCollectionCandidatesForSearch() { collectionCandidates.splice(0, collectionCandidates.length); renderCollectionCandidates(); } function clearCollectionCandidatesAfterAction() { collectionCandidates.splice(0, collectionCandidates.length); state.currentCollectionQuery = ""; state.currentCollectionProvider = ""; renderCollectionCandidates(); } async function dismissCollectionCandidate(candidateId) { const status = document.getElementById("collection-status"); if (status) status.textContent = "후보를 무시하는 중입니다."; try { const payload = await apiJson(`/api/collections/candidates/${encodeURIComponent(candidateId)}/dismiss`, { method: "POST", body: JSON.stringify({}), }); applyBootstrap(payload); renderAll(); if (status) status.textContent = "후보를 무시했습니다."; } catch (errorValue) { if (status) status.textContent = errorValue.message; } } async function runCandidateCollection(event) { event.preventDefault(); const provider = document.getElementById("collection-provider").value; const queryInput = document.getElementById("collection-query"); const status = document.getElementById("collection-status"); const submitButton = event.target.querySelector('button[type="submit"]'); const query = queryInput.value.trim(); if (!query) { state.currentCollectionQuery = ""; state.currentCollectionProvider = ""; renderCollectionCandidates(); status.textContent = "수집할 키워드를 입력하세요."; return; } const providerConfig = providers.find((item) => item.id === provider); if (!providerConfig || !providerConfig.enabled) { status.textContent = `${providerLabels[provider] || provider} 외부 검색 tool이 비활성입니다.`; return; } submitButton.disabled = true; status.textContent = "키워드 후보를 수집하는 중입니다."; try { const payload = await apiJson("/api/collections/keyword", { method: "POST", body: JSON.stringify({ provider, query }), }); // Commit the new query/provider and swap in the new candidates only after the // request succeeds, so a failed collection doesn't wipe the previous view. state.currentCollectionQuery = query; state.currentCollectionProvider = provider; applyBootstrap(payload); renderAll(); status.textContent = `${payload.collected || 0}개후보를 수집했습니다.`; } catch (errorValue) { status.textContent = errorValue.message; } finally { submitButton.disabled = false; } } function selectableCollectionCandidateInputs() { return Array.from(document.querySelectorAll("input[data-collection-candidate-id]:not(:disabled)")); } function selectedCollectionCandidateIds() { return selectableCollectionCandidateInputs() .filter((input) => input.checked) .map((input) => input.dataset.collectionCandidateId); } function setAllCollectionCandidateSelection(selected) { const inputs = selectableCollectionCandidateInputs(); const status = document.getElementById("collection-status"); inputs.forEach((input) => { input.checked = selected; }); if (!inputs.length) { status.textContent = "선택된 후보가 없습니다."; return; } status.textContent = selected ? `${inputs.length}개 후보를 선택했습니다.` : "후보 선택을 모두 해제했습니다."; } function commaSeparatedValues(elementId) { return document .getElementById(elementId) .value.split(",") .map((item) => item.trim()) .filter(Boolean); } async function promoteSelectedCollectionCandidates(event) { event.preventDefault(); const candidateIds = selectedCollectionCandidateIds(); const status = document.getElementById("collection-status"); const submitButton = document.getElementById("promote-selected-candidates"); if (!candidateIds.length) { status.textContent = "반영할 후보를 선택하세요."; return; } submitButton.disabled = true; status.textContent = "선택한 후보를 기준 DB에 반영하는 중입니다."; try { const payload = await apiJson("/api/collections/candidates/promote-batch", { method: "POST", body: JSON.stringify({ candidate_ids: candidateIds, name: document.getElementById("collection-promotion-name").value.trim(), type: document.getElementById("collection-promotion-type").value, aliases: commaSeparatedValues("collection-promotion-aliases"), keywords: commaSeparatedValues("collection-promotion-keywords"), memo: document.getElementById("collection-promotion-memo").value.trim(), }), }); applyBootstrap(payload); clearCollectionCandidatesAfterAction(); event.target.reset(); renderAll(); status.textContent = `${candidateIds.length}개후보를 기준 DB에 반영했습니다.`; } catch (errorValue) { status.textContent = errorValue.message; } finally { submitButton.disabled = false; } } async function promoteCollectionCandidate(candidateId) { const status = document.getElementById("collection-status"); status.textContent = "후보를 기준 DB에 반영하는 중입니다."; try { const payload = await apiJson(`/api/collections/candidates/${encodeURIComponent(candidateId)}/promote`, { method: "POST", body: JSON.stringify({}), }); applyBootstrap(payload); clearCollectionCandidatesAfterAction(); renderAll(); status.textContent = "후보를 기준 DB에 반영했습니다."; } catch (errorValue) { status.textContent = errorValue.message; } }