608 lines
24 KiB
JavaScript
608 lines
24 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const $ = (selector, root = document) => root.querySelector(selector);
|
|
const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
|
|
|
|
const listingDetails = {
|
|
"DEMO-HJ-101": {
|
|
title: "한남 생활권 정원 주거",
|
|
status: "검수 필요",
|
|
statusClass: "status-warn",
|
|
trade: "매매",
|
|
type: "연립주택",
|
|
price: "38억 원",
|
|
area: "118.42㎡ · 35.8평",
|
|
rooms: "방 3 · 욕실 2",
|
|
moveIn: "2026.12.20 이후 협의",
|
|
image: "../assets/hannam-interior-720.webp",
|
|
imageAlt: "큰 창과 목재 수납이 있는 거실의 연출 이미지",
|
|
blockers: "2개 차단",
|
|
ai: "사람 검토 전"
|
|
},
|
|
"DEMO-HJ-102": {
|
|
title: "한강 가까운 복층 주거",
|
|
status: "예시 공개",
|
|
statusClass: "status-pass",
|
|
trade: "전세",
|
|
type: "공동주택",
|
|
price: "전세 22억 원",
|
|
area: "142.80㎡ · 43.2평",
|
|
rooms: "방 4 · 욕실 2",
|
|
moveIn: "2026.11.30 이후 협의",
|
|
image: "../assets/hannam-evening-640.webp",
|
|
imageAlt: "서울 언덕길의 석재 주거 건물 연출 이미지",
|
|
blockers: "안내 확인",
|
|
ai: "수정 후 승인"
|
|
},
|
|
"DEMO-HJ-103": {
|
|
title: "조용한 골목의 테라스 주거",
|
|
status: "자료 오래됨",
|
|
statusClass: "status-warn",
|
|
trade: "월세",
|
|
type: "다세대주택",
|
|
price: "보증금 5억 · 월 1,200만",
|
|
area: "140.84㎡ · 42.6평",
|
|
rooms: "방 3 · 욕실 2",
|
|
moveIn: "입주 시점 다시 확인",
|
|
image: "../assets/hannam-evening-640.webp",
|
|
imageAlt: "서울 언덕길의 석재 주거 건물 연출 이미지",
|
|
blockers: "다시 확인",
|
|
ai: "입력 변경으로 오래됨"
|
|
},
|
|
"DEMO-HJ-104": {
|
|
title: "언덕 위 작은 정원 주거",
|
|
status: "계약 완료",
|
|
statusClass: "status-demo",
|
|
trade: "매매",
|
|
type: "단독주택",
|
|
price: "상담 종료",
|
|
area: "목록에서 제외",
|
|
rooms: "공개 목록 제외",
|
|
moveIn: "해당 없음",
|
|
image: "../assets/hannam-evening-640.webp",
|
|
imageAlt: "서울 언덕길의 석재 주거 건물 연출 이미지",
|
|
blockers: "게시 안 함",
|
|
ai: "종료"
|
|
},
|
|
"DEMO-HJ-105": {
|
|
title: "남산 가까운 가족 주거",
|
|
status: "초안",
|
|
statusClass: "status-demo",
|
|
trade: "전세",
|
|
type: "공동주택",
|
|
price: "조건 작성 중",
|
|
area: "132.00㎡ · 39.9평",
|
|
rooms: "입력 전",
|
|
moveIn: "입력 전",
|
|
image: "../assets/hannam-interior-720.webp",
|
|
imageAlt: "큰 창과 목재 수납이 있는 거실의 연출 이미지",
|
|
blockers: "5개 차단",
|
|
ai: "실행 전"
|
|
},
|
|
"DEMO-HJ-106": {
|
|
title: "독서당길 안쪽 복층 주거",
|
|
status: "검수 필요",
|
|
statusClass: "status-warn",
|
|
trade: "매매",
|
|
type: "공동주택",
|
|
price: "41억 원",
|
|
area: "151.20㎡ · 45.7평",
|
|
rooms: "방 4 · 욕실 3",
|
|
moveIn: "2027.01 이후 협의",
|
|
image: "../assets/hannam-evening-640.webp",
|
|
imageAlt: "서울 언덕길의 석재 주거 건물 연출 이미지",
|
|
blockers: "동의 대기",
|
|
ai: "직전 실행 실패"
|
|
}
|
|
};
|
|
|
|
const state = {
|
|
selectedId: "DEMO-HJ-101",
|
|
mode: "normal",
|
|
toastTimer: null,
|
|
draftCreated: false
|
|
};
|
|
|
|
const queuePanel = $("[data-queue-panel]");
|
|
const queueBody = $("[data-queue-body]");
|
|
const inspector = $("[data-inspector]");
|
|
const workbench = $(".admin-workbench");
|
|
const inspectorBackdrop = $("[data-inspector-backdrop]");
|
|
const sidebar = $("[data-sidebar]");
|
|
const sidebarBackdrop = $("[data-sidebar-backdrop]");
|
|
|
|
const announce = (message) => {
|
|
const region = $("[data-admin-live]");
|
|
if (!region) return;
|
|
region.textContent = message;
|
|
region.hidden = false;
|
|
window.clearTimeout(state.toastTimer);
|
|
state.toastTimer = window.setTimeout(() => {
|
|
region.hidden = true;
|
|
region.textContent = "";
|
|
}, 3600);
|
|
};
|
|
|
|
const showBanner = (title, copy, kind = "warn") => {
|
|
const banner = $("[data-system-banner]");
|
|
if (!banner) return;
|
|
banner.classList.remove("is-error", "is-info");
|
|
if (kind === "error") banner.classList.add("is-error");
|
|
if (kind === "info") banner.classList.add("is-info");
|
|
$("[data-banner-title]", banner).textContent = title;
|
|
$("[data-banner-copy]", banner).textContent = copy;
|
|
banner.hidden = false;
|
|
};
|
|
|
|
const hideBanner = () => {
|
|
const banner = $("[data-system-banner]");
|
|
if (banner) banner.hidden = true;
|
|
};
|
|
|
|
const getRows = () => $$('[data-queue-row]', queueBody);
|
|
|
|
const isCompactInspector = () => window.matchMedia("(max-width: 76rem)").matches;
|
|
|
|
const updateInspectorBackdrop = () => {
|
|
if (!inspectorBackdrop) return;
|
|
inspectorBackdrop.hidden = !(isCompactInspector() && inspector?.classList.contains("is-open"));
|
|
};
|
|
|
|
const openInspector = (id, focus = false) => {
|
|
const detail = listingDetails[id] || listingDetails["DEMO-HJ-101"];
|
|
state.selectedId = id;
|
|
|
|
getRows().forEach((row) => {
|
|
const active = row.dataset.id === id;
|
|
row.classList.toggle("is-selected", active);
|
|
row.setAttribute("aria-selected", String(active));
|
|
});
|
|
|
|
if (!inspector) return;
|
|
inspector.classList.add("is-open");
|
|
workbench?.classList.remove("is-inspector-closed");
|
|
$("[data-inspector-id]", inspector).textContent = `자료번호 ${id.replace("DEMO-HJ-", "")} · 예시 매물`;
|
|
$("[data-inspector-title]", inspector).textContent = detail.title;
|
|
const status = $("[data-inspector-status]", inspector);
|
|
status.textContent = detail.status;
|
|
status.className = `status-badge ${detail.statusClass}`;
|
|
|
|
const image = $("[data-inspector-image]", inspector);
|
|
if (image) {
|
|
image.hidden = false;
|
|
image.src = detail.image;
|
|
image.alt = detail.imageAlt;
|
|
image.parentElement?.classList.remove("is-image-error");
|
|
}
|
|
|
|
const basicGrid = $("[data-basic-grid]", inspector);
|
|
if (basicGrid) {
|
|
const pairs = [
|
|
["거래 형태", detail.trade],
|
|
["표시 가격", detail.price],
|
|
["유형", detail.type],
|
|
["전용 면적", detail.area],
|
|
["방과 욕실", detail.rooms],
|
|
["입주 가능", detail.moveIn]
|
|
];
|
|
basicGrid.replaceChildren(...pairs.map(([label, value]) => {
|
|
const wrapper = document.createElement("div");
|
|
const term = document.createElement("dt");
|
|
const description = document.createElement("dd");
|
|
term.textContent = label;
|
|
description.textContent = value;
|
|
wrapper.append(term, description);
|
|
return wrapper;
|
|
}));
|
|
}
|
|
|
|
updateInspectorBackdrop();
|
|
if (focus) $("[data-close-inspector]", inspector)?.focus();
|
|
announce(`${detail.title} 상세 정보를 열었습니다. ${detail.status} 상태입니다.`);
|
|
};
|
|
|
|
const closeInspector = ({ restoreFocus = false } = {}) => {
|
|
if (!inspector?.classList.contains("is-open")) return;
|
|
const previousId = state.selectedId;
|
|
inspector.classList.remove("is-open");
|
|
workbench?.classList.add("is-inspector-closed");
|
|
getRows().forEach((row) => {
|
|
row.classList.remove("is-selected");
|
|
row.setAttribute("aria-selected", "false");
|
|
});
|
|
if (inspectorBackdrop) inspectorBackdrop.hidden = true;
|
|
state.selectedId = null;
|
|
if (restoreFocus && previousId) {
|
|
$(`[data-queue-row][data-id="${previousId}"] [data-select-row]`)?.focus();
|
|
}
|
|
announce("선택한 매물 정보를 닫았습니다.");
|
|
};
|
|
|
|
const activateTab = (name, focus = false) => {
|
|
$$('[role="tab"]', inspector).forEach((tab) => {
|
|
const active = tab.dataset.tab === name;
|
|
tab.setAttribute("aria-selected", String(active));
|
|
tab.tabIndex = active ? 0 : -1;
|
|
if (active && focus) tab.focus();
|
|
});
|
|
$$('[data-panel]', inspector).forEach((panel) => {
|
|
panel.hidden = panel.dataset.panel !== name;
|
|
});
|
|
};
|
|
|
|
const focusMissing = () => {
|
|
if (!state.selectedId) openInspector("DEMO-HJ-101");
|
|
activateTab("legal");
|
|
window.requestAnimationFrame(() => {
|
|
const missing = $("#missing-orientation");
|
|
missing?.scrollIntoView({ block: "center", behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
missing?.focus();
|
|
announce("첫 누락 항목인 방향 판단 근거로 이동했습니다.");
|
|
});
|
|
};
|
|
|
|
const closeSidebar = () => {
|
|
sidebar?.classList.remove("is-open");
|
|
if (sidebarBackdrop) sidebarBackdrop.hidden = true;
|
|
$("[data-open-sidebar]")?.setAttribute("aria-expanded", "false");
|
|
};
|
|
|
|
const openSidebar = () => {
|
|
sidebar?.classList.add("is-open");
|
|
if (sidebarBackdrop) sidebarBackdrop.hidden = false;
|
|
$("[data-open-sidebar]")?.setAttribute("aria-expanded", "true");
|
|
$("[data-close-sidebar]")?.focus();
|
|
};
|
|
|
|
const updateResultCount = () => {
|
|
const visible = getRows().filter((row) => !row.classList.contains("is-hidden"));
|
|
const result = $("[data-result-count]");
|
|
if (result) result.textContent = `${visible.length}건`;
|
|
const empty = $("[data-queue-empty]");
|
|
if (state.mode === "normal") empty.hidden = visible.length !== 0;
|
|
return visible.length;
|
|
};
|
|
|
|
const applyFilters = () => {
|
|
const search = ($("[data-queue-search]")?.value || "").trim().toLocaleLowerCase("ko-KR");
|
|
const status = $("[data-status-filter]")?.value || "all";
|
|
getRows().forEach((row) => {
|
|
const matchesSearch = !search || row.textContent.toLocaleLowerCase("ko-KR").includes(search);
|
|
const matchesStatus = status === "all" || row.dataset.status === status;
|
|
row.classList.toggle("is-hidden", !(matchesSearch && matchesStatus));
|
|
});
|
|
const count = updateResultCount();
|
|
const emptyTitle = $("[data-empty-title]");
|
|
const emptyCopy = $("[data-empty-copy]");
|
|
if (count === 0 && state.mode === "normal") {
|
|
emptyTitle.textContent = "현재 조건에 맞는 항목이 없습니다.";
|
|
emptyCopy.textContent = "검색어와 필터를 초기화하면 전체 대기열을 다시 볼 수 있습니다.";
|
|
}
|
|
};
|
|
|
|
const sortRows = () => {
|
|
const sort = $("[data-sort-select]")?.value || "updated-desc";
|
|
const rows = getRows();
|
|
const compare = {
|
|
"updated-desc": (a, b) => b.dataset.updated.localeCompare(a.dataset.updated),
|
|
"updated-asc": (a, b) => a.dataset.updated.localeCompare(b.dataset.updated),
|
|
"title-asc": (a, b) => a.dataset.title.localeCompare(b.dataset.title, "ko"),
|
|
"status-asc": (a, b) => a.dataset.status.localeCompare(b.dataset.status, "ko")
|
|
}[sort];
|
|
rows.sort(compare).forEach((row) => queueBody.append(row));
|
|
announce("대기열 정렬을 바꿨습니다.");
|
|
};
|
|
|
|
const resetModes = () => {
|
|
queuePanel?.setAttribute("aria-busy", "false");
|
|
const loading = $("[data-queue-loading]");
|
|
const empty = $("[data-queue-empty]");
|
|
if (loading) loading.hidden = true;
|
|
if (empty) empty.hidden = true;
|
|
hideBanner();
|
|
document.body.classList.remove("is-permission-limited", "is-stale-demo");
|
|
$$('[data-review-ai], [data-simulate-save], [data-new-draft]').forEach((button) => button.disabled = false);
|
|
applyFilters();
|
|
};
|
|
|
|
const simulateState = (mode) => {
|
|
state.mode = mode;
|
|
resetModes();
|
|
|
|
if (mode === "loading") {
|
|
queuePanel?.setAttribute("aria-busy", "true");
|
|
$("[data-queue-loading]").hidden = false;
|
|
announce("대기열을 불러오는 중입니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "empty") {
|
|
const empty = $("[data-queue-empty]");
|
|
empty.hidden = false;
|
|
$("[data-empty-title]").textContent = "아직 표시할 항목이 없습니다.";
|
|
$("[data-empty-copy]").textContent = "검색어와 필터를 초기화하면 전체 목록을 다시 볼 수 있습니다.";
|
|
announce("현재 표시할 항목이 없습니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "error") {
|
|
showBanner("대기열 일부를 불러오지 못했습니다.", "마지막으로 확인한 예시 자료를 유지합니다. 재시도 전에는 게시 상태를 바꿀 수 없습니다.", "error");
|
|
announce("불러오기 실패 상태입니다. 마지막 확인 자료를 보여 줍니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "save-error") {
|
|
showBanner("지금은 변경 내용을 보관할 수 없습니다.", "입력은 화면에 남지만 완료로 표시하지 않습니다.", "error");
|
|
announce("저장할 수 없는 상태입니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "stale") {
|
|
document.body.classList.add("is-stale-demo");
|
|
showBanner("선택 매물의 자료가 오래됐습니다.", "가격, 거래 가능 상태와 현장 확인을 다시 보기 전에는 게시 준비로 이동할 수 없습니다.");
|
|
if (!state.selectedId) openInspector("DEMO-HJ-103");
|
|
const status = $("[data-inspector-status]");
|
|
if (status) {
|
|
status.textContent = "자료 오래됨";
|
|
status.className = "status-badge status-warn";
|
|
}
|
|
announce("자료가 오래되어 다시 확인이 필요합니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "conflict") {
|
|
showBanner("다른 담당자가 먼저 수정했습니다.", "현재 내용과 최신 자료의 차이를 확인한 뒤 다시 저장해야 합니다.", "error");
|
|
announce("최신 자료와 차이가 있어 다시 확인이 필요합니다.");
|
|
return;
|
|
}
|
|
|
|
if (mode === "permission") {
|
|
document.body.classList.add("is-permission-limited");
|
|
$$('[data-review-ai], [data-simulate-save], [data-new-draft]').forEach((button) => button.disabled = true);
|
|
showBanner("중개보조 권한으로 보는 중입니다.", "초안과 일정은 정리할 수 있지만 법정 검수 승인, AI 최종 승인과 게시를 할 수 없습니다.", "info");
|
|
announce("현재 권한으로는 승인과 게시를 할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
announce("기본 목록을 다시 보여 줍니다.");
|
|
};
|
|
|
|
const openDialog = (dialog) => {
|
|
if (!dialog) return;
|
|
dialog.showModal();
|
|
document.body.classList.add("is-locked");
|
|
};
|
|
|
|
const closeDialog = (dialog) => {
|
|
if (dialog?.open) dialog.close();
|
|
};
|
|
|
|
const simulateSave = () => {
|
|
if (state.mode === "permission") {
|
|
announce("현재 권한으로는 매물 정보를 수정할 수 없습니다.");
|
|
return;
|
|
}
|
|
if (state.mode === "conflict") {
|
|
showBanner("최신 자료와 차이가 있어 저장하지 않았습니다.", "최신 내용을 다시 확인하거나 현재 수정을 별도 초안으로 남겨야 합니다.", "error");
|
|
announce("버전 충돌로 저장하지 않았습니다.");
|
|
return;
|
|
}
|
|
if (state.mode === "save-error") {
|
|
showBanner("저장에 실패했습니다.", "변경 내용은 화면에 남아 있습니다. 성공 기록과 감사 로그는 만들지 않았습니다.", "error");
|
|
announce("저장에 실패했습니다. 성공으로 처리하지 않았습니다.");
|
|
return;
|
|
}
|
|
announce("변경 내용을 확인했습니다. 실제로 저장되거나 전송되지는 않습니다.");
|
|
};
|
|
|
|
const createDraft = () => {
|
|
if (state.mode === "permission") {
|
|
announce("현재 권한으로는 새 초안을 만들 수 없습니다.");
|
|
return;
|
|
}
|
|
if (state.draftCreated) {
|
|
openInspector("DEMO-HJ-NEW", true);
|
|
return;
|
|
}
|
|
state.draftCreated = true;
|
|
listingDetails["DEMO-HJ-NEW"] = {
|
|
title: "제목 없는 새 항목",
|
|
status: "초안",
|
|
statusClass: "status-demo",
|
|
trade: "미선택",
|
|
type: "미선택",
|
|
price: "입력 전",
|
|
area: "입력 전",
|
|
rooms: "입력 전",
|
|
moveIn: "입력 전",
|
|
image: "../assets/hannam-interior-720.webp",
|
|
imageAlt: "큰 창과 목재 수납이 있는 거실의 연출 이미지",
|
|
blockers: "필수 입력 대기",
|
|
ai: "실행 전"
|
|
};
|
|
|
|
const row = document.createElement("tr");
|
|
row.dataset.queueRow = "";
|
|
row.dataset.id = "DEMO-HJ-NEW";
|
|
row.dataset.status = "draft";
|
|
row.dataset.updated = new Date().toISOString();
|
|
row.dataset.title = "제목 없는 새 항목";
|
|
row.setAttribute("aria-selected", "false");
|
|
row.innerHTML = `
|
|
<td class="row-cell-select"><button type="button" class="row-select-overlay" data-select-row aria-label="제목 없는 새 항목 상세 정보 열기"></button><span class="row-title" aria-hidden="true"><span class="row-thumb row-thumb-draft"><i></i></span><span><strong>제목 없는 새 항목</strong><small>새 예시 매물</small></span></span></td>
|
|
<td><span>미선택</span><small>유형 미선택</small></td><td><strong>입력 전</strong><small>면적 입력 전</small></td><td><span>방금</span><small>새 항목</small></td><td><span class="cell-status is-blocked"><i aria-hidden="true"></i>입력 대기</span></td><td><span class="cell-status is-muted"><i aria-hidden="true"></i>분석 전</span></td><td><span class="assignee"><i>김</i>김유진</span></td>`;
|
|
queueBody.prepend(row);
|
|
applyFilters();
|
|
openInspector("DEMO-HJ-NEW", true);
|
|
announce("새 항목을 만들었습니다. 실제로 저장되거나 전송되지는 않습니다.");
|
|
};
|
|
|
|
const handleRowSelection = (button) => {
|
|
const row = button.closest("[data-queue-row]");
|
|
if (row) openInspector(row.dataset.id, true);
|
|
};
|
|
|
|
const initTabs = () => {
|
|
const tabs = $$('[role="tab"]', inspector);
|
|
tabs.forEach((tab, index) => {
|
|
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
|
tab.addEventListener("keydown", (event) => {
|
|
let targetIndex = null;
|
|
if (event.key === "ArrowRight") targetIndex = (index + 1) % tabs.length;
|
|
if (event.key === "ArrowLeft") targetIndex = (index - 1 + tabs.length) % tabs.length;
|
|
if (event.key === "Home") targetIndex = 0;
|
|
if (event.key === "End") targetIndex = tabs.length - 1;
|
|
if (targetIndex !== null) {
|
|
event.preventDefault();
|
|
activateTab(tabs[targetIndex].dataset.tab, true);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const initDialogs = () => {
|
|
const blockDialog = $("[data-block-dialog]");
|
|
const previewDialog = $("[data-preview-dialog]");
|
|
|
|
$$('[data-block-publish]').forEach((button) => button.addEventListener("click", () => openDialog(blockDialog)));
|
|
$$('[data-open-preview]').forEach((button) => button.addEventListener("click", () => openDialog(previewDialog)));
|
|
|
|
$$('[data-close-dialog]').forEach((button) => button.addEventListener("click", () => closeDialog(button.closest("dialog"))));
|
|
$$('dialog').forEach((dialog) => {
|
|
dialog.addEventListener("click", (event) => {
|
|
if (event.target === dialog) closeDialog(dialog);
|
|
});
|
|
dialog.addEventListener("close", () => {
|
|
document.body.classList.remove("is-locked");
|
|
closeInspector();
|
|
});
|
|
dialog.addEventListener("cancel", () => document.body.classList.remove("is-locked"));
|
|
});
|
|
};
|
|
|
|
const initImageFallback = () => {
|
|
$$('img').forEach((image) => {
|
|
const handleError = () => {
|
|
image.hidden = true;
|
|
image.parentElement?.classList.add("is-image-error");
|
|
image.parentElement?.setAttribute("data-image-error", "연출 이미지를 불러오지 못했습니다");
|
|
};
|
|
image.addEventListener("error", handleError);
|
|
if (image.complete && image.naturalWidth === 0) handleError();
|
|
});
|
|
};
|
|
|
|
const initGlobalActions = () => {
|
|
$("[data-close-inspector]")?.addEventListener("click", () => closeInspector({ restoreFocus: true }));
|
|
inspectorBackdrop?.addEventListener("click", () => closeInspector({ restoreFocus: true }));
|
|
$("[data-open-sidebar]")?.addEventListener("click", openSidebar);
|
|
$("[data-close-sidebar]")?.addEventListener("click", closeSidebar);
|
|
sidebarBackdrop?.addEventListener("click", closeSidebar);
|
|
$("[data-dismiss-banner]")?.addEventListener("click", hideBanner);
|
|
$("[data-reset-filter]")?.addEventListener("click", () => {
|
|
$("[data-queue-search]").value = "";
|
|
$("[data-status-filter]").value = "all";
|
|
state.mode = "normal";
|
|
$("[data-state-simulator]").value = "normal";
|
|
resetModes();
|
|
announce("검색과 필터를 초기화했습니다.");
|
|
});
|
|
$("[data-queue-search]")?.addEventListener("input", applyFilters);
|
|
$("[data-status-filter]")?.addEventListener("change", applyFilters);
|
|
$("[data-sort-select]")?.addEventListener("change", sortRows);
|
|
$("[data-state-simulator]")?.addEventListener("change", (event) => simulateState(event.target.value));
|
|
$$('[data-focus-missing]').forEach((button) => button.addEventListener("click", focusMissing));
|
|
$$('[data-simulate-save]').forEach((button) => button.addEventListener("click", simulateSave));
|
|
$("[data-new-draft]")?.addEventListener("click", createDraft);
|
|
|
|
queueBody?.addEventListener("click", (event) => {
|
|
const button = event.target.closest("[data-select-row]");
|
|
if (button) handleRowSelection(button);
|
|
});
|
|
|
|
$("[data-review-ai]")?.addEventListener("click", (event) => {
|
|
if (state.mode === "permission") {
|
|
announce("현재 권한으로는 AI 결과를 승인할 수 없습니다.");
|
|
return;
|
|
}
|
|
event.currentTarget.textContent = "수정 후 승인됨";
|
|
event.currentTarget.disabled = true;
|
|
const badge = $("#inspector-ai .status-badge");
|
|
if (badge) {
|
|
badge.textContent = "수정 후 승인";
|
|
badge.className = "status-badge status-pass";
|
|
}
|
|
announce("AI 참고 결과를 수정 후 확인 상태로 바꿨습니다. 실제 광고에는 사용되지 않습니다.");
|
|
});
|
|
|
|
$$('[data-claim-action]').forEach((button) => button.addEventListener("click", () => {
|
|
button.textContent = button.dataset.claimAction === "edit" ? "수정 반영" : "확인됨";
|
|
button.disabled = true;
|
|
announce("표시 문구의 검토 상태를 이 화면에서만 바꿨습니다.");
|
|
}));
|
|
|
|
$("[data-reschedule]")?.addEventListener("click", (event) => {
|
|
const card = event.currentTarget.closest(".appointment-card");
|
|
const time = $("div > strong", card);
|
|
time.textContent = time.textContent.includes("3:30") ? "오후 5:00~5:30" : "오후 3:30~4:00";
|
|
announce("안내용 상담 시간을 바꿨습니다. 알림은 전송되지 않았습니다.");
|
|
});
|
|
|
|
$("[data-simulate-log-failure]")?.addEventListener("click", () => {
|
|
showBanner("감사 로그를 불러오지 못했습니다.", "변경 기록을 확인할 수 없는 동안 저장과 게시를 성공으로 처리하지 않습니다.", "error");
|
|
announce("감사 로그 실패 상태입니다. 게시를 중지합니다.");
|
|
});
|
|
|
|
$("[data-command-button]")?.addEventListener("click", () => {
|
|
$("[data-queue-search]")?.focus();
|
|
announce("매물 검색으로 이동했습니다.");
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "k") {
|
|
event.preventDefault();
|
|
$("[data-queue-search]")?.focus();
|
|
}
|
|
if (event.key === "Escape" && !$('dialog[open]')) {
|
|
if (sidebar?.classList.contains("is-open")) closeSidebar();
|
|
else closeInspector({ restoreFocus: true });
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
if (!inspector?.classList.contains("is-open") || $('dialog[open]')) return;
|
|
const path = event.composedPath();
|
|
const matchesPath = (selector) => path.some((node) => node instanceof Element && node.matches(selector));
|
|
const keptOpen = matchesPath("[data-inspector], [data-select-row], [data-inspector-trigger], [data-inspector-backdrop], [data-open-preview], [data-block-publish], [data-focus-missing]");
|
|
if (!keptOpen && !matchesPath("[data-open-sidebar], [data-sidebar]")) closeInspector();
|
|
});
|
|
|
|
window.matchMedia("(max-width: 76rem)").addEventListener("change", updateInspectorBackdrop);
|
|
window.matchMedia("(min-width: 50.01rem)").addEventListener("change", (event) => {
|
|
if (event.matches) closeSidebar();
|
|
});
|
|
|
|
$$('.sidebar-nav a[href^="#inspector-"]').forEach((link) => link.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
if (!state.selectedId) openInspector("DEMO-HJ-101");
|
|
activateTab(link.getAttribute("href").replace("#inspector-", ""), true);
|
|
closeSidebar();
|
|
}));
|
|
};
|
|
|
|
const init = () => {
|
|
initTabs();
|
|
initDialogs();
|
|
initImageFallback();
|
|
initGlobalActions();
|
|
openInspector("DEMO-HJ-101");
|
|
updateResultCount();
|
|
};
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|