사용기간 확인중... AI 패키지 0/0개 · 잔여 0건 지원서 작성

기업 관리자

A안 구동 지원자가 제출하면 이 리스트에 즉시 뜹니다(localStorage).
전체 지원자
0
오늘 지원자
0
접수
0
합격(데모)
0
상태 분포
최근 7일 지원 추이
클릭하면 상세
지원자 상세
• AI 평가/AI 분석 리포트/유입경로 분석은 “고급 옵션”으로 분리 예정
• 오늘은 데모 값(가짜)로 표시
리스트에서 지원자를 클릭하세요.
선택 0건
관리자 로그(최근 50건)
AI Workspace

AI 기능

선택한 지원자를 기준으로 AI 평가와 AI 분석 리포트를 개별 실행합니다.
선택 0건
지원자 목록
AI 작업 대상만 빠르게 고릅니다.
0명
AI 평가
기존 유료 기능입니다. 점수/합불/요약 중심으로 평가 결과를 생성합니다.
• 결과 필드: 점수 / 합불 추천 / 요약 / 강점 / 리스크
• 실행 대상: 현재 선택한 지원서
AI 분석 리포트
카드형 요약 UI로 보여주는 별도 AI 아이템입니다.
• 결과 필드: 추천 / 점수 / 직무 연관성 / 핵심 역량
• 실행 후 우측 카드 미리보기에 동일 UI로 노출
AI 분석 리포트 미리보기
섹션 6 카드와 동일한 톤으로 선택 지원자의 리포트를 보여줍니다.
선택된 지원자 없음
채팅방
방 선택
관리자 모드
공고 수정
첫 화면(지원서/공고)에서 보이는 내용과 브랜딩을 수정합니다.

우측 카드 정보

이미지

본문 텍스트

미리보기 지원서 화면 보기
첫 화면 미리보기
job-example 실시간
D-?
(예시) 채용 공고 제목
접수기간:
근무지:
채용 구분
채용 형태
신입/경력
마감일
저장 후 job-example.html에서 바로 확인하세요.
공고 화면 구성(예시 UI)
job-example.html 화면에 보이는 요소를 수정합니다.

상단 네비게이션

채용공고/채용안내/마이페이지/로그인은 기본 고정으로 운영됩니다.

히어로 영역

70%
공고 히어로 강조 배경, 동의/지원서 진행 패널, 도움 카드 등 큰 강조 영역의 색상과 강도를 조절합니다.
상단 버튼 1/2/3은 기본 고정으로 운영됩니다.

섹션 타이틀/요약 라벨

모집 분야 및 상세내용, 채용 프로세스, 문의 사항, 근무지 안내, 요약 라벨(근무지/고용 형태/접수 기간)은 기본 고정으로 운영됩니다.

모집/상세 테이블

항목별로 추가/삭제하고 내용은 줄바꿈으로 입력하면 됩니다.

복리후생 카드

아이콘은 드롭다운에서 선택하고 카드 추가/삭제로 편집합니다.

채용 프로세스

단계별로 추가/삭제로 간편 편집합니다.

문의

지도

지원서 설정
운영 필수 옵션만 커스텀합니다. (자기소개서 문항, 학력 확장 옵션)

NCS 블라인드 채용

아래 항목은 지원서 전 구간에서 공통 적용됩니다.

지원 단계

기본 단계(인적사항~자기소개서)는 고정, 옵션 단계만 ON/OFF 가능합니다.
추가서류/증빙 단계의 기본 업로드는 최대 50MB 기준으로 제공합니다.

동의/제출 정책

동의 단계/동의 문구는 기본 고정으로 운영됩니다.

문의(채팅) 설정

OFF 시 지원서/공고에서 문의하기가 숨김됩니다. 보관일수는 구매 기간 기준 운영 권장.

AI 지원 설정

지원자 작성 화면의 입사지원서 AI 지원기능 노출 여부를 설정합니다.

기본정보 라벨

이름/이메일/휴대폰/직무/주소/사진 라벨은 기본 고정으로 운영됩니다.

자기소개서 파트

기본 4문항으로 시작하며, 기업별로 문항 제목 수정/추가/삭제가 가능합니다.

이력·학력 파트

기본 구성은 고정되며, 학력 확장 여부만 설정합니다.
직무 목록 (쉼표로 구분)
최대 1개
입력 0개 / 최대 1개 구매한 직무 수까지만 저장됩니다.
주소/사진 노출은 상단 NCS 블라인드 항목에서 제어합니다.
학력 단계/자격증 항목/대외경험 항목은 기본 고정으로 운영됩니다.

면접 예약 설정(MVP)

`; } function hubPrintSelected(){ const win = window.open("", "_blank", "width=1080,height=840"); if(!win){ alert("팝업이 차단되었습니다. 팝업 허용 후 다시 시도해 주세요."); return; } try{ win.document.open(); win.document.write("출력 준비중출력 문서를 준비하고 있습니다... "); win.document.close(); }catch(_e){} const ids = hubGetSelectedIds(); if(!ids.length){ win.close(); alert("먼저 출력할 지원서를 선택해 주세요."); return; } const rows = hubLoadCurrentApps().filter((a) => ids.includes(String(a?.id || ""))); if(!rows.length){ win.close(); alert("선택된 지원서를 찾을 수 없습니다."); return; } const includeTimeline = document.getElementById("hubIncludeTimeline")?.checked !== false; const printMode = document.getElementById("hubPrintMode")?.value || "full"; win.document.open(); win.document.write(hubBuildPrintHtml(rows, { includeTimeline, printMode })); win.document.close(); win.focus(); win.print(); } function hubEsc(v){ return String(v ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function hubMonth(v){ if (!v) return "-"; const m = /^(\d{4})-(\d{2})$/.exec(String(v)); if (m) return `${m[1]}.${m[2]}`; return hubEsc(v); } function hubDate(v){ if (!v) return "-"; const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(v)); if (m) return `${m[1]}.${m[2]}.${m[3]}`; return hubEsc(v); } function hubTimelineSortKey(raw){ if (!raw) return Number.MAX_SAFE_INTEGER; const txt = String(raw || "").trim(); const m1 = /^(\d{4})-(\d{2})-(\d{2})/.exec(txt); if (m1) return (Number(m1[1]) * 10000) + (Number(m1[2]) * 100) + Number(m1[3]); const m2 = /^(\d{4})-(\d{2})$/.exec(txt); if (m2) return (Number(m2[1]) * 10000) + (Number(m2[2]) * 100) + 1; return Number.MAX_SAFE_INTEGER; } function hubTimelineDateLabel(raw){ const txt = String(raw || "").trim(); if (!txt || txt === "-") return "-"; if (/^\d{4}-\d{2}-\d{2}/.test(txt)) return hubDate(txt.slice(0, 10)); if (/^\d{4}-\d{2}$/.test(txt)) return hubMonth(txt); return hubEsc(txt); } function hubBuildTimelineEvents(a){ if (!a || typeof a !== "object") return []; const edu = a.eduItems || {}; const career = a.careerItems || {}; const basicName = a.name || "-"; const out = []; const push = (date, title, desc) => { out.push({ date: date || "-", title: title || "-", desc: desc || "-", key: hubTimelineSortKey(date || "") }); }; if (a.birthDate) push(a.birthDate, "출생", basicName); const extra = edu.extraLevels || {}; ["초등학교", "중학교"].forEach((lv) => { const rows = Array.isArray(extra[lv]) ? extra[lv] : []; rows.forEach((it) => push(it?.gradMonth, lv, `${it?.name || "-"} (${it?.status || "-"})`)); }); (Array.isArray(edu.highSchoolList) ? edu.highSchoolList : []) .forEach((it) => push(it?.gradMonth, "고등학교", `${it?.name || "-"} (${it?.status || "-"})`)); (Array.isArray(edu.universityList) ? edu.universityList : []) .forEach((it) => push(it?.start || it?.end, "대학교", `${it?.name || "-"} / ${it?.major || "-"}`)); (Array.isArray(edu.graduateList) ? edu.graduateList : []) .forEach((it) => push(it?.start || it?.end, "대학원", `${it?.name || "-"} / ${it?.major || "-"}`)); const careerItems = Array.isArray(career.careerItems) ? career.careerItems : []; const overseasItems = Array.isArray(career.overseasItems) ? career.overseasItems : []; if (careerItems.length) { careerItems.forEach((it) => { const org = it?.org || ""; const role = it?.role || ""; if (!org && !role) return; push(it?.start || it?.end, "경력", `${org || "-"} / ${role || "-"}`); }); } else if (career.careerOrg || career.careerRole) { push(career.careerStart || career.careerEnd, "경력", `${career.careerOrg || "-"} / ${career.careerRole || "-"}`); } if (overseasItems.length) { overseasItems.forEach((it) => { const country = it?.country || ""; const program = it?.program || ""; if (!country && !program) return; push(it?.start || it?.end, "해외연수", `${country || "-"} / ${program || "-"}`); }); } else if (career.overseasCountry || career.overseasProgram) { push(career.overseasStart || career.overseasEnd, "해외연수", `${career.overseasCountry || "-"} / ${career.overseasProgram || "-"}`); } if (a.militaryStatus === "군필" || a.militaryStatus === "복무중") { push(a.militaryStart || a.militaryEnd, "군복무", `${a.militaryStatus}${a.militaryDetail ? ` · ${a.militaryDetail}` : ""}`); } else if (a.militaryStatus) { push("-", "병역", `${a.militaryStatus}${a.militaryReason ? ` · ${a.militaryReason}` : ""}`); } if (a.createdAt) { push(a.createdAt, "지원서 제출", `${hubDate(String(a.createdAt).slice(0, 10))} 제출 완료`); } out.sort((x, y) => x.key - y.key); return out; } function hubRenderTimeline(a){ const events = hubBuildTimelineEvents(a); if (!events.length) { return `
타임라인 데이터가 없습니다.
`; } return `
지원 타임라인
${events.map((ev) => `
${hubTimelineDateLabel(ev.date)}
${hubEsc(ev.title)} · ${hubEsc(ev.desc)}
`).join("")}
`; } function hubDraftPairs(draft){ if (!draft || typeof draft !== "object") return []; return Object.entries(draft) .filter(([, value]) => { if (value === null || value === undefined) return false; if (typeof value === "boolean") return value; return String(value).trim() !== ""; }) .map(([key, value]) => ({ label: key.split("::")[0].replaceAll("_", " ").replaceAll("-", " "), value: typeof value === "boolean" ? "예" : String(value) })); } function hubRenderDraft(title, draft){ const pairs = hubDraftPairs(draft); if (!pairs.length) return ""; return `
${hubEsc(title)} (${pairs.length}개)
${pairs.slice(0, 6).map((it) => `
${hubEsc(it.label)} ${hubEsc(it.value)}
`).join("")}
`; } function hubRenderVersionHistory(a){ const currentVersion = Math.max(1, Number(a?.version || 1)); const history = Array.isArray(a?.versionHistory) ? a.versionHistory : []; const rows = history .slice() .sort((x, y) => Number(y?.version || 0) - Number(x?.version || 0)) .slice(0, 4); return `
버전 이력
현재 · v${hubEsc(currentVersion)}
최종수정 · ${hubEsc(fmtDate(a?.updatedAt || a?.createdAt || new Date().toISOString()))}
${rows.length ? rows.map((it) => `
이전 · v${hubEsc(it?.version || "-")} (${hubEsc(fmtDate(it?.updatedAt || it?.createdAt || new Date().toISOString()))})
`).join("") : '
이전 버전 없음
'}
`; } function hubStatusOptionsHtml(selected){ const current = selected || "접수"; return HUB_STATUS_OPTIONS.map((st) => { const isSel = st === current ? "selected" : ""; return ``; }).join(""); } function hubLoadCurrentApps(){ return load(); } function hubIsSameApplication(a, target){ if (a?.id && target?.id) return a.id === target.id; return (a?.email || "") === (target?.email || "") && (a?.jobId || "") === (target?.jobId || "") && (a?.createdAt || "") === (target?.createdAt || ""); } function hubUpdateApplicationEverywhere(targetApp, patch){ const touched = []; Object.keys(localStorage) .filter((k) => k.startsWith("hireroom_applications_") && k.includes(`_${mode}`)) .forEach((key) => { let arr = []; try { arr = JSON.parse(localStorage.getItem(key) || "[]"); } catch (e) { arr = []; } if (!Array.isArray(arr) || !arr.length) return; let changed = false; const patched = arr.map((it) => { if (!hubIsSameApplication(it, targetApp)) return it; changed = true; return { ...it, ...patch, updatedAt: new Date().toISOString() }; }); if (changed) { localStorage.setItem(key, JSON.stringify(patched)); touched.push(key); } }); return touched.length; } function hubLoadAudit(){ try{ const list = JSON.parse(localStorage.getItem(HUB_AUDIT_KEY) || "[]"); return Array.isArray(list) ? list : []; }catch(e){ return []; } } function hubSaveAudit(list){ localStorage.setItem(HUB_AUDIT_KEY, JSON.stringify(list)); } function hubAppendAudit(entry){ const list = hubLoadAudit(); list.unshift({ id: `audit_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, at: new Date().toISOString(), actor: (window.FRONT && FRONT.authGet()?.email) || "admin", ...entry }); hubSaveAudit(list.slice(0, 300)); } function hubRenderAudit(){ const el = document.getElementById("hubAuditList"); if(!el) return; const q = (document.getElementById("hubQ")?.value || "").trim().toLowerCase(); const rows = hubLoadAudit() .filter((it) => { if (!q) return true; const target = `${it?.name || ""} ${it?.email || ""}`.toLowerCase(); return target.includes(q); }) .slice(0, 50); if(!rows.length){ el.innerHTML = '
관리자 로그가 없습니다.
'; return; } el.innerHTML = rows.map((it) => `
${hubEsc(it.kind || "변경")} · ${hubEsc(it.name || "-")} (${hubEsc(it.email || "-")})
${hubEsc(it.detail || "-")}
${fmtDate(it.at || new Date().toISOString())}
${hubEsc(it.actor || "-")}
`).join(""); } function hubRenderApplications(){ const q = (document.getElementById("hubQ")?.value || "").trim().toLowerCase(); const st = document.getElementById("hubStatus")?.value || "ALL"; const selectedOnly = document.getElementById("hubSelectedOnly")?.checked === true; const sort = document.getElementById("hubSort")?.value || "latest"; const density = document.getElementById("hubDensity")?.value || "comfortable"; const apps = hubLoadCurrentApps(); const byPair = {}; apps.forEach((a) => { const k = `${a.jobId || "job_demo_1"}_${a.email || ""}`; byPair[k] = (byPair[k] || 0) + 1; }); const arr = apps.filter((a) => { const okQ = !q || (a.name || "").toLowerCase().includes(q) || (a.email || "").toLowerCase().includes(q); const okS = st === "ALL" || (a.status || "접수") === st; const okSelected = !selectedOnly || hubSelectedIds.has(String(a.id || "")); return okQ && okS && okSelected; }).sort((a, b) => { if(sort === "oldest") return new Date(a.createdAt || 0) - new Date(b.createdAt || 0); if(sort === "name_asc") return String(a.name || "").localeCompare(String(b.name || ""), "ko"); if(sort === "ai_score_desc") return Number(b?.ai?.score || -1) - Number(a?.ai?.score || -1); if(sort === "ai_score_asc") return Number(a?.ai?.score || 999) - Number(b?.ai?.score || 999); return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); }); const el = document.getElementById("hubList"); const pagerEl = document.getElementById("hubPager"); const pagerMetaEl = document.getElementById("hubPagerMeta"); if(!el) return; el.classList.toggle("list-density-compact", density === "compact"); if(!arr.length){ el.innerHTML = '
조건에 맞는 지원자가 없습니다.
'; if(pagerEl) pagerEl.style.display = "none"; return; } const totalPages = Math.max(1, Math.ceil(arr.length / HUB_PAGE_SIZE)); if(hubCurrentPage > totalPages) hubCurrentPage = totalPages; const pageRows = arr.slice((hubCurrentPage - 1) * HUB_PAGE_SIZE, hubCurrentPage * HUB_PAGE_SIZE); el.innerHTML = pageRows.map((a) => { const dup = byPair[`${a.jobId || "job_demo_1"}_${a.email || ""}`] > 1; const edu = a.eduItems || {}; const career = a.careerItems || {}; const appId = a.id || ""; const aiScore = a?.ai?.score ?? "-"; const aiPass = a?.ai?.pass ?? "-"; const aiSummary = String(a?.ai?.summary || "").trim(); const aiAnalyzedAt = a?.ai?.analyzedAt ? fmtDate(a.ai.analyzedAt) : ""; const aiStrengths = Array.isArray(a?.ai?.strengths) ? a.ai.strengths.filter(Boolean) : []; const aiRisks = Array.isArray(a?.ai?.risks) ? a.ai.risks.filter(Boolean) : []; const aiNextCheck = String(a?.ai?.nextCheck || "").trim(); const step8Href = `applicant-step8.html?tenantId=${encodeURIComponent(tenantId)}&jobId=${encodeURIComponent(jobId)}&mode=${encodeURIComponent(mode)}&readonly=1${appId ? `&appId=${encodeURIComponent(appId)}` : ""}`; const hs = Array.isArray(edu.highSchoolList) ? edu.highSchoolList : []; const uni = Array.isArray(edu.universityList) ? edu.universityList : []; const grad = Array.isArray(edu.graduateList) ? edu.graduateList : []; const extra = edu.extraLevels || {}; const elem = Array.isArray(extra["초등학교"]) ? extra["초등학교"] : []; const middle = Array.isArray(extra["중학교"]) ? extra["중학교"] : []; const careerRange = career.careerStart || career.careerEnd ? `${hubMonth(career.careerStart)} ~ ${career.careerCurrent ? "현재" : hubMonth(career.careerEnd)}` : "-"; const overseasRange = career.overseasStart || career.overseasEnd ? `${hubMonth(career.overseasStart)} ~ ${hubMonth(career.overseasEnd)}` : "-"; return `
${hubEsc(a.name || "-")} · ${hubEsc(a.position || a.jobTitle || "-")}
${hubEsc(a.email || "-")} · ${fmtDate(a.createdAt || new Date().toISOString())}
AI 점수 ${hubEsc(aiScore)} ${hubEsc(aiPass)} ${aiAnalyzedAt ? `· ${hubEsc(aiAnalyzedAt)}` : ""}
${hubEsc(aiSummary || "AI 평가 전")}
${dup ? "중복의심" : "정상"} ${a.needsReview === true ? '재검토필요' : ""} v${hubEsc(Math.max(1, Number(a.version || 1)))} ${hubEsc(a.status || "접수")} 입사지원서 보기
간략하게 보기
기본 정보
이름 · ${hubEsc(a.name || "-")}
이메일 · ${hubEsc(a.email || "-")}
연락처 · ${hubEsc(a.phone || "-")}
생년월일 · ${hubDate(a.birthDate)}
병역 · ${hubEsc(a.militaryStatus || "-")}
학력
초/중 · ${elem.length + middle.length}건
고등학교 · ${hs.length}건
대학교 · ${uni.length}건
대학원 · ${grad.length}건
경력/해외연수
경력 회사/직무 · ${hubEsc(career.careerOrg || "-")} / ${hubEsc(career.careerRole || "-")}
경력 기간 · ${careerRange}
해외연수 · ${hubEsc(career.overseasCountry || "-")} / ${hubEsc(career.overseasProgram || "-")}
연수 기간 · ${overseasRange}
AI 평가
점수 · ${hubEsc(aiScore)}
합불추천 · ${hubEsc(aiPass)}
적합도 · ${hubEsc(a?.ai?.fit || "-")}
모델 · ${hubEsc(a?.ai?.aiModel || "-")}
프롬프트 버전 · ${hubEsc(a?.ai?.promptVersion || "-")}
요약 · ${hubEsc(aiSummary || "AI 평가 전")}
${aiStrengths.length ? `
강점 · ${hubEsc(aiStrengths.join(" · "))}
` : ""} ${aiRisks.length ? `
리스크 · ${hubEsc(aiRisks.join(" · "))}
` : ""} ${aiNextCheck ? `
추가 확인 · ${hubEsc(aiNextCheck)}
` : ""}
${hubRenderDraft("자격/어학", a.certItems)} ${hubRenderDraft("자기소개서", a.essayItems)} ${hubRenderDraft("포트폴리오", a.portfolioItems)} ${hubRenderDraft("추가서류", a.documentItems)}
${hubRenderVersionHistory(a)} ${hubRenderTimeline(a)}
`; }).join(""); if(pagerEl) pagerEl.style.display = totalPages > 1 ? "" : "none"; if(pagerMetaEl) pagerMetaEl.textContent = `${hubCurrentPage} / ${totalPages} 페이지 · 총 ${arr.length}건`; const prevBtn = document.getElementById("hubPrevPageBtn"); const nextBtn = document.getElementById("hubNextPageBtn"); if(prevBtn) prevBtn.disabled = hubCurrentPage <= 1; if(nextBtn) nextBtn.disabled = hubCurrentPage >= totalPages; hubSyncSelectedCount(); hubRenderAiWorkspace(); } const hubListEl = document.getElementById("hubList"); if(hubListEl){ hubListEl.addEventListener("change", (e) => { const chk = e.target.closest(".hubSelectChk"); if(chk){ const id = String(chk.getAttribute("data-app-id") || ""); if(id){ if(chk.checked) hubSelectedIds.add(id); else hubSelectedIds.delete(id); } hubSyncSelectedCount(); hubRenderAiWorkspace(); return; } if(isReadonly){ alert("읽기전용 검수 세션에서는 상태를 변경할 수 없습니다."); hubRenderApplications(); return; } const sel = e.target.closest(".hubStatusSelect"); if(!sel) return; const target = { id: sel.getAttribute("data-app-id") || "", email: sel.getAttribute("data-app-email") || "", jobId: sel.getAttribute("data-app-jobid") || "", createdAt: sel.getAttribute("data-app-created") || "", name: sel.getAttribute("data-app-name") || "" }; const nextStatus = sel.value || "접수"; const beforeStatus = sel.getAttribute("data-prev-status") || "접수"; const count = hubUpdateApplicationEverywhere(target, { status: nextStatus, needsReview: false, reviewedAt: new Date().toISOString() }); if(!count){ alert("[상태 변경 실패] 대상 지원서를 찾지 못했습니다."); return; } hubAppendAudit({ kind: "상태 변경", email: target.email, name: target.name, detail: `${beforeStatus} → ${nextStatus}` }); hubRenderApplications(); hubRenderAudit(); hubRenderAiWorkspace(); }); hubListEl.addEventListener("click", (e) => { if(isReadonly){ alert("읽기전용 검수 세션에서는 수정 허용 상태를 변경할 수 없습니다."); hubRenderApplications(); return; } const btn = e.target.closest(".hubLockToggleBtn"); if(!btn) return; const target = { id: btn.getAttribute("data-app-id") || "", email: btn.getAttribute("data-app-email") || "", jobId: btn.getAttribute("data-app-jobid") || "", createdAt: btn.getAttribute("data-app-created") || "", name: btn.getAttribute("data-app-name") || "" }; const currentlyLocked = btn.getAttribute("data-locked") === "1"; const nextLocked = !currentlyLocked; const count = hubUpdateApplicationEverywhere(target, { locked: nextLocked }); if(!count){ alert("[잠금 변경 실패] 대상 지원서를 찾지 못했습니다."); return; } hubAppendAudit({ kind: "수정 상태 변경", email: target.email, name: target.name, detail: currentlyLocked ? "수정불가 → 수정허용" : "수정허용 → 수정불가" }); hubRenderApplications(); hubRenderAudit(); }); } const hubSelectAllBtn = document.getElementById("hubSelectAllBtn"); if(hubSelectAllBtn){ hubSelectAllBtn.addEventListener("click", () => { const checks = Array.from(document.querySelectorAll("#hubList .hubSelectChk")); checks.forEach((chk) => { const id = String(chk.getAttribute("data-app-id") || ""); if(id) hubSelectedIds.add(id); }); hubRenderApplications(); }); } const hubSelectClearBtn = document.getElementById("hubSelectClearBtn"); if(hubSelectClearBtn){ hubSelectClearBtn.addEventListener("click", () => { hubSelectedIds.clear(); hubRenderApplications(); }); } const hubBulkStatusBtn = document.getElementById("hubBulkStatusBtn"); if(hubBulkStatusBtn){ hubBulkStatusBtn.addEventListener("click", () => { const nextStatus = String(document.getElementById("hubBulkStatus")?.value || ""); if(!nextStatus){ alert("적용할 상태를 먼저 선택해 주세요."); return; } const rows = hubGetSelectedApps(); if(!rows.length){ alert("상태 변경할 지원서를 먼저 선택해 주세요."); return; } let changed = 0; rows.forEach((app) => { changed += hubUpdateApplicationEverywhere({ id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }, { status: nextStatus, needsReview: false, reviewedAt: new Date().toISOString() }); }); hubAppendAudit({ kind: "일괄 상태 변경", email: "-", name: "-", detail: `${rows.length}건 → ${nextStatus}` }); hubRenderApplications(); hubRenderAudit(); hubRenderAiWorkspace(); alert(`선택 ${rows.length}건 상태를 ${nextStatus}로 변경했습니다.`); }); } const hubResetAiBtn = document.getElementById("hubResetAiBtn"); if(hubResetAiBtn){ hubResetAiBtn.addEventListener("click", () => { const rows = hubGetSelectedApps(); if(!rows.length){ alert("AI 결과를 초기화할 지원서를 먼저 선택해 주세요."); return; } rows.forEach((app) => { hubUpdateApplicationEverywhere({ id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }, { ai: null }); }); hubAppendAudit({ kind: "AI 결과 초기화", email: "-", name: "-", detail: `${rows.length}건 AI 결과 삭제` }); hubRenderApplications(); hubRenderAudit(); hubRenderAiWorkspace(); alert(`선택 ${rows.length}건의 AI 결과를 초기화했습니다.`); }); } const hubPrintBtn = document.getElementById("hubPrintBtn"); if(hubPrintBtn){ hubPrintBtn.addEventListener("click", () => hubPrintSelected()); } const hubPdfBtn = document.getElementById("hubPdfBtn"); if(hubPdfBtn){ hubPdfBtn.addEventListener("click", () => hubPrintSelected()); } async function hubRunAiBatch(options){ const forceReanalyze = options?.forceReanalyze === true; const triggerBtn = options?.triggerBtn || document.getElementById("hubAiBtn"); const label = forceReanalyze ? "AI 재분석" : "AI 평가(유료)"; const gate = hubGetAiGateState(); if(!gate.canUse){ alert(`${gate.reason}\n\n결제 페이지에서 플랜/옵션을 확인해 주세요.`); return; } const apps = hubGetAiWorkspaceApps(); if(!apps.length){ alert(forceReanalyze ? "AI 재분석할 지원서를 먼저 선택해 주세요." : "AI 평가할 지원서를 먼저 선택해 주세요."); return; } if(gate.quotaLimit > 0 && apps.length > gate.quotaRemain){ alert(`선택 건수(${apps.length}건)가 남은 AI 분석 한도(${gate.quotaRemain}건)를 초과했습니다.`); return; } const selectionState = hubGetSelectionAiState(apps); if(!forceReanalyze && selectionState.allCurrent){ alert("동일 입력 기준으로 이미 평가가 완료되었습니다. 다시 생성하려면 AI 재분석을 사용하세요."); hubRefreshAiGate(); return; } if(forceReanalyze && !selectionState.hasAnyCurrent){ alert("재분석은 기존 AI 결과가 있는 지원서에만 사용할 수 있습니다."); hubRefreshAiGate(); return; } const prevLabel = triggerBtn?.textContent || label; const startedAt = Date.now(); const quotaBefore = Math.max(0, Number(gate.quotaRemain || 0) || 0); if(triggerBtn){ triggerBtn.disabled = true; triggerBtn.textContent = `${forceReanalyze ? "AI 재분석중" : "AI 평가중"}... (0/${apps.length})`; } let okCount = 0; let failCount = 0; let reusedCount = 0; let retryRecoveredCount = 0; const failDetails = []; const failReasons = []; try{ for(let i = 0; i < apps.length; i += 1){ const app = apps[i]; try{ const sourceHash = hubBuildAiFingerprint(app); const existingAi = app?.ai && typeof app.ai === "object" ? app.ai : null; const isCurrent = !!( existingAi && String(existingAi.sourceHash || "") === sourceHash && String(existingAi.evalVersion || "") === HUB_AI_EVAL_VERSION ); if(!forceReanalyze && isCurrent){ reusedCount += 1; continue; } const { ai, attempt } = await hubRequestAiEvaluationWithRetry(app, 3); const target = { id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }; const prevAi = existingAi ? JSON.parse(JSON.stringify(existingAi)) : null; const prevHistory = Array.isArray(app?.aiHistory) ? app.aiHistory : []; const nextAnalysisVersion = Math.max(1, Number(existingAi?.analysisVersion || 0) || 0) + 1; const nextHistory = prevAi ? [...prevHistory, prevAi].slice(-10) : prevHistory.slice(-10); const changed = hubUpdateApplicationEverywhere(target, { ai: { score: ai.score, pass: ai.pass, summary: ai.summary, fit: ai.fit, recommendation: ai.recommendation, strengths: Array.isArray(ai.strengths) ? ai.strengths.slice(0, 3) : [], risks: Array.isArray(ai.risks) ? ai.risks.slice(0, 3) : [], nextCheck: ai.nextCheck || "", analyzedAt: new Date().toISOString(), generatedAt: ai.generatedAt || new Date().toISOString(), aiModel: ai.aiModel || "unknown", promptVersion: ai.promptVersion || "unknown", sourceHash, evalVersion: HUB_AI_EVAL_VERSION, analysisVersion: nextAnalysisVersion }, aiHistory: nextHistory }); if(changed) okCount += 1; else failCount += 1; if(attempt > 1){ retryRecoveredCount += 1; console.info(`[AI 분석 재시도 성공] ${attempt}회 시도`, app?.id || app?.email || "-"); } }catch(err){ const reason = String(err?.message || "unknown_error"); console.warn(forceReanalyze ? "[AI 재분석 실패]" : "[AI 평가 실패]", app?.id || app?.email || "-", reason); failDetails.push(`${app?.name || app?.email || app?.id || "-"}:${reason}`); failReasons.push(reason); failCount += 1; }finally{ if(triggerBtn){ triggerBtn.textContent = `${forceReanalyze ? "AI 재분석중" : "AI 평가중"}... (${i + 1}/${apps.length})`; } } } if(okCount > 0){ hubUpsertCurrentReq((req) => { const prevUsed = Math.max(0, Number(req.aiQuotaUsed || 0) || 0); const nextUsed = prevUsed + okCount; const total = Math.max(0, Number(req.aiCreditsTotal ?? req.aiQuotaLimit ?? 0) || 0); req.aiQuotaUsed = nextUsed; req.aiCreditsUsed = nextUsed; req.aiCreditsRemain = Math.max(0, total - nextUsed); return req; }); } const elapsedMs = Math.max(0, Date.now() - startedAt); const quotaAfter = Math.max(0, quotaBefore - okCount); const failSample = failDetails.length ? ` / 실패예시: ${failDetails.slice(0, 2).join(" | ")}` : ""; const failReasonSummary = hubCountFailReasons(failReasons); const failReasonText = failReasonSummary.length ? ` / 실패코드:${failReasonSummary.join(",")}` : ""; hubAppendAudit({ kind: forceReanalyze ? "AI 재분석 실행" : "AI 평가 실행", email: "-", name: "-", detail: `요청 ${apps.length}건 / 신규분석 ${okCount}건 / 재사용 ${reusedCount}건 / 실패 ${failCount}건 / 재시도복구 ${retryRecoveredCount}건 / 크레딧 ${quotaBefore}→${quotaAfter} / ${elapsedMs}ms${failReasonText}${failSample}` }); hubRenderAudit(); if(okCount === 0 && failCount > 0){ alert(`${forceReanalyze ? "AI 재분석" : "AI 평가"} 실패: ${failReasonSummary.join(",") || "unknown_error"}${failSample}`); }else{ alert(`${forceReanalyze ? "AI 재분석" : "AI 평가"} 완료: 신규 ${okCount}건, 재사용 ${reusedCount}건, 실패 ${failCount}건${failSample}`); } hubRenderApplications(); hubRenderAiWorkspace(); hubRefreshAiGate(); } finally { if(triggerBtn){ triggerBtn.disabled = false; triggerBtn.textContent = prevLabel; } } } async function hubRunAiReportBatch(options){ const triggerBtn = options?.triggerBtn || document.getElementById("aiWorkspaceReportBtn"); const gate = hubGetAiGateState(); if(!gate.canUse){ alert(`${gate.reason}\n\n결제 페이지에서 플랜/옵션을 확인해 주세요.`); return; } const apps = hubGetAiWorkspaceApps(); if(!apps.length){ alert("AI 분석 리포트를 생성할 지원서를 먼저 선택해 주세요."); return; } if(gate.quotaLimit > 0 && apps.length > gate.quotaRemain){ alert(`선택 건수(${apps.length}건)가 남은 AI 분석 한도(${gate.quotaRemain}건)를 초과했습니다.`); return; } const prevLabel = triggerBtn?.textContent || "AI 분석 리포트 생성"; const startedAt = Date.now(); const quotaBefore = Math.max(0, Number(gate.quotaRemain || 0) || 0); let okCount = 0; let failCount = 0; const failReasons = []; if(triggerBtn){ triggerBtn.disabled = true; triggerBtn.textContent = `AI 분석 리포트 생성중... (0/${apps.length})`; } try{ for(let i = 0; i < apps.length; i += 1){ const app = apps[i]; try{ const { ai, attempt } = await hubRequestAiEvaluationWithRetry(app, 3); const target = { id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }; const sourceHash = hubBuildAiReportFingerprint(app); const changed = hubUpdateApplicationEverywhere(target, { aiReport: { score: ai.score, pass: ai.pass, summary: ai.summary, fit: ai.fit, recommendation: ai.recommendation, strengths: Array.isArray(ai.strengths) ? ai.strengths.slice(0, 3) : [], risks: Array.isArray(ai.risks) ? ai.risks.slice(0, 3) : [], nextCheck: ai.nextCheck || "", analyzedAt: new Date().toISOString(), generatedAt: ai.generatedAt || new Date().toISOString(), aiModel: ai.aiModel || "unknown", promptVersion: ai.promptVersion || "unknown", sourceHash, evalVersion: HUB_AI_REPORT_VERSION } }); if(changed) okCount += 1; if(attempt > 1){ console.info(`[AI 분석 리포트 재시도 성공] ${attempt}회 시도`, app?.id || app?.email || "-"); } }catch(err){ failCount += 1; failReasons.push(String(err?.message || "unknown_error")); }finally{ if(triggerBtn){ triggerBtn.textContent = `AI 분석 리포트 생성중... (${i + 1}/${apps.length})`; } } } if(okCount > 0){ hubUpsertCurrentReq((req) => { const prevUsed = Math.max(0, Number(req.aiQuotaUsed || 0) || 0); const nextUsed = prevUsed + okCount; const total = Math.max(0, Number(req.aiCreditsTotal ?? req.aiQuotaLimit ?? 0) || 0); req.aiQuotaUsed = nextUsed; req.aiCreditsUsed = nextUsed; req.aiCreditsRemain = Math.max(0, total - nextUsed); return req; }); } const quotaAfter = Math.max(0, quotaBefore - okCount); const failReasonSummary = hubCountFailReasons(failReasons); hubAppendAudit({ kind: "AI 분석 리포트 생성", email: "-", name: "-", detail: `요청 ${apps.length}건 / 생성 ${okCount}건 / 실패 ${failCount}건 / 크레딧 ${quotaBefore}→${quotaAfter} / ${Math.max(0, Date.now() - startedAt)}ms${failReasonSummary.length ? ` / 실패코드:${failReasonSummary.join(",")}` : ""}` }); hubRenderApplications(); hubRenderAudit(); hubRenderAiWorkspace(); hubRefreshAiGate(); if(okCount === 0 && failCount > 0){ alert(`AI 분석 리포트 생성 실패: ${failReasonSummary.join(",") || "unknown_error"}`); } else { alert(`AI 분석 리포트 생성 완료: ${okCount}건${failCount ? ` / 실패 ${failCount}건` : ""}`); } } finally { if(triggerBtn){ triggerBtn.disabled = false; triggerBtn.textContent = prevLabel; } } } const hubAiBtn = document.getElementById("hubAiBtn"); if(hubAiBtn){ hubAiBtn.addEventListener("click", async () => { await hubRunAiBatch({ forceReanalyze: false, triggerBtn: hubAiBtn }); }); } const hubReanalyzeAiBtn = document.getElementById("hubReanalyzeAiBtn"); if(hubReanalyzeAiBtn){ hubReanalyzeAiBtn.addEventListener("click", async () => { await hubRunAiBatch({ forceReanalyze: true, triggerBtn: hubReanalyzeAiBtn }); }); } const hubStickyAiBtn = document.getElementById("hubStickyAiBtn"); if(hubStickyAiBtn && hubAiBtn){ hubStickyAiBtn.addEventListener("click", () => hubAiBtn.click()); } const aiWorkspaceOpenHubBtn = document.getElementById("aiWorkspaceOpenHubBtn"); if(aiWorkspaceOpenHubBtn){ aiWorkspaceOpenHubBtn.addEventListener("click", () => activateMenu("tab-applications-hub")); } const aiWorkspaceList = document.getElementById("aiWorkspaceList"); if(aiWorkspaceList){ aiWorkspaceList.addEventListener("click", (e) => { const btn = e.target.closest(".aiWorkspaceListBtn"); if(!btn) return; aiWorkspaceList.setAttribute("data-active-id", String(btn.getAttribute("data-app-id") || "")); hubRenderAiWorkspace(); }); } const aiWorkspaceSearch = document.getElementById("aiWorkspaceSearch"); if(aiWorkspaceSearch){ aiWorkspaceSearch.addEventListener("input", () => hubRenderAiWorkspace()); } const aiWorkspaceSort = document.getElementById("aiWorkspaceSort"); if(aiWorkspaceSort){ aiWorkspaceSort.addEventListener("change", () => hubRenderAiWorkspace()); } const aiWorkspaceFilters = document.getElementById("aiWorkspaceFilters"); if(aiWorkspaceFilters){ aiWorkspaceFilters.addEventListener("click", (e) => { const btn = e.target.closest("[data-ai-filter]"); if(!btn) return; const next = String(btn.getAttribute("data-ai-filter") || "all"); aiWorkspaceFilters.setAttribute("data-active-filter", next); aiWorkspaceFilters.querySelectorAll("[data-ai-filter]").forEach((node) => { node.classList.toggle("active", node === btn); }); hubRenderAiWorkspace(); }); } const aiWorkspaceEvalBtn = document.getElementById("aiWorkspaceEvalBtn"); if(aiWorkspaceEvalBtn){ aiWorkspaceEvalBtn.addEventListener("click", async () => { await hubRunAiBatch({ forceReanalyze: false, triggerBtn: aiWorkspaceEvalBtn }); }); } const aiWorkspaceEvalReBtn = document.getElementById("aiWorkspaceEvalReBtn"); if(aiWorkspaceEvalReBtn){ aiWorkspaceEvalReBtn.addEventListener("click", async () => { await hubRunAiBatch({ forceReanalyze: true, triggerBtn: aiWorkspaceEvalReBtn }); }); } const aiWorkspaceReportBtn = document.getElementById("aiWorkspaceReportBtn"); if(aiWorkspaceReportBtn){ aiWorkspaceReportBtn.addEventListener("click", async () => { await hubRunAiReportBatch({ triggerBtn: aiWorkspaceReportBtn }); }); } const aiWorkspaceReportResetBtn = document.getElementById("aiWorkspaceReportResetBtn"); if(aiWorkspaceReportResetBtn){ aiWorkspaceReportResetBtn.addEventListener("click", () => { const rows = hubGetSelectedApps(); if(!rows.length){ alert("리포트를 초기화할 지원서를 먼저 선택해 주세요."); return; } rows.forEach((app) => { hubUpdateApplicationEverywhere({ id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }, { aiReport: null }); }); hubAppendAudit({ kind: "AI 분석 리포트 초기화", email: "-", name: "-", detail: `${rows.length}건 리포트 삭제` }); hubRenderApplications(); hubRenderAudit(); hubRenderAiWorkspace(); }); } const hubSeedDummyBtn = document.getElementById("hubSeedDummyBtn"); if(hubSeedDummyBtn){ hubSeedDummyBtn.style.display = (isCentralWorkspace || (!!superAdminSession && !isReadonly)) ? "" : "none"; hubSeedDummyBtn.addEventListener("click", () => { if(!(isCentralWorkspace || (!!superAdminSession && !isReadonly))) return; if(isReadonly){ alert("읽기전용 검수 세션에서는 더미 생성을 사용할 수 없습니다."); return; } const prev = hubLoadCurrentApps(); const next = prev.concat(hubGenerateDummyApplications(5)); save(next); hubAppendAudit({ kind: "더미 지원자 생성", email: "-", name: "-", detail: `자동 생성 5건 / 총 ${next.length}건` }); hubRenderApplications(); hubRenderAudit(); alert("더미 지원자 5건을 생성했습니다."); }); } const hubClearDummyBtn = document.getElementById("hubClearDummyBtn"); if(hubClearDummyBtn){ hubClearDummyBtn.style.display = (isCentralWorkspace || (!!superAdminSession && !isReadonly)) ? "" : "none"; hubClearDummyBtn.addEventListener("click", () => { if(!(isCentralWorkspace || (!!superAdminSession && !isReadonly))) return; const prev = hubLoadCurrentApps(); const next = prev.filter((app) => !String(app?.id || "").startsWith("app_mock_")); save(next); hubSelectedIds.clear(); hubAppendAudit({ kind: "더미 지원자 삭제", email: "-", name: "-", detail: `${prev.length - next.length}건 삭제 / 총 ${next.length}건` }); hubRenderApplications(); hubRenderAudit(); alert(`더미 지원자 ${prev.length - next.length}건을 삭제했습니다.`); }); } const hubAiMockBtn = document.getElementById("hubAiMockBtn"); if(hubAiMockBtn){ hubAiMockBtn.style.display = isCentralWorkspace ? "" : "none"; hubAiMockBtn.addEventListener("click", async () => { if(!isCentralWorkspace) return; if(isReadonly){ alert("읽기전용 검수 세션에서는 AI 모의실행을 사용할 수 없습니다."); return; } const ids = hubGetSelectedIds(); const appsAll = hubLoadCurrentApps(); const apps = ids.length ? appsAll.filter((a) => ids.includes(String(a?.id || ""))) : appsAll; if(!apps.length){ alert("모의실행할 지원서가 없습니다. 더미 생성 후 다시 시도해 주세요."); return; } const prevLabel = hubAiMockBtn.textContent || "AI 모의실행"; hubAiMockBtn.disabled = true; hubAiMockBtn.textContent = `모의실행중... (0/${apps.length})`; let okCount = 0; try{ for(let i = 0; i < apps.length; i += 1){ const app = apps[i]; const ai = hubBuildMockAiResult(app); const sourceHash = hubBuildAiFingerprint(app); const target = { id: app.id || "", email: app.email || "", jobId: app.jobId || "", createdAt: app.createdAt || "", name: app.name || "" }; const changed = hubUpdateApplicationEverywhere(target, { ai: { score: ai.score, pass: ai.pass, summary: ai.summary, fit: ai.fit, recommendation: ai.recommendation, strengths: Array.isArray(ai.strengths) ? ai.strengths.slice(0, 3) : [], risks: Array.isArray(ai.risks) ? ai.risks.slice(0, 3) : [], nextCheck: ai.nextCheck || "", analyzedAt: new Date().toISOString(), generatedAt: ai.generatedAt || new Date().toISOString(), aiModel: ai.aiModel || "mock", promptVersion: ai.promptVersion || "mock", mode: "mock", sourceHash, evalVersion: HUB_AI_EVAL_VERSION } }); if(changed) okCount += 1; hubAiMockBtn.textContent = `모의실행중... (${i + 1}/${apps.length})`; } hubAppendAudit({ kind: "AI 모의실행", email: "-", name: "-", detail: `처리 ${apps.length}건 / 반영 ${okCount}건 / 크레딧 차감 없음` }); hubRenderApplications(); hubRenderAudit(); hubRefreshAiGate(); alert(`AI 모의실행 완료: ${okCount}건 반영 (크레딧 차감 없음)`); } finally { hubAiMockBtn.disabled = false; hubAiMockBtn.textContent = prevLabel; } }); } const hubAddonBuyBtn = document.getElementById("hubAddonBuyBtn"); if(hubAddonBuyBtn){ hubAddonBuyBtn.addEventListener("click", () => { const pick = String(document.getElementById("hubAddonSelect")?.value || "ai_basic_pack"); const current = hubGetCurrentReqRow(); if(!current){ alert("추가 구매를 적용할 결제/승인 정보가 없습니다."); return; } const updated = hubUpsertCurrentReq((req) => { if(pick === "ai_basic_pack"){ req.optAIPack = true; req.optAI = true; req.optSource = !!req.optSource; req.optPack = "ai"; const prevLimit = Math.max(0, Number(req.aiQuotaLimit || 0) || 0); const nextLimit = prevLimit + 300; const used = Math.max(0, Number(req.aiCreditsUsed ?? req.aiQuotaUsed ?? 0) || 0); req.aiQuotaLimit = nextLimit; req.aiQuotaUsed = used; req.aiCreditsTotal = nextLimit; req.aiCreditsUsed = used; req.aiCreditsRemain = Math.max(0, nextLimit - used); } else if(pick === "docs2"){ req.optDocs50 = true; req.generalPack = req.generalPack && req.generalPack !== "none" ? req.generalPack : "docs2"; } req.updatedAt = new Date().toISOString(); return req; }); if(!updated){ alert("추가 구매 적용에 실패했습니다."); return; } alert( pick === "ai_basic_pack" ? "AI 기본 분석 패키지 추가 구매가 적용되었습니다. (+300건)" : "증빙서류 2차 첨부 옵션이 적용되었습니다." ); hubRefreshAiGate(); }); } const hubAiCreditAdjustBtn = document.getElementById("hubAiCreditAdjustBtn"); if(hubAiCreditAdjustBtn){ hubAiCreditAdjustBtn.addEventListener("click", () => { const pick = String(document.getElementById("hubAiCreditAdjust")?.value || "300"); const updated = hubUpsertCurrentReq((req) => { const used = Math.max(0, Number(req.aiCreditsUsed ?? req.aiQuotaUsed ?? 0) || 0); if(pick === "reset"){ req.optAIPack = false; req.optAI = false; req.aiQuotaLimit = 0; req.aiQuotaUsed = 0; req.aiCreditsTotal = 0; req.aiCreditsUsed = 0; req.aiCreditsRemain = 0; return req; } const delta = Number(pick || 0) || 0; const prevTotal = Math.max(0, Number(req.aiCreditsTotal ?? req.aiQuotaLimit ?? 0) || 0); const nextTotal = Math.max(0, prevTotal + delta); req.optAIPack = nextTotal > 0 ? true : !!req.optAIPack; req.optAI = nextTotal > 0 ? true : !!req.optAI; req.aiQuotaLimit = nextTotal; req.aiQuotaUsed = Math.min(used, nextTotal); req.aiCreditsTotal = nextTotal; req.aiCreditsUsed = Math.min(used, nextTotal); req.aiCreditsRemain = Math.max(0, nextTotal - Math.min(used, nextTotal)); return req; }); if(!updated){ alert("AI 토큰 조정에 실패했습니다."); return; } hubAppendAudit({ kind: "AI 토큰 조정", email: "-", name: "-", detail: pick === "reset" ? "AI 0건 초기화" : `AI 토큰 ${Number(pick) > 0 ? "+" : ""}${pick}건` }); hubRefreshAiGate(); hubRenderAudit(); alert(pick === "reset" ? "AI 토큰을 0건으로 초기화했습니다." : `AI 토큰을 ${pick}건 조정했습니다.`); }); } hubRefreshAiGate(); // ---------------------- // chat hub (inline, no iframe) // ---------------------- let hubChatRooms = []; let hubActiveRoomId = null; function hubLoadRooms(){ if(!window.FRONT || typeof FRONT.roomsLoad !== "function") return []; const rows = FRONT.roomsLoad(); return Array.isArray(rows) ? rows : []; } function hubLoadMsgs(){ if(!window.FRONT || typeof FRONT.msgsLoad !== "function") return []; const rows = FRONT.msgsLoad(); return Array.isArray(rows) ? rows : []; } function hubRenderChatRooms(){ hubChatRooms = hubLoadRooms(); if(!hubActiveRoomId && hubChatRooms.length) hubActiveRoomId = hubChatRooms[0].id; const el = document.getElementById("hubChatRooms"); if(!el) return; if(!hubChatRooms.length){ el.innerHTML = '
채팅방이 없습니다.
'; return; } el.innerHTML = hubChatRooms.map((r) => ` `).join(""); } function hubRenderChatMsgs(){ const room = hubChatRooms.find((r) => r.id === hubActiveRoomId); const titleEl = document.getElementById("hubChatRoomTitle"); if(titleEl){ titleEl.textContent = room ? `${room.applicantName || "-"} (${room.applicantEmail || "-"})` : "방 선택"; } const wrap = document.getElementById("hubChatMsgs"); if(!wrap) return; const msgs = hubLoadMsgs().filter((m) => m.roomId === hubActiveRoomId); if(!msgs.length){ wrap.innerHTML = '
메시지가 없습니다.
'; return; } wrap.innerHTML = msgs.map((m) => `
${hubEsc(m.senderName || "-")} · ${fmtDate(m.createdAt || new Date().toISOString())}
${hubEsc(m.text || "")}
`).join(""); wrap.scrollTop = wrap.scrollHeight; } const hubChatRoomsEl = document.getElementById("hubChatRooms"); if(hubChatRoomsEl){ hubChatRoomsEl.addEventListener("click", (e) => { const btn = e.target.closest(".hubChatRoomBtn"); if(!btn) return; hubActiveRoomId = btn.getAttribute("data-id") || null; hubRenderChatRooms(); hubRenderChatMsgs(); }); } const hubChatSendBtn = document.getElementById("hubChatSendBtn"); if(hubChatSendBtn){ hubChatSendBtn.addEventListener("click", () => { const input = document.getElementById("hubChatInput"); const text = (input?.value || "").trim(); if(!text || !hubActiveRoomId || !window.FRONT || typeof FRONT.postMsg !== "function") return; const adminName = (window.FRONT && FRONT.authGet()?.name) || "관리자"; FRONT.postMsg(hubActiveRoomId, "admin", adminName, text); if(input) input.value = ""; hubRenderChatRooms(); hubRenderChatMsgs(); }); } function select(id){ selectedId = id; const x = listAll.find(v => v.id === id); elStatusSel.value = x?.status || "접수"; renderDetail(x); } elStatusSel.addEventListener("change", () => { if(!selectedId) return; const idx = listAll.findIndex(v => v.id === selectedId); if(idx < 0) return; listAll[idx].status = elStatusSel.value; save(listAll); renderList(); calcKpis(); renderDetail(listAll[idx]); }); ["filterPos","filterStatus","search"].forEach(id=>{ document.getElementById(id).addEventListener("input", () => renderList()); document.getElementById(id).addEventListener("change", () => renderList()); }); document.getElementById("resetBtn").addEventListener("click", () => { if(confirm("데모 데이터를 초기화할까요?")){ localStorage.removeItem(KEY); listAll = []; selectedId = null; renderList(); calcKpis(); renderDetail(null); alert("초기화 완료"); } }); // menu tabs + settings dropdown const settingsToggle = document.getElementById("settingsToggle"); const settingsMenu = document.getElementById("settingsMenu"); function openSettingsMenu(open){ if(!settingsMenu || !settingsToggle) return; settingsMenu.style.display = open ? "block" : "none"; settingsToggle.classList.toggle("active", open); } function activateMenu(target){ const sectionEl = document.getElementById(target); if(!sectionEl) return; document.querySelectorAll(".navItem, .navSubItem").forEach(b => b.classList.remove("active")); const activeBtn = document.querySelector(`[data-target="${target}"]`); if(activeBtn) activeBtn.classList.add("active"); document.querySelectorAll(".tabSection").forEach(sec => { sec.classList.toggle("active", sec.id === target); }); const p = new URLSearchParams(location.search); p.set("view", target); history.replaceState(null, "", `${location.pathname}?${p.toString()}`); const viewSelect = document.getElementById("adminViewSelect"); if(viewSelect && viewSelect.value !== target) viewSelect.value = target; if(target === "tab-chat-hub"){ hubRenderChatRooms(); hubRenderChatMsgs(); } openSettingsMenu(target.startsWith("tab-settings-")); refreshSideQuickActions(target); if(typeof hubRefreshAiGate === "function") hubRefreshAiGate(); } if(settingsToggle){ settingsToggle.addEventListener("click", () => { const isOpen = settingsMenu && settingsMenu.style.display === "block"; openSettingsMenu(!isOpen); }); } document.querySelectorAll("[data-target]").forEach(btn => { btn.addEventListener("click", () => activateMenu(btn.dataset.target)); }); const adminViewSelect = document.getElementById("adminViewSelect"); if(adminViewSelect){ adminViewSelect.addEventListener("change", () => activateMenu(adminViewSelect.value)); } const hubQ = document.getElementById("hubQ"); const hubStatus = document.getElementById("hubStatus"); const hubSort = document.getElementById("hubSort"); const hubDensity = document.getElementById("hubDensity"); const hubSelectedOnly = document.getElementById("hubSelectedOnly"); const hubQuickFilters = document.getElementById("hubQuickFilters"); if(hubQ){ hubQ.addEventListener("input", () => { hubCurrentPage = 1; hubRenderApplications(); hubRenderAudit(); }); } if(hubStatus){ hubStatus.addEventListener("change", () => { hubCurrentPage = 1; hubRenderApplications(); document.querySelectorAll("#hubQuickFilters .filterChip").forEach((btn) => { btn.classList.toggle("active", btn.getAttribute("data-status") === hubStatus.value); }); }); } if(hubSort){ hubSort.addEventListener("change", () => { hubCurrentPage = 1; hubRenderApplications(); }); } if(hubDensity){ hubDensity.addEventListener("change", () => hubRenderApplications()); } if(hubSelectedOnly){ hubSelectedOnly.addEventListener("change", () => { hubCurrentPage = 1; hubRenderApplications(); }); } if(hubQuickFilters){ hubQuickFilters.addEventListener("click", (e) => { const btn = e.target.closest(".filterChip"); if(!btn || !hubStatus) return; const next = btn.getAttribute("data-status") || "ALL"; hubStatus.value = next; document.querySelectorAll("#hubQuickFilters .filterChip").forEach((chip) => { chip.classList.toggle("active", chip === btn); }); hubCurrentPage = 1; hubRenderApplications(); }); } const hubPrevPageBtn = document.getElementById("hubPrevPageBtn"); if(hubPrevPageBtn){ hubPrevPageBtn.addEventListener("click", () => { if(hubCurrentPage <= 1) return; hubCurrentPage -= 1; hubRenderApplications(); document.getElementById("tab-applications-hub")?.scrollIntoView({ behavior: "smooth", block: "start" }); }); } const hubNextPageBtn = document.getElementById("hubNextPageBtn"); if(hubNextPageBtn){ hubNextPageBtn.addEventListener("click", () => { hubCurrentPage += 1; hubRenderApplications(); document.getElementById("tab-applications-hub")?.scrollIntoView({ behavior: "smooth", block: "start" }); }); } const aiPreviewBtn = document.getElementById("aiPreviewBtn"); if(aiPreviewBtn){ aiPreviewBtn.addEventListener("click", ()=>{ alert("AI 분석 기능은 고급 옵션입니다. 현재는 데모 미리보기만 제공합니다."); }); } const mapAddressSearchBtn = document.getElementById("mapAddressSearchBtn"); const runMapLatLngSearch = async (q) => { const msgEl = document.getElementById("mapAddressSearchMsg"); const titleInput = document.getElementById("mapAddressTitleInput"); const lineInput = document.getElementById("mapAddressLineInput"); const latInput = document.getElementById("mapLatInput"); const lngInput = document.getElementById("mapLngInput"); if(!q){ if(msgEl) msgEl.textContent = "검색어를 입력해 주세요."; return; } if(titleInput) titleInput.value = q; if(lineInput) lineInput.value = q; if(latInput) latInput.value = ""; if(lngInput) lngInput.value = ""; if(mapAddressSearchBtn) mapAddressSearchBtn.disabled = true; if(msgEl) msgEl.textContent = "주소 검색 중..."; try{ const url = `https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=kr&q=${encodeURIComponent(q)}`; const res = await fetch(url, { headers: { "Accept": "application/json" } }); const rows = await res.json().catch(()=>[]); const first = Array.isArray(rows) ? rows[0] : null; if(!first){ if(msgEl) msgEl.textContent = "좌표 검색 결과가 없어 주소 텍스트 기준으로 저장합니다."; return; } const lat = Number(first.lat); const lng = Number(first.lon); if(Number.isFinite(lat) && latInput) latInput.value = String(lat); if(Number.isFinite(lng) && lngInput) lngInput.value = String(lng); if(lineInput) lineInput.value = String(first.display_name || q); if(msgEl){ msgEl.textContent = Number.isFinite(lat) && Number.isFinite(lng) ? `검색 완료: lat ${lat.toFixed(6)}, lng ${lng.toFixed(6)}` : "주소 검색 완료"; } }catch(_e){ if(msgEl) msgEl.textContent = "좌표 검색은 실패했지만 주소 텍스트 기준으로 저장할 수 있습니다."; }finally{ if(mapAddressSearchBtn) mapAddressSearchBtn.disabled = false; } }; const runMapAddressSearch = async () => { const q = String((document.getElementById("mapAddressSearchInput") || {}).value || "").trim(); const msgEl = document.getElementById("mapAddressSearchMsg"); const titleInput = document.getElementById("mapAddressTitleInput"); const lineInput = document.getElementById("mapAddressLineInput"); if(!q){ if(msgEl) msgEl.textContent = "검색어를 입력해 주세요."; return; } if(window.daum && window.daum.Postcode){ if(msgEl) msgEl.textContent = "주소 검색창을 여는 중..."; new window.daum.Postcode({ oncomplete: async function(data){ const road = data && data.roadAddress ? data.roadAddress : ""; const jibun = data && data.jibunAddress ? data.jibunAddress : ""; const zone = data && data.zonecode ? `(${data.zonecode}) ` : ""; const selectedAddress = `${zone}${road || jibun || q}`.trim(); if(titleInput) titleInput.value = q; if(lineInput) lineInput.value = selectedAddress; const searchInput = document.getElementById("mapAddressSearchInput"); if(searchInput) searchInput.value = selectedAddress; await runMapLatLngSearch(selectedAddress); } }).open(); return; } await runMapLatLngSearch(q); }; if(mapAddressSearchBtn){ mapAddressSearchBtn.addEventListener("click", runMapAddressSearch); } const mapAddressSearchInput = document.getElementById("mapAddressSearchInput"); if(mapAddressSearchInput){ mapAddressSearchInput.addEventListener("keydown", (e) => { if(e.key !== "Enter") return; e.preventDefault(); runMapAddressSearch(); }); } function lockReadonlyUI(){ if(!isReadonly) return; document.querySelectorAll("input, textarea, select, button").forEach(el=>{ el.disabled = true; }); // keep nav buttons enabled for viewing tabs document.querySelectorAll(".navItem").forEach(el=>{ el.disabled = false; }); document.querySelectorAll(".navSubItem").forEach(el=>{ el.disabled = false; }); const viewSelect = document.getElementById("adminViewSelect"); if(viewSelect) viewSelect.disabled = false; const previewBtn = document.getElementById("previewToggleBtn"); if(previewBtn) previewBtn.disabled = false; const aiBtn = document.getElementById("aiPreviewBtn"); if(aiBtn) aiBtn.disabled = false; } // init renderList(); calcKpis(); hubRenderApplications(); hubRenderAudit(); hubRenderChatRooms(); hubRenderChatMsgs(); refreshSideQuickActions(); const initialView = getQS("view"); if(initialView){ activateMenu(initialView); } lockReadonlyUI();