Files
union-arena-deck-builder/result.html

473 lines
27 KiB
HTML

<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4516420168710424" crossorigin="anonymous"></script>
<link rel="stylesheet" href="./style/output.css" />
<title>Deck Result - UA Deck Builder</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
<style>
body {
background-color: #f8fafc;
}
.result-container {
background-image: radial-gradient(#e2e8f0 1px, transparent 1px);
background-size: 20px 20px;
}
/* html2canvas 에러 방지용: 캡처 시 oklch 색상을 대체 */
.capture-mode {
background-image: none !important;
background-color: #ffffff !important;
}
/* Tailwind v4의 oklch 방지용 명시적 색상 지정 */
.capture-mode [class*='bg-blue-'],
.capture-mode [class*='text-blue-'] {
background-color: #2563eb !important;
color: #ffffff !important;
}
.card-badge {
position: absolute;
top: -6px;
right: -6px;
background-color: #2563eb;
/* oklch 대신 일반 hex 사용 */
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 10;
}
.capture-safe-border {
/* border: 1px solid #e2e8f0 !important; */
/* 명시적인 연한 회색 */
box-shadow: none !important;
outline: none !important;
}
.key-label {
background-color: #4f46e5 !important;
color: #ffffff !important;
border: none !important;
}
.card-wrapper {
position: relative;
padding: 8px;
display: flex;
flex-direction: column;
align-items: center;
}
.card-image-box {
position: relative;
width: 100%;
aspect-ratio: 2/3;
border-radius: 0.5rem;
background-color: white;
overflow: hidden;
}
.card-badge {
position: absolute;
top: 0px;
right: 0px;
background-color: #2563eb !important;
color: white !important;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
z-index: 20;
border: 2px solid white;
}
#resultArea * {
outline: none !important;
-webkit-tap-highlight-color: transparent;
}
.key-card-border {
box-shadow: none !important;
outline: none !important;
}
.capture-safe-border {
box-shadow: none !important;
outline: none !important;
}
.capture-grid-pc {
display: grid !important;
grid-template-columns: repeat(5, 1fr) !important;
gap: 16px !important;
width: 100% !important;
}
.capture-grid-classic {
display: grid !important;
grid-template-columns: repeat(5, 1fr) !important;
gap: 20px !important;
width: 100% !important;
}
</style>
</head>
<body>
<div id="app" class="min-h-screen pb-24">
<div id="loading-screen" class="fixed inset-0 z-[100] bg-white/80 backdrop-blur-sm flex items-center justify-center" style="display: none">
<div class="flex flex-col items-center gap-3">
<div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
<p id="i18n-loadingMsg" class="text-sm font-bold text-slate-600">이미지 생성 중...</p>
</div>
</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 id="i18n-editDeck" onclick="goBackToCardList()" class="font-bold text-slate-600 hover:text-blue-600 transition-colors">← EDIT DECK</button>
<div id="i18n-previewTitle" class="font-bold text-slate-800 uppercase tracking-wider">Deck Preview</div>
</div>
<div class="max-w-[1400px] mx-auto p-4 flex flex-col lg:flex-row gap-6">
<aside class="w-full lg:w-80 space-y-4 lg:sticky lg:top-20 lg:h-fit">
<div class="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-6">
<div>
<label id="i18n-labelDeckName" class="block text-xs font-bold text-slate-400 uppercase mb-2">Deck Name</label>
<input type="text" id="deckName" class="w-full border border-slate-200 rounded-xl p-3 focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm" placeholder="미입력시 시리즈명 사용" />
</div>
<div>
<label id="i18n-labelExportStyle" class="block text-xs font-bold text-slate-400 uppercase mb-3">Export Style</label>
<div class="grid grid-cols-1 gap-2">
<button onclick="changeStyle('type1')" id="btn-type1" class="style-btn border-2 border-blue-600 bg-blue-50 text-blue-700 p-3 rounded-xl text-left transition-all">
<div id="i18n-type1Title" class="font-bold text-sm">Type 1: Signature</div>
<div id="i18n-type1Desc" class="text-[10px] opacity-70">키 카드를 강조하는 기본 스타일</div>
</button>
<button onclick="changeStyle('type2')" id="btn-type2" class="style-btn border-2 border-slate-100 hover:border-slate-200 p-3 rounded-xl text-left transition-all text-slate-500">
<div id="i18n-type2Title" class="font-bold text-sm">Type 2: Classic</div>
<div id="i18n-type2Desc" class="text-[10px] opacity-70">5열 균등 배치</div>
</button>
</div>
</div>
<div class="pt-4 border-t border-slate-100">
<div class="flex items-center justify-between">
<div>
<div id="i18n-hqTitle" class="text-sm font-bold text-slate-700">High Quality</div>
<div id="i18n-hqDesc" class="text-[10px] text-slate-400">용량 2배, 선명도 4배</div>
</div>
<button onclick="toggleHQ()" id="hq-toggle" class="relative inline-flex h-6 w-11 items-center rounded-full bg-slate-200 transition-colors">
<span id="hq-circle" class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform translate-x-1"></span>
</button>
<input type="checkbox" id="highQuality" class="hidden" />
</div>
</div>
</div>
<div id="i18n-notice" class="p-4 text-[11px] text-slate-400 leading-relaxed">
<p id="i18n-notice1">* ...</p>
<p id="i18n-notice2">* ...</p>
</div>
</aside>
<main class="flex-1">
<div class="bg-white rounded-3xl border border-slate-200 shadow-xl overflow-hidden result-container">
<div id="resultArea" class="p-8 sm:p-12 min-h-[600px] flex flex-col items-center"></div>
</div>
</main>
</div>
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white/80 backdrop-blur-md border-t z-50">
<div class="max-w-7xl mx-auto flex gap-3">
<button id="i18n-btnBack" onclick="goBackToCardList()" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-4 rounded-2xl font-bold transition-all">BACK</button>
<button id="i18n-btnDownload" onclick="saveAsImage()" class="flex-[2] bg-blue-600 hover:bg-blue-700 text-white py-4 rounded-2xl font-bold shadow-lg shadow-blue-200 transition-all active:scale-95">DOWNLOAD IMAGE</button>
</div>
</div>
</div>
<script type="module">
let currentStyle = 'type1';
let selectedCards = [];
let seriesKey = '';
// 전역 함수로 등록 (onclick 이벤트 대응)
window.changeStyle = changeStyle;
window.saveAsImage = saveAsImage;
window.toggleHQ = toggleHQ;
window.goBackToCardList = goBackToCardList;
let translations = {};
document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search);
const currentLang = urlParams.get('lang') || localStorage.getItem('lang') || 'ko';
// 1. 다국어 데이터 로드
try {
const response = await fetch(`./i18n/translations.${currentLang}.json`);
translations = await response.json();
applyTranslationsToUI();
} catch (e) {
console.error('i18n 로드 실패:', e);
}
// 2. 카드 데이터 로드 및 렌더링
let cardsParam = urlParams.get('cards') || localStorage.getItem('selectedCards');
seriesKey = urlParams.get('series') || localStorage.getItem('selectedSeries');
if (cardsParam && seriesKey) {
try {
// 데이터 로드 시 decodeURIComponent 적용
selectedCards = JSON.parse(decodeURIComponent(cardsParam));
renderResult();
} catch (e) {
console.error('Data parsing error:', e);
}
}
});
function applyTranslationsToUI() {
const setEl = (id, key, isHTML = false) => {
const el = document.getElementById(id);
if (el && translations[key]) {
isHTML ? el.innerHTML = translations[key] : el.textContent = translations[key];
}
};
setEl('i18n-editDeck', 'editDeck');
setEl('i18n-previewTitle', 'deckPreview');
setEl('i18n-labelDeckName', 'deckNameLabel');
setEl('i18n-labelExportStyle', 'exportStyle');
setEl('i18n-type1Title', 'type1Title');
setEl('i18n-type1Desc', 'type1Desc');
setEl('i18n-type2Title', 'type2Title');
setEl('i18n-type2Desc', 'type2Desc');
setEl('i18n-hqTitle', 'highQuality');
setEl('i18n-hqDesc', 'hqDesc');
setEl('i18n-notice1', 'exportNotice1');
setEl('i18n-notice2', 'exportNotice2');
setEl('i18n-btnBack', 'back');
setEl('i18n-btnDownload', 'downloadImage');
setEl('i18n-loadingMsg', 'generatingImage');
const deckInput = document.getElementById('deckName');
if (deckInput) deckInput.placeholder = translations.deckNamePlaceholder || "";
}
function renderResult() {
const container = document.getElementById('resultArea');
if (!container) return;
container.innerHTML = '';
if (currentStyle === 'type1') {
renderType1(container);
} else {
renderType2(container);
}
}
// 타입 1: 키카드 강조형
function renderType1(container) {
const keyCards = selectedCards.filter((c) => c.key);
const others = selectedCards.filter((c) => !c.key);
if (keyCards.length > 0) {
const keyRow = document.createElement('div');
keyRow.className = 'flex flex-wrap justify-center gap-4 mb-12';
keyCards.forEach((card) => {
keyRow.appendChild(createCardUI(card, 'w-48 lg:w-56', true));
});
container.appendChild(keyRow);
}
const otherGrid = document.createElement('div');
otherGrid.className = 'grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-5 gap-4 w-full';
others.forEach((card) => {
otherGrid.appendChild(createCardUI(card, 'w-full', false));
});
container.appendChild(otherGrid);
}
// 타입 2: 클래식 5열 그리드
function renderType2(container) {
const sortedCards = [...selectedCards].sort((a, b) => (b.key ? 1 : 0) - (a.key ? 1 : 0));
const grid = document.createElement('div');
grid.className = 'grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 gap-6 w-full';
sortedCards.forEach((card) => {
grid.appendChild(createCardUI(card, 'w-full', card.key));
});
container.appendChild(grid);
}
function createCardUI(card, widthClass, isKey) {
const div = document.createElement('div');
div.className = `${widthClass} card-wrapper`;
const fileName = card.imgSrc.split('/').pop();
const imgSrc = `/images/${seriesKey}/${fileName}`;
const keyLabelText = translations.keyCardLabel || "KEY CARD";
div.innerHTML = `
<div class="card-badge">${card.count}</div>
<div class="card-image-box ${isKey ? 'key-card-highlight' : ''}">
<img src="${imgSrc}" class="w-full h-full object-contain" crossorigin="anonymous">
${isKey ? `<div class="absolute bottom-0 left-0 right-0 key-label text-[10px] text-center py-1 font-bold">${keyLabelText}</div>` : ''}
</div>
<div class="mt-2 text-[10px] font-mono text-slate-400">${card.Number}</div>
`;
return div;
return div;
}
function changeStyle(style) {
currentStyle = style;
document.querySelectorAll('.style-btn').forEach((btn) => {
btn.classList.remove('border-blue-600', 'bg-blue-50', 'text-blue-700');
btn.classList.add('border-slate-100', 'text-slate-500');
});
const activeBtn = document.getElementById(`btn-${style}`);
if (activeBtn) {
activeBtn.classList.replace('border-slate-100', 'border-blue-600');
activeBtn.classList.add('bg-blue-50', 'text-blue-700');
}
renderResult();
}
function toggleHQ() {
const cb = document.getElementById('highQuality');
const toggle = document.getElementById('hq-toggle');
const circle = document.getElementById('hq-circle');
cb.checked = !cb.checked;
toggle.classList.toggle('bg-blue-600', cb.checked);
toggle.classList.toggle('bg-slate-200', !cb.checked);
circle.style.transform = cb.checked ? 'translateX(20px)' : 'translateX(0px)';
}
async function saveAsImage() {
const screen = document.getElementById('loading-screen');
screen.style.display = 'flex';
const area = document.getElementById('resultArea');
const hq = document.getElementById('highQuality').checked;
const urlParams = new URLSearchParams(window.location.search);
const seriesName = urlParams.get('seriesName') || '';
const deckNameInput = document.getElementById('deckName').value.trim();
const deckName = deckNameInput || seriesName;
const originalStyle = area.getAttribute('style') || '';
const gridElements = area.querySelectorAll('div[class*="grid"]');
const originalGrids = Array.from(gridElements).map((el) => el.className);
area.style.width = '1200px';
area.style.minWidth = '1200px';
area.style.display = 'block';
if (currentStyle === 'type1') {
const otherGrid = area.querySelector('div.grid');
if (otherGrid) otherGrid.className = 'capture-grid-pc';
} else {
const mainGrid = area.querySelector('div.grid');
if (mainGrid) mainGrid.className = 'capture-grid-classic';
}
const allCardWrappers = area.querySelectorAll('.card-wrapper');
allCardWrappers.forEach((wrapper) => {
const box = wrapper.querySelector('.card-image-box');
if (box) {
// 기존에 입혀진 모든 ring 관련 클래스나 style 제거
box.style.boxShadow = 'none';
box.style.outline = 'none';
if (box.classList.contains('key-card-highlight')) {
box.classList.add('key-card-border'); // 우리가 만든 인디고 보더 적용
} else {
box.classList.add('capture-safe-border'); // 일반 연한 보더 적용
}
}
// Type 1 상단 키 카드 너비 고정
if (wrapper.classList.contains('w-48') || wrapper.classList.contains('lg:w-56')) {
wrapper.style.width = '240px';
}
});
try {
await new Promise((resolve) => setTimeout(resolve, 300));
const scale = hq ? 2 : 1;
// 캡처 직전 임시로 너비와 여백을 고정하여 중앙 정렬 유도
const originalDisplay = area.style.display;
area.style.display = 'inline-block'; // 내용물만큼만 너비를 잡도록 설정
const dataUrl = await htmlToImage.toPng(area, {
pixelRatio: scale,
backgroundColor: '#ffffff',
// 캔버스 자체에 여백을 명시적으로 추가
canvasWidth: (area.offsetWidth + 80) * scale,
canvasHeight: (area.offsetHeight + 80) * scale,
});
area.style.display = originalDisplay;
const now = new Date();
const YYYY = now.getFullYear();
const MM = String(now.getMonth() + 1).padStart(2, '0');
const DD = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const timestamp = `${YYYY}${MM}${DD}_${hh}${mm}${ss}`;
const fileNameStr = deckName.trim() ? `uadeck_${deckName}_${timestamp}` : `uadeck_${timestamp}`;
const link = document.createElement('a');
link.download = `${fileNameStr}.png`;
link.href = dataUrl;
link.click();
} catch (e) {
console.error('Capture Error:', e);
alert('이미지 저장 실패');
} finally {
// 5. 스타일 복구
area.setAttribute('style', originalStyle);
gridElements.forEach((el, i) => {
el.className = originalGrids[i];
});
allCardWrappers.forEach((wrapper) => {
wrapper.style.width = '';
const box = wrapper.querySelector('.card-image-box');
if (box) {
// box.classList.remove('key-card-border', 'capture-safe-border');
}
});
screen.style.display = 'none';
}
}
function goBackToCardList() {
window.history.back();
}
</script>
</body>
</html>