FMOS | FINEMEAL Marketing Operating System

통합 광고 운영 센터

대시보드: 성과 요약과 가중 ROAS 모델을 확인합니다.
보안: 미인증
운영 가이드
성과 기준: ROAS / CPA
동기화 정책
매 15분 자동 수집 + 규칙 실행
운영 모드
한국형 KRW / KST 기준
제어 범위
상태/예산 일괄 + 자동복구
준비되었습니다.

성과 모델(가중)

직접 전환가치를 기준으로 간접/UTM/총합 ROAS를 계산합니다.
간접 130% / UTM 80% / 중복차감 20%

매체 연동 상태

연동 현황을 불러오는 중...

동기화 작업 현황

최근 작업 로딩 중...
생성시각 매체 상태 시작시각 종료시각 로그/오류

어트리뷰션 링크

링크와 어트리뷰션 이벤트를 불러오는 중...
미리보기 0건
No 코드 링크명 랜딩URL utm_campaign
미리보기 데이터가 없습니다.
선택 링크: 없음
선택 링크 기준 설치 코드를 생성합니다.
생성시각 매체 링크명 코드 상태 클릭수 트래킹URL 제어

자동 제어 규칙

규칙 정보를 불러오는 중...
선택 규칙: 없음
규칙ID 규칙명 대상매체 조건 액션 활성 최종실행 제어

기간별 지표

성과 추세

통합 광고 제어

선택 0건
매체 광고 계정 캠페인 상태 일예산 노출 클릭 광고비 ROAS(직접) 간접ROAS(가중) ROAS(UTM가중) 총합ROAS(가중) 제어
원본 JSON 보기

          

광고 소재 뷰

`, "", "", "" ].join("\\n"); } function renderAttributionSnippet() { const row = selectedAttributionRow(); const snippet = buildAttributionSnippet(); if (!row) { $("attrSnippetHint").textContent = "링크를 1개 이상 만든 뒤 설치 코드를 생성하세요."; $("attrSnippetOutput").value = ""; return; } $("attrSnippetHint").textContent = `선택 코드 ${row.code} 기준 스니펫`; $("attrSnippetOutput").value = snippet || ""; } function ruleConditionText(condition) { return `${condition.metric} ${condition.operator} ${condition.value}`; } function ruleActionText(action) { if (action.type === "set_status") { return `set_status -> ${action.status}`; } return `set_daily_budget(${action.mode}) -> ${action.value}`; } function renderRules() { const rows = state.rules?.rows || []; const summary = state.rules?.summary || { total: 0, enabled: 0, disabled: 0 }; $("ruleSummaryText").textContent = `전체 ${summary.total} / 활성 ${summary.enabled} / 비활성 ${summary.disabled}`; $("selectedRuleIdText").textContent = `선택 규칙: ${state.selectedRuleId || "없음"}`; $("ruleTableBody").innerHTML = rows.length ? rows .map((r) => { const selected = state.selectedRuleId === r.id ? "background:#eaf2ff;" : ""; return ` ${r.id} ${r.name} ${r.platform} ${ruleConditionText(r.condition)} ${ruleActionText(r.action)} ${r.isEnabled ? "enabled" : "disabled"} ${r.lastEvaluatedAt || "-"} `; }) .join("") : `등록된 자동 규칙이 없습니다.`; } function applyRuleTemplate() { $("ruleName").value = "ROAS 150 미만 자동 중지"; $("rulePlatform").value = "all"; $("ruleMetric").value = "roas"; $("ruleOperator").value = "<"; $("ruleValue").value = "150"; $("ruleActionType").value = "set_status"; $("ruleActionStatus").value = "paused"; $("ruleActionMode").value = "decrease_pct"; $("ruleActionValue").value = "10"; $("ruleEnabled").value = "true"; } function applyRuleRecoveryTemplate() { $("ruleName").value = "ROAS 180 이상 자동 복구"; $("rulePlatform").value = "all"; $("ruleMetric").value = "roas"; $("ruleOperator").value = ">="; $("ruleValue").value = "180"; $("ruleActionType").value = "set_status"; $("ruleActionStatus").value = "active"; $("ruleActionMode").value = "increase_pct"; $("ruleActionValue").value = "10"; $("ruleEnabled").value = "true"; } function fillRuleForm(rule) { state.selectedRuleId = rule.id; $("ruleName").value = rule.name; $("rulePlatform").value = rule.platform; $("ruleMetric").value = rule.condition.metric; $("ruleOperator").value = rule.condition.operator; $("ruleValue").value = String(rule.condition.value); $("ruleActionType").value = rule.action.type; if (rule.action.type === "set_status") { $("ruleActionStatus").value = rule.action.status; } else { $("ruleActionMode").value = rule.action.mode; $("ruleActionValue").value = String(rule.action.value); } $("ruleEnabled").value = rule.isEnabled ? "true" : "false"; renderRules(); } function buildRulePayload() { const actionType = $("ruleActionType").value; const base = { id: state.selectedRuleId || undefined, orgId: $("orgId").value.trim(), name: $("ruleName").value.trim(), platform: $("rulePlatform").value, isEnabled: $("ruleEnabled").value === "true", condition: { metric: $("ruleMetric").value, operator: $("ruleOperator").value, value: Number($("ruleValue").value) } }; if (actionType === "set_status") { return { ...base, action: { type: "set_status", status: $("ruleActionStatus").value } }; } return { ...base, action: { type: "set_daily_budget", mode: $("ruleActionMode").value, value: Number($("ruleActionValue").value) } }; } function fillIntegrationEditor(platform, overrideRow = null) { const meta = PLATFORM_META[platform]; const row = overrideRow || integrationRowMap()[platform] || null; $("integrationPlatform").value = platform; $("integrationName").value = row?.integrationName || meta.defaultName; $("integrationAuth").value = row?.authType || meta.authType; $("integrationState").value = row?.status || "connected"; const obj = {}; const keys = row?.credentialKeys || Object.keys(meta.template); for (const k of keys) { obj[k] = meta.template[k] || `demo-${platform}-${k}`; } if (Object.keys(obj).length === 0) { Object.assign(obj, meta.template); } $("integrationCredentials").value = JSON.stringify(obj, null, 2); } function applyTemplate() { fillIntegrationEditor($("integrationPlatform").value || "meta"); } async function loadDashboard() { saveSettings(); if (!state.authKey) { setStatus("접속키 인증 후 데이터를 조회할 수 있습니다.", true); openAuthModal("접속키 인증 후 조회를 진행해주세요."); return; } const orgId = $("orgId").value.trim(); if (!orgId) { setStatus("조직 ID를 입력해주세요.", true); return; } setStatus("데이터를 조회 중입니다..."); const from = $("fromDate").value; const to = $("toDate").value; const platform = $("platformFilter").value; const tasks = await Promise.allSettled([ fetchJson("/v1/control/overview", { params: { orgId, from, to, platform } }), fetchJson("/v1/kpi/daily", { params: { orgId, from, to, platform, scope: "account" } }), fetchJson("/v1/integrations", { params: { orgId, platform } }), fetchJson("/v1/sync/jobs", { params: { orgId, platform, limit: 20 } }), fetchJson("/v1/attribution/links", { params: { orgId, platform, limit: 50 } }), fetchJson("/v1/attribution/summary", { params: { orgId, from, to, platform } }), fetchJson("/v1/automation/rules", { params: { orgId, platform } }) ]); const errors = []; if (tasks[0].status === "fulfilled") state.overview = tasks[0].value; else errors.push(`overview: ${tasks[0].reason.message}`); if (tasks[1].status === "fulfilled") state.daily = tasks[1].value; else errors.push(`kpi: ${tasks[1].reason.message}`); if (tasks[2].status === "fulfilled") state.integrations = tasks[2].value; else errors.push(`integrations: ${tasks[2].reason.message}`); if (tasks[3].status === "fulfilled") state.jobs = tasks[3].value; else errors.push(`jobs: ${tasks[3].reason.message}`); if (tasks[4].status === "fulfilled") state.attributionLinks = tasks[4].value; else errors.push(`attribution.links: ${tasks[4].reason.message}`); if (tasks[5].status === "fulfilled") state.attributionSummary = tasks[5].value; else errors.push(`attribution.summary: ${tasks[5].reason.message}`); if (tasks[6].status === "fulfilled") state.rules = tasks[6].value; else errors.push(`rules: ${tasks[6].reason.message}`); renderKpis(); renderTrend(); renderIntegrationCards(); renderJobs(); renderAttribution(); renderRules(); renderCampaignTable(); const campaignCount = state.overview?.rows?.length || 0; const integrationCount = state.integrations?.rows?.length || 0; const jobCount = state.jobs?.rows?.length || 0; const attrCount = state.attributionLinks?.rows?.length || 0; const ruleCount = state.rules?.rows?.length || 0; if (errors.length) { setStatus(`부분 조회 실패: ${errors.join(" / ")}`, true); } else { setStatus(`조회 완료: 캠페인 ${campaignCount}건 / 연동 ${integrationCount}건 / 동기화작업 ${jobCount}건 / 어트리뷰션링크 ${attrCount}건 / 규칙 ${ruleCount}건`); } } async function updateCampaign(campaignId, status, dailyBudget) { const orgId = $("orgId").value.trim(); const payload = { orgId, campaignId }; if (status) payload.status = status; if (typeof dailyBudget === "number" && !Number.isNaN(dailyBudget)) { payload.dailyBudget = Math.max(0, Math.round(dailyBudget)); } return fetchJson("/v1/control/campaign/update", { method: "POST", body: payload }); } async function runSync() { if (state.syncing) { setStatus("동기화가 이미 실행 중입니다. 잠시 후 다시 시도해주세요.", true); return; } const orgId = $("orgId").value.trim(); const rows = state.overview?.rows || []; if (!rows.length) { setStatus("동기화할 캠페인이 없습니다.", true); return; } const map = new Map(); for (const r of rows) { const key = `${r.platform}::${r.adAccountId}`; if (!map.has(key)) { map.set(key, { platform: r.platform, adAccountId: r.adAccountId }); } } const targets = [...map.values()]; setStatus(`동기화 요청 중... (${targets.length}개 계정)`); state.syncing = true; $("syncBtn").disabled = true; let success = 0; let skipped = 0; let fail = 0; try { for (const t of targets) { try { const res = await fetchJson("/v1/sync/enqueue", { method: "POST", body: { orgId, platform: t.platform, adAccountId: t.adAccountId, fromDate: $("fromDate").value, toDate: $("toDate").value } }); if (res?.status === "skipped") { skipped += 1; } else { success += 1; } } catch (err) { fail += 1; } } setStatus(`동기화 요청 완료: 성공 ${success} / 건너뜀 ${skipped} / 실패 ${fail}`); await loadDashboard(); } finally { state.syncing = false; $("syncBtn").disabled = false; } } function updateSelectedCount() { $("selectedCount").textContent = `선택 ${state.selectedCampaignIds.size}건`; } function refreshSelectionFromDom() { const checks = document.querySelectorAll(".campaign-check"); state.selectedCampaignIds.clear(); checks.forEach((el) => { if (el.checked) state.selectedCampaignIds.add(el.dataset.id); }); updateSelectedCount(); } async function applyBulkStatus() { const status = $("bulkStatusSelect").value; if (!status) { setStatus("변경할 상태를 선택해주세요.", true); return; } const ids = [...state.selectedCampaignIds]; if (!ids.length) { setStatus("선택된 캠페인이 없습니다.", true); return; } setStatus(`선택 캠페인 상태 적용 중... (${ids.length}건)`); let ok = 0; let fail = 0; for (const id of ids) { try { await updateCampaign(id, status); ok += 1; } catch { fail += 1; } } setStatus(`상태 일괄 적용 완료: 성공 ${ok} / 실패 ${fail}`); await loadDashboard(); } async function applyBulkBudget() { const mode = $("bulkBudgetMode").value; const raw = Number($("bulkBudgetValue").value); if (!Number.isFinite(raw)) { setStatus("예산 값을 입력해주세요.", true); return; } const ids = [...state.selectedCampaignIds]; if (!ids.length) { setStatus("선택된 캠페인이 없습니다.", true); return; } setStatus(`선택 캠페인 예산 적용 중... (${ids.length}건)`); let ok = 0; let fail = 0; for (const id of ids) { const row = rowById(id); if (!row) { fail += 1; continue; } let nextBudget = Number(row.dailyBudget); if (mode === "set") nextBudget = raw; if (mode === "increase_pct") nextBudget = nextBudget * (1 + raw / 100); if (mode === "decrease_pct") nextBudget = nextBudget * (1 - raw / 100); try { await updateCampaign(id, null, Math.max(0, nextBudget)); ok += 1; } catch { fail += 1; } } setStatus(`예산 일괄 적용 완료: 성공 ${ok} / 실패 ${fail}`); await loadDashboard(); } async function saveAttributionLink() { const payload = buildAttributionPayload(); if (!payload.orgId || !payload.linkName || !payload.destinationUrl) { setStatus("orgId, 링크명, 랜딩 URL을 확인해주세요.", true); return; } setStatus(`어트리뷰션 링크 저장 중... (${payload.linkName})`); const res = await fetchJson("/v1/attribution/links/upsert", { method: "POST", body: payload }); if (res?.row?.id) { state.selectedAttributionId = res.row.id; $("selectedAttrIdText").textContent = `선택 링크: ${res.row.id}`; } setStatus("어트리뷰션 링크 저장 완료"); await loadDashboard(); } async function deleteAttributionLink(linkId) { const orgId = $("orgId").value.trim(); setStatus(`어트리뷰션 링크 삭제 중... (${linkId})`); await fetchJson("/v1/attribution/links/delete", { method: "POST", body: { id: linkId, orgId } }); if (state.selectedAttributionId === linkId) { state.selectedAttributionId = null; $("selectedAttrIdText").textContent = "선택 링크: 없음"; } setStatus("어트리뷰션 링크 삭제 완료"); await loadDashboard(); } async function trackAttributionPurchase() { const orgId = $("orgId").value.trim(); if (!state.selectedAttributionId) { setStatus("구매 이벤트를 보낼 링크를 먼저 선택해주세요.", true); return; } const row = (state.attributionLinks?.rows || []).find((r) => r.id === state.selectedAttributionId); if (!row) { setStatus("선택한 링크 정보를 찾지 못했습니다.", true); return; } const revenue = Number($("attrPurchaseValue").value || 0); setStatus(`구매 이벤트 전송 중... (${row.code})`); await fetchJson("/v1/attribution/events/track", { method: "POST", body: { orgId, code: row.code, eventType: "purchase", revenue, orderId: `order_${Date.now()}`, meta: { source: "dashboard_manual_test" } } }); setStatus(`구매 이벤트 전송 완료: ${row.code}, ${fmtKrw(revenue)}`); await loadDashboard(); } async function copyText(value) { try { await navigator.clipboard.writeText(value); setStatus("클립보드에 복사했습니다."); } catch { setStatus("복사 권한이 없어 수동 복사가 필요합니다.", true); } } async function saveIntegration() { const orgId = $("orgId").value.trim(); const platform = $("integrationPlatform").value; const integrationName = $("integrationName").value.trim(); const authType = $("integrationAuth").value; const status = $("integrationState").value; let credentials; try { credentials = JSON.parse($("integrationCredentials").value); } catch (err) { setStatus("자격정보(JSON) 형식이 올바르지 않습니다.", true); return; } setStatus(`${platform} 연동 저장 중...`); await fetchJson("/v1/integrations/upsert", { method: "POST", body: { orgId, platform, integrationName, authType, status, credentials } }); setStatus(`${platform} 연동 저장 완료`); await loadDashboard(); } async function testIntegration(platform = null, name = null) { const orgId = $("orgId").value.trim(); const p = platform || $("integrationPlatform").value; const n = name || $("integrationName").value.trim(); if (!n) { setStatus("연동명을 입력해주세요.", true); return; } setStatus(`${p} 연동 테스트 중...`); const result = await fetchJson("/v1/integrations/test", { method: "POST", body: { orgId, platform: p, integrationName: n } }); if (result.status === "ok") { setStatus(`${p} 연동 테스트 성공`); } else { setStatus(`${p} 연동 테스트 실패: ${result.error || "unknown"}`, true); } await loadDashboard(); } async function saveRule() { const payload = buildRulePayload(); if (!payload.orgId || !payload.name) { setStatus("orgId와 규칙명을 확인해주세요.", true); return; } setStatus(`규칙 저장 중... (${payload.name})`); const res = await fetchJson("/v1/automation/rules/upsert", { method: "POST", body: payload }); if (res?.rule?.id) { state.selectedRuleId = res.rule.id; } setStatus("규칙 저장 완료"); await loadDashboard(); } async function deleteRule(ruleId) { const orgId = $("orgId").value.trim(); setStatus(`규칙 삭제 중... (${ruleId})`); await fetchJson("/v1/automation/rules/delete", { method: "POST", body: { id: ruleId, orgId } }); if (state.selectedRuleId === ruleId) { state.selectedRuleId = null; } setStatus("규칙 삭제 완료"); await loadDashboard(); } async function runRule(dryRun) { const orgId = $("orgId").value.trim(); if (!state.selectedRuleId) { setStatus("실행할 규칙을 먼저 선택해주세요.", true); return; } const payload = { orgId, ruleId: state.selectedRuleId, fromDate: $("ruleFromDate").value || $("fromDate").value, toDate: $("ruleToDate").value || $("toDate").value, dryRun }; setStatus(`${dryRun ? "Dry Run" : "규칙 실행"} 중... (${state.selectedRuleId})`); const result = await fetchJson("/v1/automation/rules/run", { method: "POST", body: payload }); setStatus( `${dryRun ? "Dry Run" : "규칙 실행"} 완료: 매칭 ${result.matchedCount || 0} / 변경 ${result.updatedCount || 0}` ); await loadDashboard(); } document.addEventListener("click", async (ev) => { const target = ev.target; if (target.classList.contains("row-save")) { const id = target.dataset.id; const status = document.querySelector(`.row-status[data-id="${id}"]`).value; const budget = Number(document.querySelector(`.row-budget[data-id="${id}"]`).value); try { setStatus(`${id} 저장 중...`); await updateCampaign(id, status, budget); setStatus(`${id} 저장 완료`); await loadDashboard(); } catch (err) { setStatus(`저장 실패: ${err.message}`, true); } return; } if (target.classList.contains("integration-load")) { fillIntegrationEditor(target.dataset.platform); return; } if (target.classList.contains("integration-test-card")) { try { await testIntegration(target.dataset.platform, target.dataset.name); } catch (err) { setStatus(`연동 테스트 실패: ${err.message}`, true); } return; } if (target.classList.contains("attr-load")) { const row = (state.attributionLinks?.rows || []).find((r) => r.id === target.dataset.id); if (!row) { setStatus("링크 정보를 찾지 못했습니다.", true); return; } fillAttributionForm(row); setStatus(`링크 선택: ${row.linkName}`); return; } if (target.classList.contains("attr-copy")) { await copyText(target.dataset.url || ""); return; } if (target.classList.contains("attr-delete")) { try { await deleteAttributionLink(target.dataset.id); } catch (err) { setStatus(`링크 삭제 실패: ${err.message}`, true); } return; } if (target.classList.contains("rule-load")) { const ruleId = target.dataset.id; const rule = (state.rules?.rows || []).find((r) => r.id === ruleId); if (!rule) { setStatus("규칙 정보를 찾을 수 없습니다.", true); return; } fillRuleForm(rule); setStatus(`규칙 선택: ${rule.name}`); return; } if (target.classList.contains("rule-delete")) { const ruleId = target.dataset.id; try { await deleteRule(ruleId); } catch (err) { setStatus(`규칙 삭제 실패: ${err.message}`, true); } return; } }); document.addEventListener("change", (ev) => { const target = ev.target; if (target.classList.contains("campaign-check")) { if (target.checked) state.selectedCampaignIds.add(target.dataset.id); else state.selectedCampaignIds.delete(target.dataset.id); updateSelectedCount(); return; } }); $("campaignSelectAll").addEventListener("change", (ev) => { const checked = ev.target.checked; document.querySelectorAll(".campaign-check").forEach((el) => { el.checked = checked; if (checked) state.selectedCampaignIds.add(el.dataset.id); else state.selectedCampaignIds.delete(el.dataset.id); }); updateSelectedCount(); }); $("selectVisibleBtn").addEventListener("click", () => { document.querySelectorAll(".campaign-check").forEach((el) => { el.checked = true; state.selectedCampaignIds.add(el.dataset.id); }); $("campaignSelectAll").checked = true; updateSelectedCount(); }); $("clearSelectedBtn").addEventListener("click", () => { document.querySelectorAll(".campaign-check").forEach((el) => { el.checked = false; }); $("campaignSelectAll").checked = false; state.selectedCampaignIds.clear(); updateSelectedCount(); }); $("bulkApplyStatusBtn").addEventListener("click", async () => { try { refreshSelectionFromDom(); await applyBulkStatus(); } catch (err) { setStatus(`상태 일괄 적용 실패: ${err.message}`, true); } }); $("bulkApplyBudgetBtn").addEventListener("click", async () => { try { refreshSelectionFromDom(); await applyBulkBudget(); } catch (err) { setStatus(`예산 일괄 적용 실패: ${err.message}`, true); } }); $("modelApplyBtn").addEventListener("click", () => { applyModelFromDom(); saveSettings(); renderKpis(); renderTrend(); renderCampaignTable(); setStatus("성과 모델(가중)을 반영했습니다."); }); $("attrTemplateBtn").addEventListener("click", () => { state.selectedAttributionId = null; $("selectedAttrIdText").textContent = "선택 링크: 없음"; applyAttrTemplate(); renderAttributionSnippet(); setStatus("어트리뷰션 링크 템플릿을 채웠습니다."); }); $("attrBatchPreviewBtn").addEventListener("click", () => { try { state.attrBatchPreviewRows = buildAttrBatchRows(); renderAttrBatchPreview(); saveSettings(); setStatus(`미리보기 생성 완료: ${state.attrBatchPreviewRows.length}건`); } catch (err) { setStatus(`미리보기 생성 실패: ${err.message}`, true); } }); $("attrBatchRegisterBtn").addEventListener("click", async () => { try { await registerAttrBatchRows(); } catch (err) { setStatus(`일괄 등록 실패: ${err.message}`, true); } }); $("attrBatchClearBtn").addEventListener("click", () => { state.attrBatchPreviewRows = []; renderAttrBatchPreview(); setStatus("미리보기 목록을 비웠습니다."); }); $("attrPlatform").addEventListener("change", () => { if (!state.selectedAttributionId) { applyAttrTemplate(); } const platform = $("attrPlatform").value; if (!$("attrBatchGroup").value.trim()) { $("attrBatchGroup").value = `${platform}_group`; } saveSettings(); }); $("attrSaveBtn").addEventListener("click", async () => { try { await saveAttributionLink(); } catch (err) { setStatus(`어트리뷰션 링크 저장 실패: ${err.message}`, true); } }); $("attrRefreshBtn").addEventListener("click", async () => { try { await loadDashboard(); } catch (err) { setStatus(`어트리뷰션 링크 새로고침 실패: ${err.message}`, true); } }); $("attrTrackPurchaseBtn").addEventListener("click", async () => { try { await trackAttributionPurchase(); } catch (err) { setStatus(`구매 이벤트 전송 실패: ${err.message}`, true); } }); $("attrSnippetBuildBtn").addEventListener("click", () => { renderAttributionSnippet(); const hasSnippet = !!$("attrSnippetOutput").value.trim(); if (hasSnippet) { setStatus("설치 코드 생성을 완료했습니다."); } else { setStatus("스니펫을 만들 링크가 없습니다. 링크를 먼저 생성해주세요.", true); } }); $("attrSnippetCopyBtn").addEventListener("click", async () => { const text = $("attrSnippetOutput").value.trim(); if (!text) { setStatus("복사할 설치 코드가 없습니다. 먼저 생성해주세요.", true); return; } await copyText(text); }); $("attrPurchaseValue").addEventListener("change", () => { renderAttributionSnippet(); }); $("integrationTemplateBtn").addEventListener("click", () => { applyTemplate(); setStatus("연동 템플릿을 채웠습니다."); }); $("integrationPlatform").addEventListener("change", () => { applyTemplate(); }); $("integrationSaveBtn").addEventListener("click", async () => { try { await saveIntegration(); } catch (err) { setStatus(`연동 저장 실패: ${err.message}`, true); } }); $("integrationTestBtn").addEventListener("click", async () => { try { await testIntegration(); } catch (err) { setStatus(`연동 테스트 실패: ${err.message}`, true); } }); $("ruleTemplateBtn").addEventListener("click", () => { state.selectedRuleId = null; applyRuleTemplate(); renderRules(); setStatus("규칙 템플릿을 채웠습니다."); }); $("ruleRecoveryTemplateBtn").addEventListener("click", () => { state.selectedRuleId = null; applyRuleRecoveryTemplate(); renderRules(); setStatus("복구 규칙 템플릿을 채웠습니다."); }); $("ruleSaveBtn").addEventListener("click", async () => { try { await saveRule(); } catch (err) { setStatus(`규칙 저장 실패: ${err.message}`, true); } }); $("ruleReloadBtn").addEventListener("click", async () => { try { await loadDashboard(); } catch (err) { setStatus(`규칙 새로고침 실패: ${err.message}`, true); } }); $("ruleDryRunBtn").addEventListener("click", async () => { try { await runRule(true); } catch (err) { setStatus(`규칙 Dry Run 실패: ${err.message}`, true); } }); $("ruleRunBtn").addEventListener("click", async () => { try { await runRule(false); } catch (err) { setStatus(`규칙 실행 실패: ${err.message}`, true); } }); $("refreshBtn").addEventListener("click", async () => { try { await loadDashboard(); } catch (err) { setStatus(`조회 실패: ${err.message}`, true); } }); $("syncBtn").addEventListener("click", async () => { try { await runSync(); } catch (err) { setStatus(`동기화 실패: ${err.message}`, true); } }); ["apiBase", "orgId", "fromDate", "toDate", "platformFilter"].forEach((id) => { $(id).addEventListener("change", () => { saveSettings(); if (id === "apiBase" || id === "orgId") { renderAttributionSnippet(); } }); }); ["attrHostSelect", "attrBatchLandingUrl", "attrBatchGroup", "attrBatchCount", "attrBatchCodePrefix"].forEach((id) => { $(id).addEventListener("change", saveSettings); }); ["modelIndirectPct", "modelUtmPct", "modelOverlapPct"].forEach((id) => { $(id).addEventListener("change", () => { applyModelFromDom(); saveSettings(); }); }); document.querySelectorAll(".rail-page-btn").forEach((btn) => { btn.addEventListener("click", () => { setPage(btn.dataset.page); }); }); $("authToggleBtn").addEventListener("click", async () => { if (state.authKey) { clearAuthState(); setStatus("로그아웃되었습니다. 다시 로그인해주세요."); openAuthModal("로그아웃되었습니다. 접속키를 다시 입력해주세요."); return; } openAuthModal(); }); $("authLoginBtn").addEventListener("click", async () => { try { await loginWithKey(); setStatus("접속키 인증이 완료되었습니다."); await loadDashboard(); } catch (err) { setStatus(`로그인 실패: ${err.message}`, true); $("authHelpText").textContent = `로그인 실패: ${err.message}`; } }); $("authClearBtn").addEventListener("click", () => { clearAuthState(); $("authKeyInput").value = ""; $("authHelpText").textContent = "저장된 접속키를 삭제했습니다."; setStatus("저장된 접속키를 삭제했습니다."); }); $("authCloseBtn").addEventListener("click", () => { closeAuthModal(); }); $("authModal").addEventListener("click", (ev) => { if (ev.target && ev.target.id === "authModal") { closeAuthModal(); } }); $("authKeyInput").addEventListener("keydown", async (ev) => { if (ev.key !== "Enter") return; ev.preventDefault(); $("authLoginBtn").click(); }); document.addEventListener("keydown", (ev) => { if (ev.key === "Escape" && !$("authModal").hidden) { closeAuthModal(); } }); window.addEventListener("popstate", () => { const urlPage = pageFromUrl() || "dashboard"; setPage(urlPage, { syncUrl: false, persist: true }); }); async function boot() { loadSettings(); if (!state.authKey) { openAuthModal("접속키를 먼저 입력해주세요."); setStatus("접속키 인증 대기 중입니다."); return; } try { const verified = await verifyAuthKey(state.authKey); setAuthState(verified); await loadDashboard(); } catch (err) { clearAuthState(); openAuthModal(`기존 접속키 검증 실패: ${err.message}`); setStatus(`초기 인증 실패: ${err.message}`, true); } } boot().catch((err) => { setStatus(`초기 로딩 실패: ${err.message}`, true); });