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:
116
cardList.html
116
cardList.html
@@ -6,7 +6,9 @@
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script>
|
||||
<title>Card List</title>
|
||||
<script src="/script/navigation.js"></script>
|
||||
<script src="/script/api.js"></script>
|
||||
<link rel="stylesheet" href="./style/output.css" />
|
||||
<link rel="stylesheet" href="./style/auth-header.css" />
|
||||
<script type="module" src="/i18n/i18n.js"></script>
|
||||
<link href="/style/navigation.css" rel="stylesheet" />
|
||||
<link href="/style/style.css" rel="stylesheet" />
|
||||
@@ -81,12 +83,13 @@
|
||||
</div>
|
||||
|
||||
<div class="sticky top-0 z-50 h-16 bg-white/80 backdrop-blur-md border-b px-4 py-3 flex justify-between items-center">
|
||||
<button onclick="history.back()" class="font-bold text-slate-600 hover:text-blue-600 cursor-pointer">← BACK</button>
|
||||
<button type="button" onclick="goToHome()" class="font-bold text-slate-600 hover:text-blue-600 cursor-pointer">← 홈</button>
|
||||
<div class="font-bold text-slate-800">
|
||||
<span id="i18n-searchResults">검색결과</span>
|
||||
:
|
||||
<span id="SearchResultsCounter" class="text-blue-600">0</span>
|
||||
</div>
|
||||
<div id="auth-header-slot"></div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[1400px] mx-auto p-4 flex flex-col lg:flex-row gap-6">
|
||||
@@ -240,6 +243,8 @@
|
||||
let cardList = [];
|
||||
let selectedCards = [];
|
||||
let selectedColors = new Set(['All']);
|
||||
let currentShareCode = null;
|
||||
let currentDeckName = '';
|
||||
|
||||
const getEl = () => ({
|
||||
searchQuery: document.getElementById('searchQuery'),
|
||||
@@ -253,37 +258,63 @@
|
||||
specialCount: document.getElementById('specialCardCount'),
|
||||
});
|
||||
|
||||
const seriesKey = new URLSearchParams(window.location.search).get('series');
|
||||
const nameKey = new URLSearchParams(window.location.search).get('seriesName');
|
||||
let seriesKey = new URLSearchParams(window.location.search).get('series');
|
||||
let nameKey = new URLSearchParams(window.location.search).get('seriesName');
|
||||
|
||||
let t = {};
|
||||
|
||||
async function init() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const lang = urlParams.get('lang') || localStorage.getItem('lang') || 'ko';
|
||||
const shareCode = urlParams.get('d');
|
||||
|
||||
if (!seriesKey) return;
|
||||
const el = getEl();
|
||||
if (el.loadingScreen) el.loadingScreen.style.display = 'flex';
|
||||
|
||||
try {
|
||||
// 1. 다국어 데이터 로드
|
||||
const tResponse = await fetch(`./i18n/translations.${lang}.json`);
|
||||
t = await tResponse.json();
|
||||
applyI18n();
|
||||
|
||||
let deckCards = null;
|
||||
|
||||
if (shareCode && window.UaDeckApi) {
|
||||
const deck = await UaDeckApi.getDeck(shareCode);
|
||||
seriesKey = deck.series;
|
||||
nameKey = deck.seriesName || deck.series;
|
||||
currentDeckName = deck.name || '';
|
||||
deckCards = deck.cards;
|
||||
|
||||
const me = UaDeckApi.getCachedUser();
|
||||
if (me && deck.author && deck.author.id === me.id) {
|
||||
currentShareCode = deck.shareCode;
|
||||
}
|
||||
|
||||
if (!urlParams.get('series')) {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set('series', seriesKey);
|
||||
newUrl.searchParams.set('seriesName', nameKey);
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!seriesKey) return;
|
||||
|
||||
const module = await import(`./datas/${seriesKey}.js`);
|
||||
cardList = module.cardList;
|
||||
|
||||
initSeriesFilter();
|
||||
|
||||
const cardsParam = new URLSearchParams(window.location.search).get('cards');
|
||||
const savedData = cardsParam ? decodeURIComponent(cardsParam) : localStorage.getItem('selectedCards');
|
||||
|
||||
if (savedData) {
|
||||
const parsed = JSON.parse(decodeURIComponent(savedData));
|
||||
cardList = syncCardData(cardList, parsed);
|
||||
selectedCards = parsed;
|
||||
if (deckCards) {
|
||||
cardList = syncCardData(cardList, deckCards);
|
||||
selectedCards = deckCards;
|
||||
} else {
|
||||
const savedData = localStorage.getItem('selectedCards');
|
||||
if (savedData) {
|
||||
const parsed = JSON.parse(decodeURIComponent(savedData));
|
||||
cardList = syncCardData(cardList, parsed);
|
||||
selectedCards = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
applyInitialFilter();
|
||||
@@ -466,6 +497,14 @@
|
||||
}
|
||||
|
||||
// 전역 핸들러
|
||||
window.goToHome = function () {
|
||||
const lang =
|
||||
new URLSearchParams(window.location.search).get('lang') ||
|
||||
localStorage.getItem('lang') ||
|
||||
'ko';
|
||||
window.location.href = lang && lang !== 'ko' ? `/?lang=${lang}` : '/';
|
||||
};
|
||||
|
||||
window.handleCount = function (imgSrc, delta, event) {
|
||||
event.stopPropagation();
|
||||
const card = cardList.find((c) => c.imgSrc === imgSrc);
|
||||
@@ -605,24 +644,23 @@
|
||||
}
|
||||
|
||||
function syncCardData(base, saved) {
|
||||
const map = new Map(base.map((c) => [c.imgSrc, c]));
|
||||
const byImg = new Map(base.map((c) => [c.imgSrc, c]));
|
||||
const byNum = new Map(base.map((c) => [c.Number, c]));
|
||||
saved.forEach((s) => {
|
||||
const item = map.get(s.imgSrc);
|
||||
const item = byImg.get(s.imgSrc) || byNum.get(s.number || s.Number);
|
||||
if (item) {
|
||||
item.count = s.count;
|
||||
item.key = s.key;
|
||||
item.key = Boolean(s.key);
|
||||
}
|
||||
});
|
||||
return Array.from(map.values());
|
||||
return base;
|
||||
}
|
||||
|
||||
function saveAndRefresh() {
|
||||
selectedCards = cardList.filter((c) => c.count > 0);
|
||||
const encoded = encodeURIComponent(JSON.stringify(selectedCards));
|
||||
localStorage.setItem('selectedCards', encoded);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('cards', encoded);
|
||||
window.history.replaceState({}, '', url);
|
||||
localStorage.setItem('selectedSeries', seriesKey);
|
||||
updateCounters();
|
||||
updateCardList();
|
||||
}
|
||||
@@ -713,8 +751,11 @@
|
||||
|
||||
// 4. 저장소 및 URL 동기화
|
||||
localStorage.removeItem('selectedCards');
|
||||
localStorage.removeItem('selectedSeries');
|
||||
currentShareCode = null;
|
||||
currentDeckName = '';
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('cards');
|
||||
url.searchParams.delete('d');
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
// 5. 전체 갱신
|
||||
@@ -750,19 +791,21 @@
|
||||
document.querySelectorAll('input[name="triggerType"]').forEach((r) => {
|
||||
r.addEventListener('change', updateCardList);
|
||||
});
|
||||
document.getElementById('goToResultPage').addEventListener('click', () => {
|
||||
if (this.disabled) return; // 비활성화 상태면 무시
|
||||
document.getElementById('goToResultPage').addEventListener('click', function () {
|
||||
if (this.disabled) return;
|
||||
|
||||
// 현재 페이지에서 선택된(cardList에 있는) 카드만 필터링해서 전송
|
||||
const currentSeriesSelected = cardList.filter(c => c.count > 0);
|
||||
const currentSeriesSelected = cardList.filter((c) => c.count > 0);
|
||||
if (currentSeriesSelected.length === 0) {
|
||||
alert(translations.noCardsSelected || '선택된 카드가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSeriesSelected.length === 0) {
|
||||
alert(translations.noCardsSelected || "선택된 카드가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams = encodeURIComponent(JSON.stringify(selectedCards));
|
||||
window.location.href = `/result.html?series=${seriesKey}&seriesName=${nameKey}&cards=${queryParams}`;
|
||||
selectedCards = currentSeriesSelected;
|
||||
const encoded = encodeURIComponent(JSON.stringify(selectedCards));
|
||||
localStorage.setItem('selectedCards', encoded);
|
||||
localStorage.setItem('selectedSeries', seriesKey);
|
||||
const lang = new URLSearchParams(window.location.search).get('lang') || localStorage.getItem('lang') || 'ko';
|
||||
window.location.href = `/result.html?series=${seriesKey}&seriesName=${encodeURIComponent(nameKey || '')}&lang=${lang}`;
|
||||
});
|
||||
|
||||
const resetModal = document.getElementById('resetModal');
|
||||
@@ -822,16 +865,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 로컬 스토리지 저장 (현재 시리즈 데이터만 깨끗하게 유지)
|
||||
// 3. 로컬 스토리지 저장
|
||||
const encoded = encodeURIComponent(JSON.stringify(selectedCards));
|
||||
localStorage.setItem('selectedCards', encoded);
|
||||
localStorage.setItem('selectedSeries', seriesKey);
|
||||
|
||||
// 4. URL 업데이트 (새로고침 시 데이터 유지용)
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('cards', encoded);
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
updateCounters(); // 하단 바 숫자만 업데이트
|
||||
updateCounters();
|
||||
}
|
||||
|
||||
let selectedSeriesGroups = new Set(['All']);
|
||||
@@ -889,5 +928,6 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="/script/auth-header.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user