v4.0.0: 회원·덱 API, 인증 UI, 프로필 및 레이아웃 통일
Express/SQLite API와 Docker 구성을 추가하고 로그인·덱 저장·공개 덱·관리자·프로필 화면을 구현했다. 결과 화면 저장 UX, 헤더 프로필, 애드센스용 app-shell 레이아웃(1280px) 및 문서를 반영했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
28
integrations/README.md
Normal file
28
integrations/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 연동 예제
|
||||
|
||||
`integrations/example.html`을 브라우저에서 열거나, 배포 후 아래 URL로 테스트할 수 있습니다.
|
||||
|
||||
## 옆 프로젝트에 복사할 파일
|
||||
|
||||
```
|
||||
integrations/uadeck-deck-client.js
|
||||
```
|
||||
|
||||
## 최소 연동 코드
|
||||
|
||||
```javascript
|
||||
const client = new UaDeckDeckClient({
|
||||
baseUrl: 'https://uniare.sori.studio/api/v1',
|
||||
imageBase: 'https://uniare.sori.studio',
|
||||
});
|
||||
|
||||
// UADECK 공유 링크에서 덱 로드
|
||||
const deck = await client.fetchFromUrl('https://uniare.sori.studio/d/SHARE_CODE');
|
||||
|
||||
// 카드 사용
|
||||
for (const card of deck.cards) {
|
||||
console.log(card.number, 'x', card.count, card.imageUrl);
|
||||
}
|
||||
```
|
||||
|
||||
자세한 내용은 `docs/api-integration.md`를 참고하세요.
|
||||
58
integrations/example.html
Normal file
58
integrations/example.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>UADECK API 연동 예제</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
|
||||
input { width: 100%; padding: 0.5rem; margin: 0.5rem 0 1rem; box-sizing: border-box; }
|
||||
button { padding: 0.5rem 1rem; cursor: pointer; }
|
||||
pre { background: #f1f5f9; padding: 1rem; overflow: auto; font-size: 12px; border-radius: 8px; }
|
||||
.card { display: flex; gap: 0.5rem; align-items: center; margin: 0.25rem 0; font-size: 13px; }
|
||||
.card img { width: 40px; height: 56px; object-fit: cover; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>UADECK API 연동 예제</h1>
|
||||
<p>공유 링크 또는 shareCode로 덱을 불러옵니다.</p>
|
||||
|
||||
<label>API Base URL</label>
|
||||
<input type="text" id="baseUrl" value="/api/v1" />
|
||||
|
||||
<label>공유 링크 또는 코드</label>
|
||||
<input type="text" id="shareInput" placeholder="https://uniare.sori.studio/d/x7Kp9m2Q 또는 x7Kp9m2Q" />
|
||||
|
||||
<button type="button" id="loadBtn">덱 불러오기</button>
|
||||
|
||||
<h2>결과</h2>
|
||||
<pre id="jsonOut">—</pre>
|
||||
<div id="cardList"></div>
|
||||
|
||||
<script src="./uadeck-deck-client.js"></script>
|
||||
<script>
|
||||
document.getElementById('loadBtn').addEventListener('click', async () => {
|
||||
const baseUrl = document.getElementById('baseUrl').value.trim();
|
||||
const input = document.getElementById('shareInput').value.trim();
|
||||
const client = new UaDeckDeckClient({ baseUrl });
|
||||
|
||||
try {
|
||||
const deck = await client.fetchFromUrl(input);
|
||||
document.getElementById('jsonOut').textContent = JSON.stringify(deck, null, 2);
|
||||
document.getElementById('cardList').innerHTML = deck.cards
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="card">
|
||||
<img src="${c.imageUrl}" alt="" onerror="this.style.display='none'" />
|
||||
<span>${c.number} ×${c.count}${c.key ? ' ★' : ''}</span>
|
||||
</div>`,
|
||||
)
|
||||
.join('');
|
||||
} catch (err) {
|
||||
document.getElementById('jsonOut').textContent = 'Error: ' + err.message;
|
||||
document.getElementById('cardList').innerHTML = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
130
integrations/uadeck-deck-client.js
Normal file
130
integrations/uadeck-deck-client.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
Reference in New Issue
Block a user