fix: preserve search input and add choseong lookup

This commit is contained in:
2026-07-21 11:23:42 +09:00
parent 5ccf8b8ffd
commit b6208c0971
11 changed files with 801 additions and 64 deletions

View File

@@ -119,6 +119,7 @@
let namedPlaylistConfirmation = null;
let manualDraft = null;
let manualDraftKey = "";
let manualDraftResetPending = false;
let operatorCatalogState = null;
let operatorCatalogNameDraft = "";
let operatorCatalogDraftKey = "";
@@ -149,6 +150,12 @@
let industryRenderStructureKey = "";
let overseasRenderStructureKey = "";
let themeRenderStructureKey = "";
let comparisonRenderStructureKey = "";
let expertRenderStructureKey = "";
let tradingHaltRenderStructureKey = "";
let manualFinancialRenderStateKey = "";
let manualListRenderStateKey = "";
let operatorCatalogRenderStateKey = "";
let renderedTreeTabId = null;
let treeInteractionLocked = false;
let treeFocusOwnedBeforeRender = false;
@@ -339,6 +346,7 @@
const controlFocus = activeElement && activeElement.id
? {
id: activeElement.id,
value: typeof activeElement.value === "string" ? activeElement.value : null,
selectionStart: Number.isInteger(activeElement.selectionStart)
? activeElement.selectionStart : null,
selectionEnd: Number.isInteger(activeElement.selectionEnd)
@@ -423,17 +431,28 @@
const canRestoreFocus = treeFocusOwnedBeforeRender &&
!modalFocusManager.getActiveModal();
const focused = findTreeNodeByKey(saved.focusKey);
if (canRestoreFocus && focused && typeof focused.focus === "function") {
const control = canRestoreFocus && saved.controlFocus && saved.controlFocus.id
? document.getElementById(saved.controlFocus.id)
: null;
const canRestoreControl = control && categoryTree.contains(control) &&
control.disabled !== true && typeof control.focus === "function";
// A focused editor must never be blurred through the previously selected
// tree node. Apart from losing the local draft, that blur commits/cancels
// an in-progress Korean IME composition on every native status refresh.
if (canRestoreFocus && !canRestoreControl && focused &&
typeof focused.focus === "function") {
focused.focus({ preventScroll: true });
}
if (canRestoreFocus && saved.controlFocus && saved.controlFocus.id) {
const control = document.getElementById(saved.controlFocus.id);
if (control && categoryTree.contains(control) && control.disabled !== true &&
typeof control.focus === "function") {
control.focus({ preventScroll: true });
if (Number.isInteger(saved.controlFocus.selectionStart) &&
Number.isInteger(saved.controlFocus.selectionEnd) &&
typeof control.setSelectionRange === "function") {
if (canRestoreControl) {
const controlWasRecreated = document.activeElement !== control;
if (controlWasRecreated && typeof saved.controlFocus.value === "string" &&
typeof control.value === "string") {
control.value = saved.controlFocus.value;
}
if (controlWasRecreated) control.focus({ preventScroll: true });
if (Number.isInteger(saved.controlFocus.selectionStart) &&
Number.isInteger(saved.controlFocus.selectionEnd) &&
typeof control.setSelectionRange === "function" && controlWasRecreated) {
try {
control.setSelectionRange(
saved.controlFocus.selectionStart,
@@ -441,7 +460,6 @@
} catch (_) {
// Non-text controls can expose selection properties without accepting a range.
}
}
}
}
if (Number.isFinite(saved.scrollTop)) categoryTree.scrollTop = saved.scrollTop;
@@ -457,6 +475,21 @@
}
}
function setTransientControlsBusy(root, busy) {
if (!root) return;
root.querySelectorAll("button, input, select, textarea").forEach(function (control) {
if (busy) {
if (control.dataset.busyDisabled === undefined) {
control.dataset.busyDisabled = control.disabled === true ? "true" : "false";
}
control.disabled = true;
} else if (control.dataset.busyDisabled !== undefined) {
control.disabled = control.dataset.busyDisabled === "true";
delete control.dataset.busyDisabled;
}
});
}
function isOperatorMutationLocked(state) {
const playout = state && state.playout;
return !state || state.isBusy === true || state.fixedSectionBatch != null ||
@@ -1399,7 +1432,6 @@
const catalog = state.fixedCatalog;
const batch = state.fixedSectionBatch;
if (!catalog || !Array.isArray(catalog.sections)) {
categoryTree.replaceChildren();
return;
}
@@ -1823,10 +1855,43 @@
overseasRenderStructureKey = structureKey;
}
function updateComparisonWorkspace(workspace, comparison) {
["domestic", "world"].forEach(function (kind) {
const input = workspace.querySelector("#comparison-" + kind + "-search");
if (input && document.activeElement !== input) {
input.value = comparison[kind + "Search"].query || "";
}
});
}
function renderComparison(state) {
const comparison = state.comparison;
if (!comparison || !comparison.domesticSearch || !comparison.worldSearch) return;
const structureKey = JSON.stringify({
domestic: {
results: comparison.domesticSearch.results,
selectedResultId: comparison.domesticSearch.selectedResultId,
isTruncated: comparison.domesticSearch.isTruncated
},
world: {
results: comparison.worldSearch.results,
selectedResultId: comparison.worldSearch.selectedResultId,
isTruncated: comparison.worldSearch.isTruncated
},
fixedTargets: comparison.fixedTargets,
selectedFixedTargetId: comparison.selectedFixedTargetId,
firstTarget: comparison.firstTarget,
secondTarget: comparison.secondTarget,
savedPairs: comparison.savedPairs,
actions: comparison.actions
});
const existingWorkspace = categoryTree.querySelector(".comparison-workspace");
if (existingWorkspace && structureKey === comparisonRenderStructureKey) {
updateComparisonWorkspace(existingWorkspace, comparison);
return;
}
const workspace = document.createElement("div");
workspace.className = "comparison-workspace";
@@ -1971,6 +2036,8 @@
workspace.append(searches, fixed, pairPanel, actions);
categoryTree.replaceChildren(workspace);
comparisonRenderStructureKey = structureKey;
updateComparisonWorkspace(workspace, comparison);
}
function themeActionIsClickable(action) {
@@ -2169,9 +2236,28 @@
updateThemeWorkspace(workspace, theme);
}
function updateExpertWorkspace(workspace, expert) {
const input = workspace.querySelector("#expert-search");
if (input && document.activeElement !== input) {
input.value = expert.search.query || "";
}
}
function renderExpert(state) {
const expert = state.expert;
if (!expert || !expert.search || !expert.preview) return;
const structureKey = JSON.stringify({
results: expert.search.results,
isTruncated: expert.search.isTruncated,
selectedExpert: expert.selectedExpert,
preview: expert.preview,
actions: expert.actions
});
const existingWorkspace = categoryTree.querySelector(".expert-workspace");
if (existingWorkspace && structureKey === expertRenderStructureKey) {
updateExpertWorkspace(existingWorkspace, expert);
return;
}
const workspace = document.createElement("div");
workspace.className = "expert-workspace";
const search = document.createElement("div");
@@ -2247,11 +2333,28 @@
actions.appendChild(editRecommendations);
workspace.append(search, body, actions);
categoryTree.replaceChildren(workspace);
expertRenderStructureKey = structureKey;
updateExpertWorkspace(workspace, expert);
}
function updateTradingHaltWorkspace(workspace, halt) {
const input = workspace.querySelector("#halt-search");
if (input && document.activeElement !== input) input.value = halt.query || "";
}
function renderTradingHalt(state) {
const halt = state.tradingHalt;
if (!halt || !Array.isArray(halt.results)) return;
const structureKey = JSON.stringify({
results: halt.results,
nameSort: halt.nameSort,
actions: halt.actions
});
const existingWorkspace = categoryTree.querySelector(".halt-workspace");
if (existingWorkspace && structureKey === tradingHaltRenderStructureKey) {
updateTradingHaltWorkspace(existingWorkspace, halt);
return;
}
const workspace = document.createElement("div");
workspace.className = "halt-workspace";
const search = document.createElement("div");
@@ -2292,6 +2395,8 @@
});
workspace.append(search, results, actions);
categoryTree.replaceChildren(workspace);
tradingHaltRenderStructureKey = structureKey;
updateTradingHaltWorkspace(workspace, halt);
}
function blankManualRecord(screen) {
@@ -2465,19 +2570,38 @@
if (record) manualDraft = record;
}
function captureManualDraftForChangedModel() {
if (!manualModal.hidden && !manualDraftResetPending) rememberManualDraft();
manualDraftResetPending = false;
}
function renderManualFinancial(state) {
const manual = state.manualFinancial;
manualButtons.forEach(function (button) {
button.disabled = state.isBusy === true;
});
if (!manual) {
setTransientControlsBusy(manualDialog, false);
manualModal.hidden = true;
manualWorkspace.replaceChildren();
manualDraft = null;
manualDraftKey = "";
manualDraftResetPending = false;
manualFinancialRenderStateKey = "";
return;
}
const renderStateKey = JSON.stringify(manual);
if (!manualModal.hidden && renderStateKey === manualFinancialRenderStateKey) {
setTransientControlsBusy(manualDialog, state.isBusy === true);
return;
}
captureManualDraftForChangedModel();
// Clear the previous transient snapshot before applying a changed model.
// Otherwise a busy -> new-model transition can restore the old screen's
// semantic disabled states over the controls rendered below.
setTransientControlsBusy(manualDialog, false);
if (manualModal.hidden) rememberModalOpener(manualDialog);
manualModal.hidden = false;
manualTitle.textContent = manual.label + " · GraphE 수동 입력";
@@ -2594,12 +2718,9 @@
body.append(list, editor);
manualWorkspace.replaceChildren(toolbar, body);
manualClose.disabled = state.isBusy === true;
if (state.isBusy === true) {
manualWorkspace.querySelectorAll("button, input").forEach(function (control) {
control.disabled = true;
});
}
manualClose.disabled = false;
setTransientControlsBusy(manualDialog, state.isBusy === true);
manualFinancialRenderStateKey = renderStateKey;
}
function isCanonicalNamedPlaylistTitle(value) {
@@ -2614,13 +2735,25 @@
button.disabled = state.isBusy === true || fixedBatch != null;
});
if (!manual || manual.isOpen !== true) {
setTransientControlsBusy(manualListDialog, false);
clearDelayedClick(manualViResultClickTimer);
manualViResultClickTimer = null;
manualListModal.hidden = true;
manualListWorkspace.replaceChildren();
manualListRenderStateKey = "";
return;
}
const renderStateKey = JSON.stringify({
manual: manual,
fixedBatch: fixedBatch
});
if (!manualListModal.hidden && renderStateKey === manualListRenderStateKey) {
setTransientControlsBusy(manualListDialog, state.isBusy === true);
return;
}
setTransientControlsBusy(manualListDialog, false);
if (manualListModal.hidden) rememberModalOpener(manualListDialog);
manualListModal.hidden = false;
const busy = state.isBusy === true;
@@ -2669,7 +2802,7 @@
audience === manual.audience ? "active" : "",
audienceLabels[audience]);
button.type = "button";
button.disabled = busy || fixedBatch != null;
button.disabled = fixedBatch != null;
button.addEventListener("click", function () {
send("open-manual-list", { screen: audience });
});
@@ -2697,16 +2830,15 @@
input.type = "text";
input.maxLength = 256;
input.value = row[field] || "";
input.disabled = busy || manual.isAvailable !== true || manual.writesQuarantined;
input.disabled = manual.isAvailable !== true || manual.writesQuarantined;
input.addEventListener("change", function () {
manualListWorkspace.querySelectorAll("button, input").forEach(function (control) {
control.disabled = true;
});
send("set-manual-net-sell-cell", {
setTransientControlsBusy(manualListDialog, true);
const sent = send("set-manual-net-sell-cell", {
rowId: row.rowId,
field: field,
value: input.value
});
if (!sent) setTransientControlsBusy(manualListDialog, false);
});
td.appendChild(input);
tr.appendChild(td);
@@ -2725,7 +2857,7 @@
input.value = manual.searchQuery || "";
const searchButton = manualElement("button", "", "검색");
searchButton.type = "button";
searchButton.disabled = busy || manual.isAvailable !== true;
searchButton.disabled = manual.isAvailable !== true;
function submitSearch() {
if (input.value.trim()) send("search-manual-vi", { query: input.value });
}
@@ -2746,7 +2878,7 @@
manual.selectedSearchResultId === row.resultId ? "selected" : "",
row.displayName + " · " + row.marketText);
button.type = "button";
button.disabled = busy;
button.disabled = false;
button.addEventListener("click", function () {
clearDelayedClick(manualViResultClickTimer);
manualViResultClickTimer = window.setTimeout(function () {
@@ -2782,7 +2914,9 @@
[["↑", "up"], ["↓", "down"]].forEach(function (move) {
const button = manualElement("button", "", move[0]);
button.type = "button";
button.disabled = busy || (move[1] === "up" ? index === 0 : index === manual.viItems.length - 1);
button.disabled = move[1] === "up"
? index === 0
: index === manual.viItems.length - 1;
button.addEventListener("click", function () {
send("move-manual-vi-item", { itemId: row.itemId, direction: move[1] });
});
@@ -2790,7 +2924,7 @@
});
const remove = manualElement("button", "", "삭제");
remove.type = "button";
remove.disabled = busy;
remove.disabled = false;
remove.addEventListener("click", function () {
send("delete-manual-vi-item", { itemId: row.itemId });
});
@@ -2813,7 +2947,7 @@
const actions = manualElement("div", "manual-list-actions-row");
const importButton = manualElement("button", "", "원본 1회 가져오기");
importButton.type = "button";
importButton.disabled = busy || manual.isAvailable !== true || manual.writesQuarantined;
importButton.disabled = manual.isAvailable !== true || manual.writesQuarantined;
importButton.addEventListener("click", function () {
if (window.confirm("원본 파일을 현재 빈 저장소로 한 번만 가져옵니다. 기존 파일은 덮어쓰지 않습니다.")) {
send("import-manual-lists", {});
@@ -2821,12 +2955,12 @@
});
const refresh = manualElement("button", "", "재조회");
refresh.type = "button";
refresh.disabled = busy || manual.isAvailable !== true;
refresh.disabled = manual.isAvailable !== true;
refresh.addEventListener("click", function () { send("refresh-manual-list", {}); });
if (manual.screen === "vi") {
const clear = manualElement("button", "", "전체 삭제");
clear.type = "button";
clear.disabled = busy || manual.viItems.length === 0;
clear.disabled = manual.viItems.length === 0;
clear.addEventListener("click", function () {
if (window.confirm("VI 목록을 모두 지우시겠습니까?")) {
send("delete-all-manual-vi-items", {});
@@ -2841,14 +2975,16 @@
? "확인 후 다음"
: "확인 후 섹션 추가";
}
confirm.disabled = busy || manual.canSave !== true;
confirm.disabled = manual.canSave !== true;
confirm.addEventListener("click", function () {
send("confirm-manual-list", {});
});
actions.append(importButton, refresh, confirm);
fragment.appendChild(actions);
manualListWorkspace.replaceChildren(fragment);
manualListClose.disabled = busy;
manualListClose.disabled = false;
setTransientControlsBusy(manualListDialog, busy);
manualListRenderStateKey = renderStateKey;
}
function openNamedPlaylist(mode) {
@@ -3234,6 +3370,7 @@
operatorCatalogMutationLocked = isOperatorMutationLocked(state);
operatorCatalogState = catalog || null;
if (!catalog || catalog.isOpen !== true) {
setTransientControlsBusy(operatorCatalogDialog, false);
draggedOperatorCatalogRowId = null;
operatorCatalogDragBlockedRowId = null;
clearOperatorCatalogDragVisuals();
@@ -3244,6 +3381,7 @@
operatorCatalogNameDraft = "";
operatorCatalogNameStatus.textContent = "";
operatorCatalogNameStatus.className = "";
operatorCatalogRenderStateKey = "";
return;
}
@@ -3253,6 +3391,20 @@
clearOperatorCatalogDragVisuals();
}
const renderStateKey = JSON.stringify(catalog);
if (!operatorCatalogModal.hidden && renderStateKey === operatorCatalogRenderStateKey) {
setTransientControlsBusy(operatorCatalogDialog, busy);
operatorCatalogDraftRows.querySelectorAll("[data-operator-catalog-row-id]")
.forEach(function (row) {
row.draggable = !operatorCatalogMutationLocked;
row.setAttribute(
"aria-disabled",
operatorCatalogMutationLocked ? "true" : "false");
});
return;
}
setTransientControlsBusy(operatorCatalogDialog, false);
if (operatorCatalogModal.hidden) rememberModalOpener(operatorCatalogDialog);
operatorCatalogModal.hidden = false;
const isTheme = catalog.entity === "theme";
@@ -3262,18 +3414,18 @@
: catalog.mode === "create" ? "전문가 추가" : "전문가 추천 편집";
operatorCatalogThemeTab.classList.toggle("selected", isTheme);
operatorCatalogExpertTab.classList.toggle("selected", !isTheme);
operatorCatalogThemeTab.disabled = busy || editing;
operatorCatalogExpertTab.disabled = busy || editing;
operatorCatalogThemeTab.disabled = editing;
operatorCatalogExpertTab.disabled = editing;
operatorCatalogToolbar.hidden = editing;
operatorCatalogListPanel.hidden = editing;
operatorCatalogBody.classList.toggle("editor-only", editing);
operatorCatalogClose.disabled = busy;
operatorCatalogSearch.disabled = busy;
operatorCatalogSession.disabled = busy || !isTheme;
operatorCatalogClose.disabled = false;
operatorCatalogSearch.disabled = false;
operatorCatalogSession.disabled = !isTheme;
operatorCatalogSession.hidden = !isTheme;
operatorCatalogNewMarket.disabled = busy || !isTheme;
operatorCatalogNewMarket.disabled = !isTheme;
operatorCatalogNewMarket.hidden = !isTheme;
operatorCatalogNew.disabled = busy || catalog.canBeginCreate !== true;
operatorCatalogNew.disabled = catalog.canBeginCreate !== true;
operatorCatalogNew.textContent = isTheme ? "새 ThemeA" : "새 EList";
if (document.activeElement !== operatorCatalogQuery) {
operatorCatalogQuery.value = catalog.query || "";
@@ -3288,7 +3440,7 @@
button.type = "button";
button.dataset.operatorCatalogResultId = row.resultId;
button.className = row.isSelected ? "selected" : "";
button.disabled = busy;
button.disabled = false;
button.setAttribute("role", "option");
button.setAttribute("aria-selected", row.isSelected ? "true" : "false");
const code = document.createElement("strong");
@@ -3316,7 +3468,7 @@
? "코드 " + catalog.codeLabel : "";
operatorCatalogNameLabel.textContent = isTheme ? "테마명" : "전문가명";
operatorCatalogName.placeholder = isTheme ? "ThemeA 이름" : "전문가 이름";
operatorCatalogName.disabled = busy || !editing ||
operatorCatalogName.disabled = !editing ||
(!isTheme && catalog.mode === "edit");
const currentThemeName = operatorCatalogNameDraft.trim();
const checkedCurrentThemeName = isTheme && currentThemeName.length > 0 &&
@@ -3325,7 +3477,7 @@
? catalog.themeNameAvailability
: "notChecked";
operatorCatalogNameCheck.hidden = !isTheme;
operatorCatalogNameCheck.disabled = busy || !isTheme || !editing ||
operatorCatalogNameCheck.disabled = !isTheme || !editing ||
catalog.canCheckThemeName !== true || currentThemeName.length === 0;
operatorCatalogNameStatus.hidden = !isTheme;
operatorCatalogNameStatus.className = themeNameAvailability === "available" ||
@@ -3342,8 +3494,8 @@
if (document.activeElement !== operatorCatalogStockQuery) {
operatorCatalogStockQuery.value = catalog.stockQuery || "";
}
operatorCatalogStockQuery.disabled = busy || !editing;
operatorCatalogStockSearch.disabled = busy || !editing;
operatorCatalogStockQuery.disabled = !editing;
operatorCatalogStockSearch.disabled = !editing;
const showCatalogRows = isTheme || catalog.mode === "edit";
operatorCatalogStockSearchLine.hidden = !showCatalogRows;
operatorCatalogStockResults.hidden = !showCatalogRows;
@@ -3355,7 +3507,7 @@
operatorCatalogAmountLabel.hidden = true;
operatorCatalogAmount.hidden = true;
operatorCatalogAmount.disabled = true;
operatorCatalogAddStock.disabled = busy || catalog.canAddStock !== true;
operatorCatalogAddStock.disabled = catalog.canAddStock !== true;
const stockFragment = document.createDocumentFragment();
(catalog.stockResults || []).forEach(function (row) {
@@ -3363,7 +3515,7 @@
button.type = "button";
button.dataset.operatorCatalogStockResultId = row.resultId;
button.className = row.isSelected ? "selected" : "";
button.disabled = busy || row.isCompatible !== true;
button.disabled = row.isCompatible !== true;
button.title = "더블클릭하면 구성 종목에 추가합니다.";
button.setAttribute("role", "option");
button.setAttribute("aria-selected", row.isSelected ? "true" : "false");
@@ -3430,25 +3582,25 @@
detail.value = row.buyAmount == null ? "" : String(row.buyAmount);
detail.dataset.operatorCatalogBuyAmountRowId = row.rowId;
detail.setAttribute("aria-label", row.displayName + " 매수가");
detail.disabled = busy;
detail.disabled = false;
}
const up = document.createElement("button");
up.type = "button";
up.textContent = "Up";
up.dataset.operatorCatalogMoveRowId = row.rowId;
up.dataset.operatorCatalogMoveDirection = "up";
up.disabled = busy || index === 0;
up.disabled = index === 0;
const down = document.createElement("button");
down.type = "button";
down.textContent = "Down";
down.dataset.operatorCatalogMoveRowId = row.rowId;
down.dataset.operatorCatalogMoveDirection = "down";
down.disabled = busy || index + 1 === catalog.draftRows.length;
down.disabled = index + 1 === catalog.draftRows.length;
const remove = document.createElement("button");
remove.type = "button";
remove.textContent = "Delete";
remove.dataset.operatorCatalogRemoveRowId = row.rowId;
remove.disabled = busy;
remove.disabled = false;
line.append(order, name, detail, up, down, remove);
draftFragment.appendChild(line);
});
@@ -3462,11 +3614,11 @@
if (activeRow) activeRow.focus({ preventScroll: true });
}
operatorCatalogSave.disabled = busy || catalog.canSave !== true;
operatorCatalogSave.disabled = catalog.canSave !== true;
operatorCatalogDeleteAll.hidden = !isTheme || !editing;
operatorCatalogDeleteAll.disabled = busy || !isTheme || !editing ||
operatorCatalogDeleteAll.disabled = !isTheme || !editing ||
(catalog.draftRows || []).length === 0;
operatorCatalogCancel.disabled = busy || !editing;
operatorCatalogCancel.disabled = !editing;
const messages = [];
if (catalog.statusMessage) messages.push(catalog.statusMessage);
if (catalog.resultsAreTruncated) messages.push("목록 일부만 표시 중입니다.");
@@ -3478,6 +3630,8 @@
operatorCatalogStatus.classList.toggle(
"error",
catalog.writesQuarantined === true || catalog.statusKind === "error");
setTransientControlsBusy(operatorCatalogDialog, busy);
operatorCatalogRenderStateKey = renderStateKey;
}
function renderDialog(state) {
@@ -3661,6 +3815,7 @@
manualResultClickTimer = null;
manualDraft = null;
manualDraftKey = "";
manualDraftResetPending = false;
send("open-manual-financial", { screen: button.dataset.manualScreen });
});
});
@@ -3689,6 +3844,7 @@
manualResultClickTimer = null;
manualDraft = null;
manualDraftKey = "";
manualDraftResetPending = false;
send("activate-manual-financial-result", {
resultId: row.dataset.manualResultId
});
@@ -3702,6 +3858,7 @@
manualResultClickTimer = null;
manualDraft = null;
manualDraftKey = "";
manualDraftResetPending = false;
send("load-manual-financial", { resultId: resultId });
}, 250);
return;
@@ -3736,7 +3893,11 @@
const screen = manualDraft ? manualDraft.screen : "";
manualDraft = blankManualRecord(screen);
manualDraftKey = screen + "|new";
send("begin-manual-financial-create", {});
manualDraftResetPending = true;
if (!send("begin-manual-financial-create", {})) {
manualDraftResetPending = false;
rememberManualDraft();
}
return;
}
if (event.target.closest("button[data-manual-stock-search]")) {