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}`; } 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 `