/** * UNIARE 덱 API 클라이언트 (옆 프로젝트 연동용) * * @example * const client = new UaDeckDeckClient({ baseUrl: 'https://uniare.sori.studio/api/v1' }); * const deck = await client.fetchDeck('x7Kp9m2Q'); * deck.cards.forEach(c => console.log(c.number, c.count, c.imgSrc)); */ class UaDeckDeckClient { /** * @param {object} options * @param {string} options.baseUrl API 베이스 (예: https://uniare.sori.studio/api/v1) * @param {string} [options.imageBase] 이미지 호스트 (기본: baseUrl에서 /api/v1 제거) */ constructor(options = {}) { this.baseUrl = (options.baseUrl || '/api/v1').replace(/\/$/, ''); this.imageBase = options.imageBase || this.baseUrl.replace(/\/api\/v1$/, ''); } /** * 공유 코드로 덱 조회 * @param {string} shareCode * @returns {Promise} */ async fetchDeck(shareCode) { const res = await fetch(`${this.baseUrl}/decks/${encodeURIComponent(shareCode)}`); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `덱 조회 실패 (${res.status})`); } return this._normalizeDeck(data); } /** * URL에서 shareCode 추출 * @param {string} url 예: https://uniare.sori.studio/d/x7Kp9m2Q 또는 ?d=x7Kp9m2Q * @returns {string|null} */ static parseShareCode(url) { try { const u = new URL(url, 'https://placeholder.local'); const fromQuery = u.searchParams.get('d'); if (fromQuery) return fromQuery; const match = u.pathname.match(/\/d\/([A-Za-z0-9]+)/); return match ? match[1] : null; } catch { return null; } } /** * URL 또는 코드로 덱 로드 * @param {string} urlOrCode * @returns {Promise} */ async fetchFromUrl(urlOrCode) { const code = UaDeckDeckClient.parseShareCode(urlOrCode) || urlOrCode; return this.fetchDeck(code); } /** * 카드 이미지 전체 URL * @param {string} imgSrc API 응답의 imgSrc (예: /ua36/UA36BT_MCR-1-052.png) * @returns {string} */ imageUrl(imgSrc) { return `${this.imageBase}/images${imgSrc.startsWith('/') ? imgSrc : `/${imgSrc}`}`; } /** * @param {object} raw API 응답 * @returns {UaDeckDeck} */ _normalizeDeck(raw) { return { shareCode: raw.shareCode, shareType: raw.shareType, series: raw.series, seriesName: raw.seriesName, name: raw.name, totalCards: raw.totalCards, author: raw.author, createdAt: raw.createdAt, updatedAt: raw.updatedAt, viewCount: raw.viewCount, cards: (raw.cards || []).map((c) => ({ number: c.number, count: c.count, key: Boolean(c.key), imgSrc: c.imgSrc, imageUrl: this.imageUrl(c.imgSrc), color: c.color, rarity: c.rarity, trigger: c.trigger, parallel: Boolean(c.parallel), })), }; } } if (typeof module !== 'undefined' && module.exports) { module.exports = { UaDeckDeckClient }; } if (typeof window !== 'undefined') { window.UaDeckDeckClient = UaDeckDeckClient; } /** * @typedef {object} UaDeckCard * @property {string} number * @property {number} count * @property {boolean} key * @property {string} imgSrc * @property {string} imageUrl * @property {string} color * @property {string} rarity * @property {string|null} trigger * @property {boolean} parallel */ /** * @typedef {object} UaDeckDeck * @property {string} shareCode * @property {string} shareType * @property {string} series * @property {string} seriesName * @property {string} name * @property {number} totalCards * @property {UaDeckCard[]} cards */