/** * Exponential Backoff を使用して関数を実行する * * @param {Function} fn - 実行する非同期関数 * @param {number} maxRetries - 最大試行回数(デフォルト: 5) * @param {number} initialDelay - 初期遅延時間(ミリ秒、デフォルト: 500) * @param {number} maxDelay - 最大遅延時間(ミリ秒、デフォルト: 5000) * @param {number} multiplier - バックオフ乗数(デフォルト: 2) * @returns {Promise} - 関数の実行結果 */ async function exponentialBackoff( fn, maxRetries = 3, initialDelay = 500, maxDelay = 5000, multiplier = 2 ) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await fn(); return result; } catch (error) { lastError = error; // 最後の試行の場合はループを終了 if (attempt === maxRetries) { break; } // バックオフ時間を計算 (ジッターを追加) して待機 const baseDelay = Math.min(initialDelay * Math.pow(multiplier, attempt), maxDelay); const jitter = Math.random() * baseDelay * 0.1; // 0-10%のランダムなジッター const delay = baseDelay + jitter; await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } /** * カードラインナップを取得する * * @param {string[]} filters.categories - Categories to filter by * @param {string[]} filters.conditions - Conditions to filter by * @param {string} orders - Sort order * @param {number} limit - Limit of the results * @param {number} offset - Offset of the results * @returns {Promise} - Card lineup objects */ async function fetchCardLineup(filters, orders, limit = 100, offset = 0) { const params = new URLSearchParams(); const filterParams = []; if (filters.categories) { params.append('filters', filters.categories.map((v) => `categories[contains]${v}`).join('[and]')); } if (filters.conditions) { params.append('filters', filters.conditions.map((v) => `conditions[contains]${v}`).join('[and]')); } if (filterParams.length > 0) { params.append('filters', filterParams.join('[and]')); } params.append('orders', orders); params.append('offset', offset); params.append('limit', limit); const response = await exponentialBackoff(() => fetch(`/microcms/api/v1/card-lineup?${params.toString()}`)); const data = await response.json(); return data; } /** * カードラインナップを全件取得する * カードラインナップを取得して、ジェネレーターで返します。呼び出し元は for 文を利用して全件を参照することができます。 * * @param {string[]} filters.categories - Categories to filter by * @param {string[]} filters.conditions - Conditions to filter by * @param {string} orders - Sort order * @returns {AsyncGenerator} - Async generator of card objects */ async function* scrollCardLineup(filters, orders) { const limit = 100; let offset = 0; let total = 0; do { const data = await fetchCardLineup(filters, orders, limit, offset); for (const card of data.contents) { yield card; }; total = data.totalCount; offset += limit; } while (total > offset); } /** * カードラインナップを読み込む * * @param {HTMLElement} parent - Parent element to append the cloned template * @param {HTMLTemplateElement} template - Template to clone * @param {string[]} filters.categories - Categories to filter by * @param {string[]} filters.conditions - Conditions to filter by * @param {string} orders - Sort order * @param {Function} options.callback - Callback function to execute after the template is cloned * @param {Object} options.queryParams - Query parameters to add to the URL */ async function loadCardLineup(parent, template, filters, orders, options = {}) { for await (const item of scrollCardLineup(filters, orders)) { if (!item) { continue; } const newClone = template.content.cloneNode(true); const link = newClone.querySelector('.__cms-link'); if (link && item.lp_url) { if (options.queryParams) { link.href = `${item.lp_url}?${new URLSearchParams(options.queryParams).toString()}`; } else { link.href = item.lp_url; } } const bannerPC = newClone.querySelector('.__cms-banner-pc'); if (bannerPC && item.banner_img_pc && item.banner_img_pc.url) { bannerPC.src = item.banner_img_pc.url; bannerPC.alt = item.banner_img_pc.alt || ''; } const bannerSP = newClone.querySelector('.__cms-banner-sp'); if (bannerSP && item.banner_img_sp && item.banner_img_sp.url) { bannerSP.src = item.banner_img_sp.url; bannerSP.alt = item.banner_img_sp.alt || ''; } const title = newClone.querySelector('.__cms-title'); if (title && item.title) { title.innerHTML = item.title.replace(/\n/g, '
'); } const cardImage = newClone.querySelector('.__cms-card-image'); if (cardImage && item.card_img && item.card_img.url) { cardImage.src = item.card_img.url; cardImage.alt = item.card_img.alt || ''; } if (options.callback) { options.callback(newClone, item); } parent.appendChild(newClone); } } /** * お知らせを10件取得する * * @param {string} category - Target to filter by * @param {string} orders - Sort order * @returns {Promise} - お知らせ一覧 */ async function fetchNotifications(category, orders = '+publishedAt') { const params = new URLSearchParams(); params.append('filters', `categories[contains]${category}`); params.append('orders', orders); params.append('limit', 10); const response = await fetch(`/microcms/api/v1/epnt-notifications?${params.toString()}`); const data = await response.json(); return data.contents; } /** * お知らせを読み込む * * @param {HTMLElement} parent - Parent element to append the cloned template * @param {HTMLTemplateElement} template - Template to clone * @param {string} category - Category to filter by * @param {string} orders - Sort order */ async function loadNotifications(parent, template, category, orders) { const notifications = await fetchNotifications(category, orders); notifications.forEach((item) => { const newClone = template.content.cloneNode(true); const content = newClone.querySelector('.__cms-notification-content'); if (content && item.content) { content.innerHTML = item.content; } parent.appendChild(newClone); }); } /** * 残価クレジットのお知らせを10件取得する * * @returns {Promise} - お知らせ一覧 */ async function fetchAFSLoanNotifications() { const params = new URLSearchParams(); params.append('orders', '-publishedAt'); params.append('limit', 10); const response = await fetch(`/microcms/api/v1/afsloan-notifications?${params.toString()}`); const data = await response.json(); return data.contents; }