Express/SQLite API와 Docker 구성을 추가하고 로그인·덱 저장·공개 덱·관리자·프로필 화면을 구현했다. 결과 화면 저장 UX, 헤더 프로필, 애드센스용 app-shell 레이아웃(1280px) 및 문서를 반영했다. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.9 KiB
JavaScript
131 lines
3.9 KiB
JavaScript
/**
|
|
* UADECK 덱 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<UaDeckDeck>}
|
|
*/
|
|
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<UaDeckDeck>}
|
|
*/
|
|
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
|
|
*/
|