(() => { "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 = `