const CHANNEL_SPECS = [ { key: 'ddona_individual', label: '떠나요 개별', shortLabel: '개별', abbr: '개', fields: [ { name: 'ddona_individual_room_type_id', label: 'roomTypeId' }, { name: 'ddona_individual_room_id', label: 'roomId' }, ], requireNumeric: true, }, { key: 'ddona_integrated', label: '떠나요 통합', shortLabel: '통합', abbr: '떠', fields: [ { name: 'ddona_integrated_room_type_id', label: 'roomTypeId' }, { name: 'ddona_integrated_room_id', label: 'roomId' }, ], requireNumeric: true, }, { key: 'yeogi_integrated', label: '여기어때', shortLabel: '여기어때', abbr: '여', fields: [{ name: 'yeogi_room_id', label: 'roomId' }], }, { key: 'naver_integrated', label: '네이버', shortLabel: '네이버', abbr: 'N', fields: [{ name: 'naver_room_id', label: 'roomId' }], }, { key: 'yanolja_integrated', label: '야놀자', shortLabel: '야놀자', abbr: '야', fields: [{ name: 'yanolja_room_type_id', label: 'roomTypeId' }], }, { key: 'onda_integrated', label: '온다', shortLabel: '온다', abbr: '온', fields: [ { name: 'onda_rateplan_id', label: 'rateplanId' }, { name: 'onda_roomtype_id', label: 'roomtypeId' }, ], requireNumeric: true, }, ]; function channelIconHtml(spec, options = {}) { const { size = 'md', label = false, vertical = false } = options; const abbr = spec.abbr || spec.label.slice(0, 1); const iconCls = `ch-icon ch-icon--${spec.key} ch-icon-${size}`; const icon = ``; if (!label) return icon; const wrapCls = `ch-icon-wrap${vertical ? ' ch-icon-wrap--col' : ''}`; const text = spec.shortLabel || spec.label; return `${icon}${text}`; } function isFilled(value) { return Boolean(value && String(value).trim()); } function isNumericId(value) { return Boolean(value && /^\d+$/.test(String(value).trim())); } function evaluateChannel(room, spec) { const fieldNames = spec.fields.map((f) => f.name); const values = fieldNames.map((f) => room[f]); const filled = values.map(isFilled); const detail = values.filter(isFilled).join(' / ') || '—'; if (filled.every(Boolean)) { if (spec.requireNumeric && values.some((v) => isFilled(v) && !isNumericId(v))) { return { status: 'partial', detail }; } return { status: 'ok', detail }; } if (filled.some(Boolean)) { return { status: 'partial', detail }; } return { status: 'missing', detail: '—' }; } function roomIsFullyMapped(room) { return CHANNEL_SPECS.every((spec) => evaluateChannel(room, spec).status === 'ok'); } function renderChannelSummaryCards(rooms, containerId, options = {}) { const el = document.getElementById(containerId); if (!el) return; const { activeChannelKey = null, onCardClick = null } = options; const total = rooms.length; if (!total) { el.innerHTML = '

객실 없음 — discover를 먼저 실행하세요.

'; return; } el.innerHTML = CHANNEL_SPECS.map((spec) => { let ok = 0; let partial = 0; rooms.forEach((room) => { const st = evaluateChannel(room, spec).status; if (st === 'ok') ok += 1; else if (st === 'partial') partial += 1; }); const pct = Math.round((ok / total) * 100); const incomplete = total - ok; const cardState = pct === 100 ? 'ok' : (ok + partial > 0 ? 'partial' : 'missing'); const isActive = activeChannelKey === spec.key; const isFilterable = Boolean(onCardClick && incomplete > 0); const incompleteHint = incomplete > 0 ? ` · 미완료 ${incomplete}실` : ''; const clickHint = isFilterable ? ' · 클릭 시 미완료만 표시' : ''; const ariaLabel = `${spec.label} 매핑 ${pct}% (${ok}/${total})${incompleteHint}${clickHint}`; const activeCls = isActive ? ' match-card--active' : ''; const interactiveAttrs = isFilterable ? ` role="button" tabindex="0" data-channel-key="${spec.key}"` : ''; const ringColor = cardState === 'ok' ? 'var(--obs-success)' : (cardState === 'partial' ? 'var(--obs-warning)' : '#888'); const shortName = spec.shortLabel || spec.label; return `
${channelIconHtml(spec, { size: 'sm', label: false })} ${shortName} ${ok}/${total}
`; }).join(''); if (!onCardClick) return; el.querySelectorAll('.match-card[data-channel-key]').forEach((card) => { const key = card.dataset.channelKey; const activate = () => onCardClick(key, card); card.addEventListener('click', activate); card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(); } }); }); } function renderMatchLegend(containerId, options = {}) { const el = document.getElementById(containerId); if (!el) return; const { showFilterHint = false, filterIncompleteOnly = false, filterHiddenOnly = false, } = options; const statusItems = [ { cls: 'ok', label: '완료' }, { cls: 'partial', label: '일부' }, { cls: 'missing', label: '미매핑' }, ].map((item) => ` ${item.label} `).join(''); const hint = showFilterHint ? '카드 클릭 · 채널별 미완료' : ''; const filters = `
`; el.innerHTML = `
${hint}
${statusItems}
${filters}
`; } function statusLabel(status) { if (status === 'ok') return '완료'; if (status === 'partial') return '일부'; return '미매핑'; } function escapeHtml(s) { return String(s ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function matchCellTooltip(spec, ev) { const channel = spec?.label || ''; if (ev.status === 'missing') { return channel ? `${channel}: 미매핑 — 클릭하여 입력` : '미매핑 — 클릭하여 입력'; } const detail = ev.detail && ev.detail !== '—' ? ev.detail : ''; return [channel, statusLabel(ev.status), detail].filter(Boolean).join(' · '); } function truncateMatchDetail(detail, maxLen = 10) { if (!detail || detail === '—') return ''; const first = detail.split(' / ')[0]; if (first.length <= maxLen) return first; return `${first.slice(0, maxLen)}…`; } function matchCellHtml(ev, spec) { const tip = escapeHtml(matchCellTooltip(spec, ev)); const aria = escapeHtml(matchCellTooltip(spec, ev)); if (ev.status === 'ok') { return ` `; } if (ev.status === 'partial') { const hint = truncateMatchDetail(ev.detail); return ` ${hint ? `${escapeHtml(hint)}` : '일부'} `; } return ` `; }