v3.0.0: 저장소 재시작 및 전체 자산 반영

This commit is contained in:
2026-03-30 16:42:56 +09:00
commit 0988e31689
9461 changed files with 129835 additions and 0 deletions

37
.ai-rules.md Normal file
View File

@@ -0,0 +1,37 @@
# 프로젝트 AI 가이드라인 및 문서화 규칙
## 🌐 기본 응답 원칙
- 모든 대화와 문서는 **한국어**로 작성한다.
- 코드 내 주석은 반드시 **JSDoc 형식**을 사용한다.
## 🧾 Git 및 버전 관리 규칙
- Git 작성자 정보는 프로젝트 기준 계정으로 통일한다.
- Git 커밋 메시지는 반드시 **한국어**로 작성한다.
- 버전이 올라가는 작업은 `docs/update.md`에 먼저 반영하고, 같은 버전명을 Git 태그에도 맞춘다.
- 원격 저장소에 푸시하기 전, 민감 정보(실명, 개인 이메일, 비밀키, 로컬 경로)가 포함되지 않았는지 확인한다.
## 📂 문서 자동 관리 규칙
모든 작업 수행 후, AI는 관련 내용을 아래 지정된 파일에 즉시 반영해야 한다.
1. **작업 이력 (docs/update.md)**
- 수행한 작업 내용을 상세히 기록한다.
- 당일 업데이트라도 수정 시마다 버전을 갱신하여 하나의 파일에 누적 관리한다. (별도 파일 생성 금지)
2. **할 일 및 이슈 (docs/todo.md)**
- 현재 직면한 문제점과 다음에 이어서 진행할 작업 목록을 정리한다.
3. **기술 명세 (docs/spec.md)**
- API 명세, 기획 내용, 데이터베이스 스키마 구조를 최신 상태로 유지한다.
4. **코딩 컨벤션 (docs/convention.md)**
- 프로젝트 전용 코딩 스타일 및 네이밍 규칙(Variable, Function, Class 등)을 정의하고 준수한다.
5. **의사결정 이력 (docs/history.md)**
- 주요 설계 결정 사항 및 시스템 아키텍처 변경 이력을 기록한다.
6. **파일-화면 매핑 가이드 (docs/map.md)**
- 연결성 기록: 특정 파일(Ex: Login.jsx)이 브라우저 화면의 어느 경로(/login)와 어느 기능(로그인 버튼 등)을 담당하는지 초보자 관점에서 기록한다.
## ⚠️ 실행 지침
- 새로운 코드를 작성하거나 수정하기 전, 반드시 `docs/` 내 관련 문서들을 먼저 참조한다.
- 작업 완료 후 위 문서들의 업데이트가 누락되지 않도록 확인한다.

1
.cursorrules Normal file
View File

@@ -0,0 +1 @@
모든 작업 시 프로젝트 루트의 .ai-rules.md 지침을 엄격히 준수하고, 작업 종료 시마다 docs/ 폴더 내 문서를 업데이트할 것.

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
images/** filter=lfs diff=lfs merge=lfs -text

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Node.js
node_modules/
package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build output
build/
dist/
# Logs
logs/
*.log
# Editor/IDE
.idea/
.vscode/
*.swp
.DS_Store
# 기타
/coverage

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"printWidth": 300,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSameLine": true,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "ignore",
"embeddedLanguageFormatting": "off"
}

14
.prettierrc.json Normal file
View File

@@ -0,0 +1,14 @@
{
"arrowParens": "avoid",
"bracketSameLine": true,
"bracketSpacing": true,
"endOfLine": "auto",
"htmlWhitespaceSensitivity": "ignore",
"printWidth": 100,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

893
cardList.html Normal file
View File

@@ -0,0 +1,893 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<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>
<link rel="stylesheet" href="./style/output.css" />
<script type="module" src="/i18n/i18n.js"></script>
<link href="/style/navigation.css" rel="stylesheet" />
<link href="/style/style.css" rel="stylesheet" />
<link href="/style/output.css" rel="stylesheet" />
<style>
body { background-color: #f8fafc;}
#card-grid { padding-bottom: 180px !important;}
.cardContainer { transition: all 0.2s; }
#cardList { padding-bottom: 120px; }
.fixed-footer { background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); border-top: 1px solid #e2e8f0; }
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.overlay img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
.cardContainer {
background: white;
border-radius: 12px;
padding: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s;
}
.cardContainer:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.fixed-footer {
background: rgba(30, 41, 59, 0.95);
backdrop-filter: blur(8px);
color: white;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.color-filter-btn {
transition: all 0.2s ease;
outline: 2px solid transparent;
outline-offset: -2px;
opacity: 0.5;
}
.color-filter-btn.is-active {
opacity: 1 !important;
outline: 2px solid currentColor !important;
background-color: white !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div id="app">
<div id="loading-screen" class="fixed inset-0 z-50 bg-white flex items-center justify-center" style="display: none">
<div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></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 onclick="history.back()" class="font-bold text-slate-600 hover:text-blue-600 cursor-pointer">← BACK</button>
<div class="font-bold text-slate-800">
<span id="i18n-searchResults">검색결과</span>
:
<span id="SearchResultsCounter" class="text-blue-600">0</span>
</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-64 space-y-4 lg:sticky lg:top-20 lg:h-fit">
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
<button id="resetSelection" class="w-full mb-4 flex items-center justify-center gap-2 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-xl font-bold text-xs transition-all border border-red-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span id="i18n-resetBtn">선택 초기화 (RESET)</span>
</button>
<div class="mb-4">
<label class="block text-xs font-bold text-slate-400 uppercase mb-2">Search</label>
<input type="text" id="searchQuery" class="w-full border border-slate-200 rounded-lg p-2 focus:ring-2 focus:ring-blue-500 outline-none transition-all" placeholder="카드 번호 입력" />
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-slate-400 uppercase mb-2">Color</label>
<div class="grid grid-cols-3 gap-2">
<button class="color-filter-btn border rounded-md py-1 text-xs font-bold transition-all" data-color="All">All</button>
<button class="color-filter-btn border border-red-100 bg-red-50 text-red-500 rounded-md py-1 text-xs font-bold transition-all" data-color="Red">Red</button>
<button class="color-filter-btn border border-blue-100 bg-blue-50 text-blue-500 rounded-md py-1 text-xs font-bold transition-all" data-color="Blue">Blue</button>
<button class="color-filter-btn border border-green-100 bg-green-50 text-green-500 rounded-md py-1 text-xs font-bold transition-all" data-color="Green">Green</button>
<button class="color-filter-btn border border-yellow-100 bg-yellow-50 text-yellow-600 rounded-md py-1 text-xs font-bold transition-all" data-color="Yellow">Yellow</button>
<button class="color-filter-btn border border-purple-100 bg-purple-50 text-purple-500 rounded-md py-1 text-xs font-bold transition-all" data-color="Purple">Purple</button>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-slate-400 uppercase mb-2">Series Filter</label>
<div id="seriesFilterContainer" class="flex flex-wrap gap-2">ㅅㄷㄴㅅ</div>
</div>
<div class="flex lg:flex-col">
<div class="w-full space-y-2 lg:pb-4">
<label class="block text-xs font-bold text-slate-400 uppercase mb-2">Type</label>
<label class="flex items-center gap-2 text-sm font-medium cursor-pointer">
<input type="checkbox" id="includeBasic" checked class="rounded border-slate-300" />
Basic
</label>
<label class="flex items-center gap-2 text-sm font-medium cursor-pointer">
<input type="checkbox" id="includeAP" checked class="rounded border-slate-300" />
AP
</label>
<label class="flex items-center gap-2 text-sm font-medium cursor-pointer">
<input type="checkbox" id="includeParallel" checked class="rounded border-slate-300" />
Parallel
</label>
</div>
<div class="w-full lg:pt-4 lg:border-t border-slate-100 space-y-2">
<label class="block text-xs font-bold text-slate-400 uppercase mb-2">Trigger</label>
<label class="flex items-center gap-2 text-xs font-medium cursor-pointer">
<input type="radio" name="triggerType" value="allTrigger" checked />
All
</label>
<label class="flex items-center gap-2 text-xs font-medium cursor-pointer">
<input type="radio" name="triggerType" value="specialTrigger" />
Special
</label>
<label class="flex items-center gap-2 text-xs font-medium cursor-pointer">
<input type="radio" name="triggerType" value="colorTrigger" />
Color
</label>
<label class="flex items-center gap-2 text-xs font-medium cursor-pointer">
<input type="radio" name="triggerType" value="finalTrigger" />
Final
</label>
</div>
</div>
</div>
</aside>
<main class="flex-1">
<div id="card-grid" class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 sm:gap-4"></div>
</main>
</div>
<div class="fixed bottom-0 left-0 right-0 fixed-footer p-3 sm:p-4 z-50 shadow-[0_-10px_20px_rgba(0,0,0,0.05)]">
<div class="max-w-7xl mx-auto flex flex-col gap-3">
<div class="flex flex-wrap justify-center items-center gap-x-3 gap-y-1 text-[10px] sm:text-xs font-bold text-slate-600 select-none">
<div class="flex items-center gap-1">
<span class="text-slate-400">COLOR</span>
<span id="colorCardCount" class="text-blue-600">0</span>
/4
</div>
<span class="text-slate-200">|</span>
<div class="flex items-center gap-1">
<span class="text-slate-400">FINAL</span>
<span id="finalCardCount" class="text-blue-600">0</span>
/4
</div>
<span class="text-slate-200">|</span>
<div class="flex items-center gap-1">
<span class="text-slate-400">SPECIAL</span>
<span id="specialCardCount" class="text-blue-600">0</span>
/4
</div>
<span class="text-slate-200">|</span>
<div class="flex items-center gap-1">
<span class="text-slate-400">KEY</span>
<span id="keyCardCount" class="text-indigo-600">0</span>
/4
</div>
<span class="text-slate-200">|</span>
<div class="flex items-center gap-1 bg-slate-100 px-2 py-0.5 rounded-full">
<span class="text-slate-500">TOTAL</span>
<span id="totalCardCount" class="text-blue-600">0</span>
</div>
</div>
<button id="goToResultPage" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95">MAKE DECK LIST</button>
</div>
</div>
</div>
<div id="imageOverlay" class="overlay">
<img src="" alt="Card Preview" />
</div>
<div id="resetModal" class="overlay" style="display: none; background: rgba(15, 23, 42, 0.7); backdrop-filter: blur(4px)">
<div class="bg-white rounded-2xl p-6 max-w-sm w-[90%] shadow-2xl border border-slate-200" onclick="event.stopPropagation()">
<div class="flex flex-col items-center text-center">
<div class="w-12 h-12 bg-red-50 rounded-full flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
<h3 id="i18n-modalTitle" class="text-lg font-bold text-slate-900 mb-2">선택 초기화</h3>
<p id="i18n-modalDesc" class="text-sm text-slate-500 mb-6">
현재까지 선택한 모든 카드가 사라집니다.
<br />
정말 초기화하시겠습니까?
</p>
<div class="flex w-full gap-3">
<button id="cancelReset" class="flex-1 py-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold text-sm transition-all cursor-pointer">취소</button>
<button
id="confirmReset"
style="background-color: #ef4444 !important; color: white !important; cursor: pointer"
onmouseover="this.style.backgroundColor = '#dc2626'"
onmouseout="this.style.backgroundColor = '#ef4444'"
class="flex-1 py-3 rounded-xl text-white font-bold text-sm transition-all shadow-md">
초기화 실행
</button>
</div>
</div>
</div>
</div>
<script>
let cardList = [];
let selectedCards = [];
let selectedColors = new Set(['All']);
const getEl = () => ({
searchQuery: document.getElementById('searchQuery'),
cardGrid: document.getElementById('card-grid'),
loadingScreen: document.getElementById('loading-screen'),
resultsCounter: document.getElementById('SearchResultsCounter'),
keyCount: document.getElementById('keyCardCount'),
totalCount: document.getElementById('totalCardCount'),
finalCount: document.getElementById('finalCardCount'),
colorCount: document.getElementById('colorCardCount'),
specialCount: document.getElementById('specialCardCount'),
});
const seriesKey = new URLSearchParams(window.location.search).get('series');
const 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';
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();
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;
}
applyInitialFilter();
updateCounters();
syncToStorageAndCounters();
} catch (err) {
console.error('Data load error:', err);
} finally {
if (el.loadingScreen) el.loadingScreen.style.display = 'none';
}
}
// 1. 데이터 로드 후 시리즈 목록 추출
const allSeries = [...new Set(cardList.map((card) => card.Number.split('/')[0]))];
// 2. 선택된 시리즈를 관리할 Set (기본 "All")
let selectedSeries = new Set(['All']);
// 3. 필터 버튼 렌더링 함수
function renderSeriesFilters() {
const container = document.getElementById('seriesFilterContainer');
// "All" 버튼 추가
let html = `
<button data-series="All" class="series-btn px-3 py-1 rounded-full text-xs font-bold border transition-all ${selectedSeries.has('All') ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-400'}">
All
</button>
`;
// 데이터에서 추출한 시리즈 버튼들 추가
allSeries.forEach((series) => {
const isActive = selectedSeries.has(series);
html += `
<button data-series="${series}" class="series-btn px-3 py-1 rounded-full text-xs font-bold border transition-all ${isActive ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-400'}">
${series}
</button>
`;
});
container.innerHTML = html;
// 클릭 이벤트 바인딩
document.querySelectorAll('.series-btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
const series = e.target.dataset.series;
toggleSeriesFilter(series);
});
});
}
// 4. 필터 선택 로직
function toggleSeriesFilter(series) {
if (series === 'All') {
selectedSeries.clear();
selectedSeries.add('All');
} else {
selectedSeries.delete('All');
if (selectedSeries.has(series)) {
selectedSeries.delete(series);
} else {
selectedSeries.add(series);
}
// 아무것도 선택 안되면 다시 "All"로
if (selectedSeries.size === 0) selectedSeries.add('All');
}
renderSeriesFilters(); // 버튼 UI 업데이트
updateCardList(); // 카드 목록 새로고침 (필터 적용)
}
function displayCardList(list) {
const el = getEl();
if (!el.cardGrid) return;
el.cardGrid.innerHTML = '';
if (el.resultsCounter) el.resultsCounter.textContent = list.length;
if (list.length === 0) {
el.cardGrid.innerHTML = `
<div class="col-span-full py-20 flex flex-col items-center justify-center text-slate-400">
<svg ...></svg>
<p class="text-sm font-bold">${t.noSearchResults || '검색 결과 없음'}</p>
<p class="text-xs mt-1">${t.noSearchResultsDesc || ''}</p>
</div>
`;
return;
}
list.forEach((card, index) => {
const cardContainer = document.createElement('div');
cardContainer.className = 'cardContainer group flex flex-col p-2 bg-white rounded-lg border border-slate-200 shadow-sm hover:border-blue-300 transition-colors';
cardContainer.setAttribute('data-imgsrc', card.imgSrc);
const fileName = card.imgSrc.split('/').pop();
const imgSrc = `images/${seriesKey}/${fileName}`;
cardContainer.innerHTML = `
<div class="card_image cursor-pointer relative w-full mb-2 rounded overflow-hidden bg-slate-50" style="aspect-ratio: 2/3;">
<img src="${imgSrc}" class="absolute inset-0 w-full h-full object-contain transition-transform"
onerror="console.error('이미지 로드 실패:', '${card.Number}', '${imgSrc}'); this.src='/images/uapr/comingsoon.png'; this.onerror=null;">
</div>
<div class="flex justify-between items-center mb-1.5 px-0.5">
<div class="font-bold px-1.5 py-0.5 rounded text-[8px] lg:text-[9px] leading-none truncate" style="${getRarityStyle(card.rarity)}">
${card.rarity || ''}
</div>
<div class="text-[8px] lg:text-[9px] text-slate-400 font-mono truncate">
${card.color === 'AP' ? 'AP' : card.Number.slice(-5)}
</div>
</div>
<div class="grid grid-cols-3 items-center bg-slate-100 rounded-md p-0.5 mb-1.5 h-7 lg:h-8 overflow-hidden text-center">
<button onclick="handleCount('${card.imgSrc}', -1, event)"
class="minusBtn h-full w-full flex items-center justify-center rounded bg-white shadow-sm text-xs font-bold text-slate-600 hover:bg-red-50 transition-all ${card.count === 0 ? 'invisible' : 'visible'}">
-
</button>
<span class="countText text-xs lg:text-sm font-bold text-slate-800 leading-none">
${card.count}
</span>
<button onclick="handleCount('${card.imgSrc}', 1, event)"
class="plusBtn h-full w-full flex items-center justify-center rounded bg-white shadow-sm text-xs font-bold text-slate-600 hover:bg-blue-50 transition-all ${isMax(card) ? 'invisible' : 'visible'}">
+
</button>
</div>
<button onclick="handleKey('${card.imgSrc}', event)"
class="keyBtn w-full py-1 rounded text-[9px] lg:text-[10px] font-bold border transition-all ${card.key && card.count > 0 ? 'bg-indigo-600 border-indigo-600 text-white shadow-md' : 'bg-white border-slate-200 text-slate-400'}">
KEY
</button>
`;
cardContainer.querySelector('.card_image').onclick = () => showOverlay(card, index, list);
el.cardGrid.appendChild(cardContainer);
});
}
function updateColorFilterUI() {
document.querySelectorAll('.color-filter-btn').forEach((btn) => {
const isSelected = selectedColors.has(btn.dataset.color);
if (isSelected) {
// 활성화: 테두리 두께를 바꾸는 대신 shadow(ring)를 사용하여 레이아웃 고정
btn.style.opacity = '1';
btn.style.boxShadow = 'inset 0 0 0 2px currentColor, 0 4px 6px -1px rgba(0,0,0,0.1)';
btn.style.fontWeight = '800'; // 글자 굵기로 강조
btn.style.backgroundColor = 'white'; // 배경을 밝게
} else {
// 비활성화
btn.style.opacity = '0.4';
btn.style.boxShadow = 'none';
btn.style.fontWeight = 'bold';
btn.style.backgroundColor = 'transparent';
}
});
}
function applyI18n() {
const safeSet = (id, key, isHTML = false) => {
const el = document.getElementById(id);
if (el && t[key]) {
isHTML ? (el.innerHTML = t[key]) : (el.textContent = t[key]);
}
};
safeSet('i18n-back', 'back');
safeSet('i18n-searchResults', 'searchResults');
safeSet('i18n-resetBtn', 'resetSelection');
safeSet('i18n-labelSearch', 'search');
safeSet('i18n-labelColor', 'color');
safeSet('i18n-labelSeries', 'seriesFilter');
safeSet('i18n-labelType', 'type');
safeSet('i18n-labelTrigger', 'trigger');
safeSet('i18n-makeDeck', 'makeDeckList');
safeSet('i18n-modalTitle', 'resetModalTitle');
safeSet('i18n-modalDesc', 'resetModalDesc', true);
safeSet('i18n-cancel', 'cancel');
safeSet('i18n-confirm', 'confirm');
// 플레이스홀더는 별도 처리
const searchInput = document.getElementById('searchQuery');
if (searchInput && t.searchPlaceholder) searchInput.placeholder = t.searchPlaceholder;
}
// 전역 핸들러
window.handleCount = function (imgSrc, delta, event) {
event.stopPropagation();
const card = cardList.find((c) => c.imgSrc === imgSrc);
if (!card) return;
// 1. 데이터 업데이트
const currentTotal = cardList.filter((c) => c.Number === card.Number).reduce((s, c) => s + c.count, 0);
if (delta > 0 && currentTotal < (card.maxCount || 4)) {
card.count++;
} else if (delta < 0 && card.count > 0) {
card.count--;
if (card.count === 0) card.key = false;
}
// 2. UI 업데이트
const container = document.querySelector(`.cardContainer[data-imgsrc="${imgSrc}"]`);
if (container) {
const minusBtn = container.querySelector('.minusBtn');
const plusBtn = container.querySelector('.plusBtn');
const countSpan = container.querySelector('.countText');
const keyBtn = container.querySelector('.keyBtn'); // 클래스로 직접 참조
if (countSpan) countSpan.textContent = card.count;
// 마이너스 버튼 제어
if (minusBtn) {
if (card.count > 0) {
minusBtn.classList.remove('invisible');
minusBtn.classList.add('visible');
} else {
minusBtn.classList.remove('visible');
minusBtn.classList.add('invisible');
}
}
// 플러스 버튼 제어 (isMax 결과에 따라 즉시 반영)
if (plusBtn) {
if (isMax(card)) {
plusBtn.classList.remove('visible');
plusBtn.classList.add('invisible');
} else {
plusBtn.classList.remove('invisible');
plusBtn.classList.add('visible');
}
}
// 키 버튼 상태 업데이트
if (keyBtn) {
keyBtn.className = `keyBtn w-full py-1 rounded text-[9px] lg:text-[10px] font-bold border transition-all ${card.key && card.count > 0 ? 'bg-indigo-600 border-indigo-600 text-white shadow-md' : 'bg-white border-slate-200 text-slate-400'}`;
}
}
syncToStorageAndCounters();
};
window.handleKey = function (imgSrc, event) {
event.stopPropagation();
const card = cardList.find((c) => c.imgSrc === imgSrc);
if (!card || card.count === 0) return;
const currentKeys = cardList.filter((c) => c.key && c.count > 0).length;
if (card.key) {
card.key = false;
} else if (currentKeys < 4) {
card.key = true;
}
const container = document.querySelector(`.cardContainer[data-imgsrc="${imgSrc}"]`);
if (container) {
// 특정 클래스(.keyBtn)를 가진 요소를 정확히 찾아 변경
const keyBtn = container.querySelector('.keyBtn');
if (keyBtn) {
keyBtn.className = `keyBtn w-full py-1 rounded text-[9px] lg:text-[10px] font-bold border transition-all ${card.key && card.count > 0 ? 'bg-indigo-600 border-indigo-600 text-white shadow-md' : 'bg-white border-slate-200 text-slate-400'}`;
}
}
syncToStorageAndCounters();
};
function isMax(card) {
return cardList.filter((c) => c.Number === card.Number).reduce((s, c) => s + c.count, 0) >= (card.maxCount || 4);
}
function updateCardList() {
const el = getEl();
const query = el.searchQuery.value.toLowerCase();
const triggerRadio = document.querySelector('input[name="triggerType"]:checked');
const trigger = triggerRadio ? triggerRadio.value : 'allTrigger';
const isBasicEnabled = document.getElementById('includeBasic').checked;
const isAPEnabled = document.getElementById('includeAP').checked;
const isParallelEnabled = document.getElementById('includeParallel').checked;
const filtered = cardList.filter((card) => {
// 1. 유형 필터 (Basic, AP, Parallel) - 조건 우선순위 재정립
let typeMatch = false;
// 패러렐 우선 체크: 패러렐이 켜져 있고 해당 카드가 패러렐인 경우
if (card.parallel) {
if (isParallelEnabled) typeMatch = true;
}
// 패러렐이 아닌 카드들 중에서 AP와 Basic 체크
else {
if (card.color === 'AP') {
if (isAPEnabled) typeMatch = true;
} else {
if (isBasicEnabled) typeMatch = true;
}
}
if (!typeMatch) return false;
// 2. 색상 필터
if (!selectedColors.has('All')) {
const cardColor = card.color === 'Multi' ? 'ALL COLOR' : card.color;
if (!selectedColors.has(cardColor)) return false;
}
// 3. 트리거 필터
if (trigger !== 'allTrigger') {
const tMap = { specialTrigger: 'SPECIAL', colorTrigger: 'COLOR', finalTrigger: 'FINAL' };
if (card.trigger !== tMap[trigger]) return false;
}
// 4. 시리즈 그룹 필터
const cardGroup = card.Number.split(/[/-]/)[0];
const matchesSeries = selectedSeriesGroups.has('All') || selectedSeriesGroups.has(cardGroup);
if (!matchesSeries) return false;
// 5. 검색어 필터
if (query && !card.Number.toLowerCase().includes(query)) return false;
return true;
});
displayCardList(filtered);
}
function syncCardData(base, saved) {
const map = new Map(base.map((c) => [c.imgSrc, c]));
saved.forEach((s) => {
const item = map.get(s.imgSrc);
if (item) {
item.count = s.count;
item.key = s.key;
}
});
return Array.from(map.values());
}
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);
updateCounters();
updateCardList();
}
function updateCounters() {
const el = getEl(); // 이전 코드에서 정의한 DOM 참조 함수
let s = { k: 0, c: 0, f: 0, sp: 0, t: 0 };
cardList.forEach((c) => {
if (c.count > 0) {
if (c.key) s.k++;
if (c.trigger === 'COLOR') s.c += c.count;
if (c.trigger === 'FINAL') s.f += c.count;
if (c.trigger === 'SPECIAL') s.sp += c.count;
s.t += c.count;
}
});
// 각 요소를 찾아 텍스트 업데이트 (존재 여부 확인 포함)
const updateText = (id, value) => {
const element = document.getElementById(id);
if (element) element.textContent = value;
};
updateText('colorCardCount', s.c);
updateText('finalCardCount', s.f);
updateText('specialCardCount', s.sp);
updateText('keyCardCount', s.k);
updateText('totalCardCount', s.t);
}
function getRarityStyle(r) {
if (!r) return '';
r = r.trim();
if (r.includes('★') || r === 'UR') return 'background: linear-gradient(45deg, #f472b6, #60a5fa); color: white;';
if (['SR', 'PcSR'].includes(r)) return 'background: #F2F320; color: black;';
return 'background: #f1f5f9; color: #64748b;';
}
function showOverlay(card, index, list) {
const overlay = document.getElementById('imageOverlay');
const img = overlay.querySelector('img');
img.src = `images/${seriesKey}/${card.imgSrc.split('/').pop()}`;
overlay.style.display = 'flex';
overlay.onclick = (e) => {
if (e.target === overlay) overlay.style.display = 'none';
};
}
function applyInitialFilter() {
document.getElementById('includeBasic').checked = true;
document.getElementById('includeAP').checked = true;
document.getElementById('includeParallel').checked = true;
updateCardList();
}
const resetModal = document.getElementById('resetModal');
// 초기화 실행 함수
function executeReset() {
// 1. 데이터 초기화
cardList.forEach((card) => {
card.count = 0;
card.key = false;
});
selectedCards = [];
// 2. 필터 상태값 초기화
selectedColors = new Set(['All']);
selectedSeriesGroups = new Set(['All']);
const searchQueryEl = document.getElementById('searchQuery');
if (searchQueryEl) searchQueryEl.value = '';
// 유형(Type) 체크박스 초기화 (모두 체크 상태로)
['includeBasic', 'includeAP', 'includeParallel'].forEach(id => {
const el = document.getElementById(id);
if (el) el.checked = true;
});
// 트리거 라디오 버튼 초기화 (All 선택)
const allTriggerRadio = document.querySelector('input[name="triggerType"][value="allTrigger"]');
if (allTriggerRadio) allTriggerRadio.checked = true;
// 3. UI 컴포넌트 갱신
updateColorFilterUI(); // 색상 버튼 스타일 초기화
renderSeriesFilters(); // 시리즈 버튼 스타일 초기화 (또는 initSeriesFilter)
// 4. 저장소 및 URL 동기화
localStorage.removeItem('selectedCards');
const url = new URL(window.location.href);
url.searchParams.delete('cards');
window.history.replaceState({}, '', url);
// 5. 전체 갱신
updateCounters();
updateCardList();
syncToStorageAndCounters(); // 버튼 활성화 상태까지 체크
// 6. 모달 닫기
const resetModal = document.getElementById('resetModal');
if (resetModal) resetModal.style.display = 'none';
}
document.addEventListener('DOMContentLoaded', () => {
init();
document.querySelectorAll('.color-filter-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const color = btn.dataset.color;
if (color === 'All') {
selectedColors.clear();
selectedColors.add('All');
} else {
selectedColors.delete('All');
selectedColors.has(color) ? selectedColors.delete(color) : selectedColors.add(color);
if (selectedColors.size === 0) selectedColors.add('All');
}
updateColorFilterUI();
updateCardList();
});
});
['searchQuery', 'includeBasic', 'includeAP', 'includeParallel'].forEach((id) => {
document.getElementById(id).addEventListener('input', updateCardList);
});
document.querySelectorAll('input[name="triggerType"]').forEach((r) => {
r.addEventListener('change', updateCardList);
});
document.getElementById('goToResultPage').addEventListener('click', () => {
if (this.disabled) return; // 비활성화 상태면 무시
// 현재 페이지에서 선택된(cardList에 있는) 카드만 필터링해서 전송
const currentSeriesSelected = cardList.filter(c => c.count > 0);
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}`;
});
const resetModal = document.getElementById('resetModal');
const resetBtn = document.getElementById('resetSelection');
const cancelBtn = document.getElementById('cancelReset');
const confirmBtn = document.getElementById('confirmReset');
// 1. 초기화 버튼 클릭 시 모달 표시
if (resetBtn) {
resetBtn.onclick = (e) => {
e.preventDefault();
resetModal.style.setProperty('display', 'flex', 'important');
};
}
// 2. 취소 버튼 클릭 시 모달 닫기
if (cancelBtn) {
cancelBtn.onclick = () => {
resetModal.style.display = 'none';
};
}
// 3. 실행 버튼 클릭 시 데이터 초기화 및 닫기
if (confirmBtn) {
confirmBtn.onclick = () => {
executeReset(); // 이전에 만든 데이터 초기화 함수 호출
resetModal.style.display = 'none';
};
}
// 4. 배경 클릭 시 닫기
resetModal.onclick = (e) => {
if (e.target === resetModal) {
resetModal.style.display = 'none';
}
};
});
// 공통 저장 및 카운터 업데이트 함수
function syncToStorageAndCounters() {
// 1. 전체 선택된 카드 추출
selectedCards = cardList.filter((c) => c.count > 0);
// 2. 현재 시리즈에 해당하는 카드가 있는지 확인 (활성화 조건)
const hasCardsInCurrentSeries = selectedCards.length > 0;
const resultBtn = document.getElementById('goToResultPage');
if (resultBtn) {
if (hasCardsInCurrentSeries) {
resultBtn.disabled = false;
resultBtn.classList.remove('opacity-50', 'cursor-not-allowed');
resultBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
} else {
resultBtn.disabled = true;
resultBtn.classList.add('opacity-50', 'cursor-not-allowed');
resultBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
}
}
// 3. 로컬 스토리지 저장 (현재 시리즈 데이터만 깨끗하게 유지)
const encoded = encodeURIComponent(JSON.stringify(selectedCards));
localStorage.setItem('selectedCards', encoded);
// 4. URL 업데이트 (새로고침 시 데이터 유지용)
const url = new URL(window.location.href);
url.searchParams.set('cards', encoded);
window.history.replaceState({}, '', url);
updateCounters(); // 하단 바 숫자만 업데이트
}
let selectedSeriesGroups = new Set(['All']);
function initSeriesFilter() {
const container = document.getElementById('seriesFilterContainer');
// 데이터에서 시리즈 목록 추출
const seriesGroups = [...new Set(cardList.map((c) => c.Number.split(/[/-]/)[0]))]; //
// 초기 HTML 생성 (All 버튼 포함)
container.innerHTML = `
<button data-series="All" class="series-btn px-3 py-1 rounded-full text-xs font-bold border transition-all ${selectedSeriesGroups.has('All') ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white border-slate-200 text-slate-500'}">
All
</button>
`;
seriesGroups.forEach((group) => {
const isSel = selectedSeriesGroups.has(group);
container.innerHTML += `
<button data-series="${group}" class="series-btn px-3 py-1 rounded-full text-xs font-bold border transition-all ${isSel ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-400'}">
${group}
</button>
`;
});
// 클릭 이벤트 핸들러
container.addEventListener('click', (e) => {
const btn = e.target.closest('.series-btn'); // 정확한 버튼 클릭 감지
if (!btn) return;
const series = btn.dataset.series;
if (series === 'All') {
selectedSeriesGroups.clear();
selectedSeriesGroups.add('All');
} else {
selectedSeriesGroups.delete('All');
if (selectedSeriesGroups.has(series)) {
selectedSeriesGroups.delete(series);
} else {
selectedSeriesGroups.add(series);
}
// 아무것도 선택 안된 경우 다시 All 선택
if (selectedSeriesGroups.size === 0) selectedSeriesGroups.add('All');
}
// 1. 모든 버튼 UI 일괄 업데이트
document.querySelectorAll('.series-btn').forEach((b) => {
const isSel = selectedSeriesGroups.has(b.dataset.series);
b.className = `series-btn px-3 py-1 rounded-full text-xs font-bold border transition-all ${isSel ? 'bg-blue-600 border-blue-600 text-white' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-400'}`;
});
// 2. 필터링된 목록 다시 그리기
updateCardList();
});
}
</script>
</body>
</html>

2767
datas/ua01.js Normal file

File diff suppressed because it is too large Load Diff

2911
datas/ua02.js Normal file

File diff suppressed because it is too large Load Diff

2749
datas/ua03.js Normal file

File diff suppressed because it is too large Load Diff

4030
datas/ua04.js Normal file

File diff suppressed because it is too large Load Diff

2871
datas/ua05.js Normal file

File diff suppressed because it is too large Load Diff

1618
datas/ua06.js Normal file

File diff suppressed because it is too large Load Diff

2730
datas/ua07.js Normal file

File diff suppressed because it is too large Load Diff

2819
datas/ua08.js Normal file

File diff suppressed because it is too large Load Diff

1640
datas/ua09.js Normal file

File diff suppressed because it is too large Load Diff

2728
datas/ua10.js Normal file

File diff suppressed because it is too large Load Diff

1689
datas/ua11.js Normal file

File diff suppressed because it is too large Load Diff

1759
datas/ua12.js Normal file

File diff suppressed because it is too large Load Diff

1608
datas/ua13.js Normal file

File diff suppressed because it is too large Load Diff

1658
datas/ua14.js Normal file

File diff suppressed because it is too large Load Diff

2954
datas/ua15.js Normal file

File diff suppressed because it is too large Load Diff

1708
datas/ua16.js Normal file

File diff suppressed because it is too large Load Diff

1648
datas/ua17.js Normal file

File diff suppressed because it is too large Load Diff

2707
datas/ua18.js Normal file

File diff suppressed because it is too large Load Diff

1710
datas/ua19.js Normal file

File diff suppressed because it is too large Load Diff

1688
datas/ua20.js Normal file

File diff suppressed because it is too large Load Diff

1726
datas/ua21.js Normal file

File diff suppressed because it is too large Load Diff

1177
datas/ua22.js Normal file

File diff suppressed because it is too large Load Diff

2777
datas/ua23.js Normal file

File diff suppressed because it is too large Load Diff

1655
datas/ua24.js Normal file

File diff suppressed because it is too large Load Diff

1196
datas/ua25.js Normal file

File diff suppressed because it is too large Load Diff

1317
datas/ua26.js Normal file

File diff suppressed because it is too large Load Diff

3160
datas/ua27.js Normal file

File diff suppressed because it is too large Load Diff

1226
datas/ua28.js Normal file

File diff suppressed because it is too large Load Diff

3109
datas/ua29.js Normal file

File diff suppressed because it is too large Load Diff

2859
datas/ua30.js Normal file

File diff suppressed because it is too large Load Diff

1699
datas/ua31.js Normal file

File diff suppressed because it is too large Load Diff

1178
datas/ua32.js Normal file

File diff suppressed because it is too large Load Diff

1276
datas/ua33.js Normal file

File diff suppressed because it is too large Load Diff

1187
datas/ua34.js Normal file

File diff suppressed because it is too large Load Diff

1716
datas/ua35.js Normal file

File diff suppressed because it is too large Load Diff

1748
datas/ua36.js Normal file

File diff suppressed because it is too large Load Diff

1688
datas/ua37.js Normal file

File diff suppressed because it is too large Load Diff

1238
datas/ua38.js Normal file

File diff suppressed because it is too large Load Diff

1217
datas/ua39.js Normal file

File diff suppressed because it is too large Load Diff

1688
datas/ua40.js Normal file

File diff suppressed because it is too large Load Diff

1698
datas/ua41.js Normal file

File diff suppressed because it is too large Load Diff

1778
datas/ua42.js Normal file

File diff suppressed because it is too large Load Diff

1198
datas/ua43.js Normal file

File diff suppressed because it is too large Load Diff

1709
datas/ua44.js Normal file

File diff suppressed because it is too large Load Diff

1397
datas/ua45.js Normal file

File diff suppressed because it is too large Load Diff

1348
datas/ua46.js Normal file

File diff suppressed because it is too large Load Diff

1779
datas/ua47.js Normal file

File diff suppressed because it is too large Load Diff

1749
datas/ua48.js Normal file

File diff suppressed because it is too large Load Diff

1297
datas/ua49.js Normal file

File diff suppressed because it is too large Load Diff

1007
datas/ua99 sample mini.js Normal file

File diff suppressed because it is too large Load Diff

1318
datas/ua99 sample.js Normal file

File diff suppressed because it is too large Load Diff

54
datas/uapr.js Normal file
View File

@@ -0,0 +1,54 @@
const uaprData = [
{
imgSrc: '/uapr/UAPR_2023-AP01.png',
Number: 'UAPR/2023-AP01',
count: 0,
key: false,
color: 'AP',
parallel: false,
rarity: '',
trigger: null,
},
{
imgSrc: '/uapr/UAPR_2023-AP02.png',
Number: 'UAPR/2023-AP02',
count: 0,
key: false,
color: 'AP',
parallel: false,
rarity: '',
trigger: null,
},
{
imgSrc: '/uapr/UAPR_2023-AP03.png',
Number: 'UAPR/2023-AP03',
count: 0,
key: false,
color: 'AP',
parallel: false,
rarity: '',
trigger: null,
},
{
imgSrc: '/uapr/UAPR_2023-AP04.png',
Number: 'UAPR/2023-AP04',
count: 0,
key: false,
color: 'AP',
parallel: false,
rarity: '',
trigger: null,
},
{
imgSrc: '/uapr/UAPR_2023-AP05.png',
Number: 'UAPR/2023-AP05',
count: 0,
key: false,
color: 'AP',
parallel: false,
rarity: '',
trigger: null,
},
];
export default uaprData;

43
docs/convention.md Normal file
View File

@@ -0,0 +1,43 @@
# 코딩 컨벤션
## 기본 원칙
- 모든 문서와 커뮤니케이션은 한국어 기준으로 유지한다.
- 코드 주석은 필요 시 `JSDoc` 형식을 우선한다.
- 기존 정적 사이트 구조를 존중하고, 대규모 구조 변경 시에는 먼저 `docs/history.md`에 의사결정을 기록한다.
## 파일 및 폴더 규칙
- 시리즈 데이터 파일은 `datas/uaXX.js`, `datas/uapr.js`처럼 시리즈 키와 일치하게 관리한다.
- 이미지 폴더는 `images/<seriesKey>/` 규칙을 유지한다.
- 새 문서는 `docs/` 하위에서 목적별 단일 파일에 누적 관리한다.
## JavaScript 규칙
- 기존 코드 스타일에 맞춰 기본 들여쓰기는 4칸을 사용한다.
- 상수는 `const`, 재할당 변수는 `let`을 사용한다.
- DOM 참조가 반복될 경우 `getEl()`처럼 묶어서 관리하는 패턴을 우선 고려한다.
- URL 파라미터, `localStorage`, 동적 import는 예외 처리와 함께 사용한다.
- 카드 데이터 객체의 필드명은 기존 호환성을 위해 현재 형태(`imgSrc`, `Number`, `count`, `key`)를 유지한다.
## HTML 규칙
- 정적 페이지는 의미 단위별로 `header`, `main`, `aside` 등을 우선 사용한다.
- 페이지별 스크립트가 길어질 경우 신규 기능은 별도 JS 파일 분리 여부를 먼저 검토한다.
- 인라인 이벤트(`onclick`)는 기존 파일에 이미 존재하므로 즉시 제거 대상은 아니지만, 신규 대형 기능은 `addEventListener` 방식 우선 검토한다.
## CSS 규칙
- 공통 UI는 가능한 한 Tailwind 유틸리티 클래스를 우선 사용한다.
- 페이지 특화 보정 스타일만 인라인 `<style>` 또는 전용 CSS 파일에 둔다.
- 캡처 안정성처럼 이유가 분명한 예외 스타일은 주석으로 목적을 남긴다.
## 데이터 관리 규칙
- 카드 데이터 추가 시 최소 필드는 `imgSrc`, `Number`, `count`, `key`, `color`, `parallel`, `rarity`, `trigger`를 맞춘다.
- 동일 카드의 패러렐 판본은 동일 `Number`를 유지하고 `parallel` 값으로 구분한다.
- 파일명 변경 시 `datas/*.js``images/<seriesKey>/`가 함께 일치하는지 확인한다.
## Git 규칙
- 커밋 메시지는 한국어로 작성한다.
- 버전 상승 작업은 `docs/update.md` 반영 후 태그명과 맞춘다.
- 민감 정보, 로컬 경로, 개인 식별 정보가 커밋되지 않도록 확인한다.
## 이미지 자산 규칙
- `images/**``Git LFS`로 관리한다.
- 이미지 파일을 새로 추가하거나 교체할 때는 일반 Git 객체로 들어가지 않도록 `.gitattributes` 설정을 유지한다.
- 대량 이미지 추가 전에는 원격 저장소의 LFS 정책과 비용 한도를 확인한다.

44
docs/history.md Normal file
View File

@@ -0,0 +1,44 @@
# 의사결정 이력
## 2026-03-30 / 문서 체계 초기화
- 배경: 프로젝트 루트에 AI 작업 규칙은 있었지만 `docs/` 실체가 없어 규칙과 실제 운영 상태가 분리되어 있었다.
- 결정: 규칙 문서가 요구하는 핵심 문서 6종을 먼저 생성하고, 이후 변경 시 이 파일들을 기준 문서로 유지한다.
- 기대 효과: 작업 내역, 기술 구조, 화면-파일 연결, 향후 할 일을 한곳에서 추적할 수 있다.
## 2026-03-30 / 이미지 관리 방식 1차 판단
- 배경: `images/` 디렉터리 용량이 약 `1.7G`, 파일 수가 `9,370개` 수준으로 증가했다.
- 관찰:
- 개별 파일이 아주 크지 않아도 Git 일반 객체로 누적되면 clone 및 fetch 비용이 계속 증가한다.
- 이미지 추가가 잦은 프로젝트 특성상 저장소 히스토리 비대화가 반복될 가능성이 높다.
- 결정:
- 신규 이미지 유입 관리 목적이라면 `Git LFS` 도입을 권장한다.
- 단, 이미 커진 저장소를 즉시 가볍게 만드는 해결책으로 `Git LFS`만 기대해서는 안 된다.
- 히스토리 크기까지 줄여야 하면 `git lfs migrate` 또는 자산 저장소 분리/CDN 이전을 별도 의사결정으로 다룬다.
- 보류 사유:
- 원격 저장소의 LFS 지원 여부, 무료 용량, 대역폭 정책을 아직 확인하지 않았다.
- 기존 협업자들의 개발 환경에 `git lfs` 설치를 요구하게 되므로 도입 공지가 필요하다.
## 2026-03-30 / 이미지 히스토리 전체 LFS 마이그레이션 결정
- 배경: 목적이 "앞으로 예방"이 아니라 "기존 저장소 용량 절감"으로 명확해졌다.
- 확인:
- 로컬 환경에 `git lfs`가 설치되어 있다.
- 원격 저장소에 LFS 엔드포인트가 설정되어 있다.
- 결정:
- `images/**` 전체 히스토리를 `Git LFS`로 마이그레이션한다.
- 현재 작업 중인 `ua27` 데이터 및 이미지 추가분도 함께 반영한다.
- 영향:
- 기존 커밋 해시가 모두 재작성된다.
- 원격 반영 시 강제 푸시가 필요하다.
- 협업 중인 다른 클론은 재동기화 절차가 필요하다.
## 2026-03-30 / 새 루트 커밋 기준 저장소 재시작 결정
- 배경: `images/**`는 LFS 포인터로 전환됐지만, 저장소 자체는 과거 Git 객체 영향으로 여전히 비대했다.
- 결정:
- 현재 로컬 작업본을 단일 기준 스냅샷으로 보고 새 루트 커밋을 만든다.
- 원격 `main`은 새 루트 커밋으로 교체한다.
- 기존 히스토리는 로컬 백업 브랜치에서만 보존한다.
- 새 버전 체계는 `v3.0.0`부터 다시 시작한다.
- 이미지 자산은 새 시작 이후에도 `Git LFS` 정책을 유지한다.
- 이유:
- 가장 확실하게 Git 일반 히스토리 용량을 줄일 수 있다.
- 추가적인 서버 GC 상태나 숨은 과거 객체를 더 추적하지 않고도 정리 목적을 달성할 수 있다.

50
docs/map.md Normal file
View File

@@ -0,0 +1,50 @@
# 파일-화면 매핑 가이드
## `/`
- 진입 파일: `index.html`
- 화면 역할: 시리즈 선택 홈 화면
- 사용자 행동:
- 언어 선택
- 시리즈 카드 클릭
- 연결 파일:
- `i18n/translations.<lang>.json`: 제목, 설명, 시리즈명
- `script/navigation.js`: 공통 네비게이션 오버레이
- `style/output.css`: 공통 스타일
## `/cardList.html?series=<seriesKey>`
- 진입 파일: `cardList.html`
- 화면 역할: 카드 검색, 필터링, 수량 선택, 키 카드 지정
- 사용자 행동:
- 검색어 입력
- 색상/타입/트리거 필터
- 카드 수량 조절
- 키 카드 지정
- 덱 결과 페이지 이동
- 연결 파일:
- `datas/<seriesKey>.js`: 현재 시리즈 카드 원본 데이터
- `images/<seriesKey>/`: 카드 이미지 원본
- `i18n/translations.<lang>.json`: 버튼/문구 번역
- `script/navigation.js`: 공통 네비게이션
## `/result.html`
- 진입 파일: `result.html`
- 화면 역할: 선택한 덱 미리보기 및 이미지 저장
- 사용자 행동:
- 덱 이름 입력
- 출력 스타일 선택
- 고화질 저장 토글
- PNG 다운로드
- 연결 파일:
- `localStorage.selectedCards` 또는 `cards` 쿼리스트링: 덱 데이터 소스
- `images/<seriesKey>/`: 결과 이미지 렌더링 대상
- `i18n/translations.<lang>.json`: 결과 화면 번역
## 공통 보조 파일
- `script/navigation.js`: 오버레이 메뉴 생성 및 이동 처리
- `i18n/i18n.js`: `data-i18n` 기반 공통 번역 적용
- `style/style.css`, `style/navigation.css`, `style/output.css`: 공통/페이지 스타일
## 초보자용 이해 포인트
- 이 프로젝트는 프레임워크 앱이 아니라 HTML 파일 3개가 직접 화면 역할을 나눠 갖는 구조다.
- 카드 정보는 `datas/`에, 실제 카드 이미지는 `images/`에 따로 있다.
- 화면에서 보이는 한 장의 카드는 "데이터 파일의 객체 1개 + 이미지 폴더의 파일 1개" 조합으로 만들어진다.

101
docs/spec.md Normal file
View File

@@ -0,0 +1,101 @@
# 기술 명세
## 1. 프로젝트 개요
- 프로젝트명: `UADECK V3.1 - Union Arena Deck Builder`
- 형태: 정적 HTML/CSS/JavaScript 기반 덱 빌더
- 패키지 의존성: `tailwindcss`, `@tailwindcss/cli`
- 빌드 산출 스타일: `style/output.css`
## 2. 화면 구조
### `/index.html`
- 시리즈 선택 랜딩 페이지다.
- `i18n/translations.<lang>.json`을 직접 fetch하여 시리즈명과 소개 문구를 렌더링한다.
- 사용자가 시리즈를 선택하면 `cardList.html?series=<key>&seriesName=<name>&lang=<lang>`로 이동한다.
### `/cardList.html`
- 특정 시리즈의 카드 목록을 보여주고 덱을 편집한다.
- 쿼리스트링의 `series` 값을 기준으로 `./datas/${seriesKey}.js`를 동적 import 한다.
- 필터 기능:
- 검색어
- 색상(`All`, `Red`, `Blue`, `Green`, `Yellow`, `Purple`)
- 시리즈 하위 그룹
- 타입(`Basic`, `AP`, `Parallel`)
- 트리거(`All`, `Special`, `Color`, `Final`)
- 카드 수량 증감, 키 카드 지정, 초기화, 상세 오버레이, 결과 페이지 이동을 담당한다.
### `/result.html`
- 선택한 카드 목록을 미리보기하고 이미지로 저장한다.
- 스타일 모드:
- `type1`: 키 카드 강조형
- `type2`: 클래식 5열형
- `html-to-image` CDN을 사용해 덱 이미지를 PNG로 저장한다.
## 3. 데이터 구조
### 카드 데이터 파일
- 위치: `datas/*.js`
- 파일 수: 현재 `52개`
- 각 파일은 `export const cardList = [...]` 형태를 사용한다.
### 카드 객체 스키마
```js
{
imgSrc: '/ua24/UA24BT_SHY-1-001.png',
Number: 'UA24BT/SHY-1-001',
count: 0,
key: false,
color: 'Red',
parallel: false,
rarity: 'C',
trigger: null,
}
```
### 필드 의미
- `imgSrc`: 원본 상대 식별자. 실제 렌더링 시 `images/${seriesKey}/${fileName}` 형태로 재구성한다.
- `Number`: 카드 번호. 동일 카드 판본 묶음과 필터 기준으로 사용한다.
- `count`: 현재 덱에 담긴 수량.
- `key`: 키 카드 여부.
- `color`: 카드 색상 또는 일부 특수 분류 값.
- `parallel`: 패러렐 여부.
- `rarity`: 희귀도 표기.
- `trigger`: `null`, `COLOR`, `FINAL`, `SPECIAL` 등 트리거 정보.
- `maxCount`: 일부 카드만 선택적으로 사용하며 기본값은 `4`로 동작한다.
## 4. 이미지 규칙
- 기본 위치: `images/<seriesKey>/`
- 저장 정책: `images/**``Git LFS` 대상으로 관리한다.
- 렌더링 규칙:
- 카드 목록 화면: `images/${seriesKey}/${fileName}`
- 결과 화면: `/images/${seriesKey}/${fileName}`
- 폴백 이미지: `/images/uapr/comingsoon.png`
- 현재 용량 현황:
- `images/``1.7G`
- 이미지 파일 약 `9,370개`
## 5. 다국어
- 지원 언어: `ko`, `ja`, `en`
- 번역 파일:
- `i18n/translations.ko.json`
- `i18n/translations.ja.json`
- `i18n/translations.en.json`
- `index.html`은 번역 JSON을 직접 읽는다.
- 공통 텍스트 치환은 `i18n/i18n.js``data-i18n` 기반 로직을 사용한다.
## 6. 상태 저장
- `localStorage.lang`: 현재 언어
- `localStorage.selectedCards`: 선택 카드 목록(JSON 문자열을 `encodeURIComponent` 처리)
- `localStorage.selectedSeries`: 일부 흐름에서 사용하는 선택 시리즈 값
## 7. 스타일 및 자산
- Tailwind 유틸리티 클래스와 페이지별 인라인 스타일을 혼용한다.
- 공통 스타일 파일:
- `style/output.css`
- `style/style.css`
- `style/navigation.css`
## 8. 알려진 제약
- `cardList.html``result.html`의 스크립트가 페이지 내부에 크게 포함되어 있어 모듈 분리 전까지는 변경 영향 범위가 넓다.
- `script/navigation.js`의 일부 이동 대상 페이지는 현재 저장소에 존재하지 않는다.
- 이미지가 Git 일반 객체로 계속 누적되면 저장소 히스토리 크기가 지속 증가한다.

19
docs/todo.md Normal file
View File

@@ -0,0 +1,19 @@
# 할 일 및 이슈
## 현재 이슈
- `images/` 디렉터리가 약 `1.7G`까지 증가해 저장소 clone, fetch, checkout 비용이 커졌다.
- 이미지 파일 수가 `9,000+` 단위라서 카드 추가 빈도가 올라갈수록 Git 객체 수와 히스토리 부담이 계속 누적될 가능성이 높다.
- `script/navigation.js`에는 현재 프로젝트에 없는 페이지(`deckHistory.html`, `preset.html`, `myDatabase.html`)로 이동하는 코드가 남아 있다.
- `cardList.html``result.html` 내부 스크립트 비중이 커서 기능 확장 시 유지보수 난도가 올라갈 수 있다.
- 데이터 파일 네이밍과 샘플 파일(`datas/ua99 sample.js`, `datas/ua99 sample mini.js`) 처리 기준이 문서상 명확하지 않았다.
## 다음 작업 제안
- 원격 저장소에서 `Git LFS` 업로드가 정상 완료되었는지 확인한다.
- 협업자가 있다면 히스토리 재작성 이후 동기화 방법을 공지한다.
- `Git LFS` 도입 후에도 저장소 운영 비용이 크면 이미지 저장소 분리/CDN 이전을 재검토한다.
- `cardList.html`의 인라인 스크립트를 별도 JS 모듈로 분리하는 리팩터링을 검토한다.
- 존재하지 않는 페이지 링크를 정리하거나, 향후 구현 예정이면 문서에 상태를 명확히 표기한다.
## 판단 가이드
- 신규 이미지 추가 관리가 목적이면 `Git LFS` 추적만으로도 충분하다.
- 기존 clone 크기까지 줄이는 것이 목적이면 `Git LFS` 히스토리 마이그레이션 또는 자산 저장소 분리가 필요하다.

40
docs/update.md Normal file
View File

@@ -0,0 +1,40 @@
# 업데이트 이력
## 2026-03-30 / docs-bootstrap-1
### 수행 작업
- 프로젝트 루트의 `.ai-rules.md``.cursorrules`를 검토하여 문서 운영 규칙을 확인했다.
- 부재하던 `docs/` 디렉터리를 생성하고 문서 기본 세트(`update.md`, `todo.md`, `spec.md`, `convention.md`, `history.md`, `map.md`)를 초기 구축했다.
- 정적 사이트 구조를 기준으로 주요 페이지(`index.html`, `cardList.html`, `result.html`)와 보조 스크립트(`script/navigation.js`, `i18n/i18n.js`)의 역할을 문서화했다.
- 카드 데이터셋 구조(`datas/*.js`)와 이미지 경로 규칙(`images/<seriesKey>/<fileName>`)을 정리했다.
- 이미지 저장소 현황을 점검했다.
- 점검 결과: `images/` 디렉터리 약 `1.7G`, 이미지 파일 약 `9,370개`, 데이터 파일 `52개`.
- 대용량 이미지 자산 관리를 위한 `Git LFS` 도입 검토 사항과 주의점을 문서에 반영했다.
### 메모
- 현재 저장소에는 `docs/`가 없었기 때문에 이번 작업은 문서 체계 초기 세팅 성격이 강하다.
- `Git LFS`는 신규 이미지 유입 관리에는 유효하지만, 저장소 자체를 즉시 가볍게 만들지는 않으므로 히스토리 정리 전략을 별도 검토해야 한다.
## 2026-03-30 / lfs-migration-1
### 수행 작업
- `images/**` 전체를 `Git LFS` 대상으로 관리하기 위해 `.gitattributes`를 추가했다.
- 기존 저장소 용량 절감을 목표로 `Git LFS` 히스토리 마이그레이션을 수행하는 방향으로 결정했다.
- 현재 변경분을 포함한 뒤 전체 히스토리를 재작성하고 강제 푸시하는 절차를 진행한다.
### 메모
- 이번 작업은 단순 추적 설정이 아니라 과거 커밋까지 다시 쓰는 작업이므로 커밋 해시가 변경된다.
- 원격 반영 시 일반 푸시가 아닌 `force-with-lease`가 필요하다.
- 마이그레이션 체크아웃 이후 `ua27`의 추가 이미지와 데이터 보완분이 확인되어 최종 반영 대상에 포함했다.
## 2026-03-30 / repo-reset-1
### 수행 작업
- 저장소 용량을 근본적으로 단순화하기 위해 현재 로컬 작업본을 기준으로 새 루트 커밋 하나만 남기는 방향으로 전환했다.
- 기존 Git 히스토리는 원격 `main`에서는 제거하고, 로컬 백업 브랜치로만 보존하는 방식으로 진행한다.
- `images/**`는 계속 `Git LFS` 대상으로 유지한다.
- 새 시작 버전은 `v3.0.0`으로 정하고, 기존 `1.x`, `2.x` 버전 이력은 새 원격 기준에서 계승하지 않기로 했다.
### 메모
- 이 작업 후 원격 저장소는 사실상 "현재 스냅샷 기반 새 저장소"처럼 동작한다.
- 과거 커밋 이력, 기존 태그, 옛 해시는 원격 기준으로 더 이상 사용하지 않게 된다.

BIN
favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

60
i18n/i18n.js Normal file
View File

@@ -0,0 +1,60 @@
// 계층구조 사용시
async function loadTranslations(lang) {
try {
const response = await fetch(`/i18n/translations.${lang}.json`);
if (!response.ok) {
throw new Error(`Failed to load translations for ${lang}`);
}
return await response.json();
} catch (error) {
console.error(error);
return {};
}
}
async function localizePage(lang) {
const translations = await loadTranslations(lang);
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
const key = element.dataset.i18n;
const localizedText = findNestedValue(translations, key);
if (localizedText) {
element.textContent = localizedText;
}
});
}
function findNestedValue(obj, key) {
if (obj.hasOwnProperty(key)) {
return obj[key];
}
for (const prop in obj) {
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
const result = findNestedValue(obj[prop], key);
if (result) {
return result;
}
}
}
return null;
}
document.addEventListener('DOMContentLoaded', async () => {
const storedLang = localStorage.getItem('lang');
if (storedLang) {
await localizePage(storedLang);
return;
}
const urlParams = new URLSearchParams(window.location.search);
const queryLang = urlParams.get('lang');
const lang = queryLang || 'ko';
localStorage.setItem('lang', lang);
await localizePage(lang);
});
// 계층구조 미사용시 코드는 주석 처리된 상태로 유지

130
i18n/translations.en.json Normal file
View File

@@ -0,0 +1,130 @@
{
"mainTitle": "Master Your Strategy",
"mainDesc": "Explore all Union Arena series at a glance and<br class='hidden sm:block' /> build the ultimate deck that fits your strategy.",
"back": "← BACK",
"searchResults": "Search Results",
"resetSelection": "Reset Selection",
"searchPlaceholder": "Enter card number",
"color": "Color",
"seriesFilter": "Series Filter",
"type": "Type",
"trigger": "Trigger",
"noSearchResults": "No cards found.",
"noSearchResultsDesc": "Try adjusting the filters or search terms.",
"makeDeckList": "CREATE DECK LIST",
"resetModalTitle": "Reset Selection",
"resetModalDesc": "All selected cards will be removed from the list.<br />Are you sure you want to reset?",
"cancel": "Cancel",
"confirm": "Reset Now",
"editDeck": "← EDIT DECK",
"deckPreview": "Deck Preview",
"deckNameLabel": "Deck Name",
"deckNamePlaceholder": "Series name used if empty",
"exportStyle": "Export Style",
"type1Title": "Type 1: Signature",
"type1Desc": "Standard style highlighting Key Cards",
"type2Title": "Type 2: Classic",
"type2Desc": "Even 5-column grid layout",
"highQuality": "High Quality",
"hqDesc": "2x size, 4x sharpness",
"exportNotice1": "* If image fails to generate, please check 'Pop-up' settings.",
"exportNotice2": "* On iOS, you may need to long-press the image to save.",
"generatingImage": "Generating Image...",
"downloadImage": "DOWNLOAD IMAGE",
"keyCardLabel": "KEY CARD",
"back": "BACK",
"saveError": "Failed to save image",
"all": "All",
"metaDeck": "Meta Deck",
"titlePrompt": "Please select a title",
"selectTitle": "Select Title",
"deckName": "Deck Name",
"deckNamePlaceHolder": "Please enter the name",
"myDeckHistory": "My Deck History",
"notHistory": "No saved history",
"preview": "Preview",
"back": "Back",
"close": "Close",
"reset": "Reset",
"dataBackup": "Data Backup",
"dataRecovery": "Data Recovery",
"saveAsImage": "Save as Image",
"selectDeckTemplate": "Select Deck Template",
"nowSaving": "Now Saving ...",
"search": "Search",
"searchPlaceHolder": "ex) 008 ...",
"noSearchResults": "No search results.",
"scrollingNotice": "This site is optimized for PC (Chrome browser).",
"cardTypeTitle": "Cards Type",
"includeParallel": "Show parallel cards",
"includeActionPoint": "Show AP Cards",
"onlyParallel": "Show only parallel cards",
"onlyActionPoint": "Show only AP Cards",
"triggerTypeTitle": "Trigger Type",
"includeAllTrigger": "ALL",
"includeColorTrigger": "COLOR",
"includeFinalTrigger": "FINAL",
"includeSpecialTrigger": "SPECIAL",
"resetCountConfirm": "Anything written to this set will be deleted. Do you still want to reset it?",
"SearchResults": "Search Results",
"SearchResultsCounter": "Cards",
"limitOverAlert": "There are cards exceeding the quantity limit. Should we proceed as is?",
"highQualityTitle": "Save in high definition",
"highQualityDesc": "※ Caution: Quality is doubled, file size is quadrupled!",
"previewMessage1": "Preview is not supported on mobile.",
"previewMessage2": "After saving the image, please check.",
"series": {
"ua01": "Code Geass: Lelouch of the Rebellion",
"ua02": "Jujutsu Kaisen",
"ua03": "HUNTER×HUNTER",
"ua04": "THE iDOLM@STER SHINY COLORS",
"ua05": "Demon Slayer: Kimetsu no Yaiba",
"ua06": "Tales of ARISE",
"ua07": "That Time I Got Reincarnated as a Slime",
"ua08": "Bleach: Thousand-Year Blood War",
"ua09": "Me & Roboco",
"ua10": "My Hero Academia",
"ua11": "Gintama",
"ua12": "Blue Lock",
"ua13": "TEKKEN 7",
"ua14": "Dr.STONE",
"ua15": "Sword Art Online",
"ua16": "SYNDUALITY Noir",
"ua17": "Toriko",
"ua18": "GODDESS OF VICTORYNIKKE",
"ua19": "Haikyu!!",
"ua20": "BLACK CLOVER",
"ua21": "Yu Yu Hakusho",
"ua22": "GAMERA -Rebirth-",
"ua23": "Attack on Titan",
"ua24": "SHY",
"ua25": "Undead Unluck",
"ua26": "The 100 Girlfriends Who Really, Really, Really, Really Love You",
"ua27": "Gakuen iDOLM@STER",
"ua28": "Kaiju No.8",
"ua29": "Kamen Rider",
"ua30": "Arknights",
"ua31": "Puella Magi Madoka Magica",
"ua32": "Shangri-La Frontier",
"ua33": "2.5 Dimensional Seduction",
"ua34": "CODE GEASS: Roze of the Recapture",
"ua35": "ONE-PUNCH MAN",
"ua36": "Macross",
"ua37": "Fullmetal Alchemist",
"ua38": "WIND BREAKER",
"ua39": "ULTIMATE MUSCLE",
"ua40": "Re:ZERO -Starting Life in Another World-",
"ua41": "Rurouni Kenshin: Meiji Kenkaku Romantan",
"ua42": "Monogatari Series",
"ua43": "SAKAMOTO DAYS",
"ua44": "Rebuild of Evangelion",
"ua45": "To Love Ru",
"ua46": "Kagurabachi",
"ua47": "Tokyo Ghoul",
"ua48": "Kingdom",
"ua49": "Chained Soldier"
},
"uapr": "UNION ARENA"
}

146
i18n/translations.ja.json Normal file
View File

@@ -0,0 +1,146 @@
{
"mainTitle": "Master Your Strategy",
"mainDesc": "ユニオンアリーナの全シリーズを一目で確認し、<br class='hidden sm:block' /> あなたの戦略に最適なデッキを構築してみてください。",
"back": "← 戻る",
"searchResults": "検索結果",
"resetSelection": "選択リセット",
"searchPlaceholder": "カード番号を入力",
"color": "カラー",
"seriesFilter": "シリーズフィルター",
"type": "タイプ",
"trigger": "トリガー",
"noSearchResults": "該当するカードがありません。",
"noSearchResultsDesc": "フィルターを変更するか、検索ワードを確認してください。",
"makeDeckList": "デッキリスト作成",
"resetModalTitle": "選択リセット",
"resetModalDesc": "現在選択されているすべてのカードが削除されます。<br />本当にリセットしますか?",
"cancel": "キャンセル",
"confirm": "リセット実行",
"editDeck": "← デッキを編集",
"deckPreview": "デッキプレビュー",
"deckNameLabel": "デッキ名",
"deckNamePlaceholder": "未入力時はシリーズ名を使用",
"exportStyle": "書き出しスタイル",
"type1Title": "タイプ 1: Signature",
"type1Desc": "キーカードを強調する基本スタイル",
"type2Title": "タイプ 2: Classic",
"type2Desc": "5列均等配置スタイル",
"highQuality": "高画質モード",
"hqDesc": "容量2倍、鮮明度4倍アップ",
"exportNotice1": "* 画像が生成されない場合は「ポップアップ許可」を確認してください。",
"exportNotice2": "* iOS環境では画像を長押しして保存する必要がある場合があります。",
"generatingImage": "画像生成中...",
"downloadImage": "画像を保存する",
"keyCardLabel": "KEY CARD",
"back": "戻る",
"saveError": "画像の保存に失敗しました",
"all": "全て",
"metaDeck": "メタデッキ",
"titlePrompt": "タイトルを選択してください",
"selectTitle": "タイトルを選択",
"deckName": "デッキ名",
"deckNamePlaceHolder": "名前を入力してください",
"myDeckHistory": "私のデッキ記録",
"notHistory": "保存された記録はありません",
"close": "CLOSE",
"reset": "リセット",
"dataBackup": "データバックアップ",
"dataRecovery": "データ復元",
"selectDeckTemplate": "Select Deck Template",
"nowSaving": "保存中...",
"search": "検索",
"searchPlaceHolder": "ex) 008 ...",
"noSearchResults": "検索結果がありません。",
"scrollingNotice": "このサイトはPCクロームブラウザに最適化されています。",
"cardTypeTitle": "カード種類",
"onlyParallel": "パラレルカードのみを表示",
"onlyActionPoint": "APカードのみを表示",
"triggerTypeTitle": "トリガー効果",
"includeAllTrigger": "ALL",
"includeColorTrigger": "COLOR",
"includeFinalTrigger": "FINAL",
"includeSpecialTrigger": "SPECIAL",
"resetCountConfirm": "このセットに記録されている内容はすべて削除されます。\nそれでもリセットしますか",
"limitOverAlert": "制限数を超えるカードがあります。このまま続行しますか?",
"highQualityTitle": "高画質で保存する",
"highQualityDesc": "※ 注意: 品質は2倍、容量は4倍です",
"previewMessage1": "モバイルでのプレビューはサポートされていません。",
"previewMessage2": "画像を保存した後、確認してください。",
"series": {
"ua01": "コードギアス 反逆のルルーシュ",
"ua02": "呪術廻戦",
"ua03": "HUNTER×HUNTER",
"ua04": "アイドルマスター シャイニーカラーズ",
"ua05": "鬼滅の刃",
"ua06": "Tales of ARISE",
"ua07": "転生したらスライムだった件",
"ua08": "BLEACH 千年血戦篇",
"ua09": "僕とロボコ",
"ua10": "僕のヒーローアカデミア",
"ua11": "銀魂",
"ua12": "ブルーロック",
"ua13": "鉄拳7",
"ua14": "Dr.STONE",
"ua15": "ソードアート・オンライン",
"ua16": "SYNDUALITY Noir",
"ua17": "トリコ",
"ua18": "勝利の女神NIKKE",
"ua19": "ハイキュー‼",
"ua20": "ブラッククローバー",
"ua21": "幽☆遊☆白書",
"ua22": "GAMERA -Rebirth-",
"ua23": "進撃の巨人",
"ua24": "SHY",
"ua25": "アンデッドアンラック",
"ua26": "君のことが大大大大大好きな100人の彼女",
"ua27": "学園アイドルマスター",
"ua28": "怪獣8号",
"ua29": "仮面ライダー",
"ua30": "アークナイツ",
"ua31": "魔法少女まどか☆マギカ",
"ua32": "シャングリラ・フロンティア",
"ua33": "2.5次元の誘惑",
"ua34": "コードギアス 奪還のロゼ",
"ua35": "ワンパンマン",
"ua36": "マクロス",
"ua37": "鋼の錬金術師",
"ua38": "WIND BREAKER",
"ua39": "キン肉マン",
"ua40": "Re:ゼロから始める異世界生活",
"ua41": "るろうに剣心 -明治剣客浪漫譚-",
"ua42": "〈物語〉シリーズ",
"ua43": "SAKAMOTO DAYS",
"ua44": "ヱヴァンゲリヲン新劇場版",
"ua45": "To LOVEる-とらぶる-",
"ua46": "カグラバチ",
"ua47": "東京喰種トーキョーグール",
"ua48": "キングダム",
"ua49": "魔都精兵のスレイブ"
},
"uapr": "UNION ARENA",
"CardList": {
"ResultsCounter": {
"SearchResults": "検索結果",
"SearchResultsCounter": "件"
},
"CardType": {
"includeBasic": "기본 카드",
"includeParallel": "パラレルカード",
"includeActionPoint": "APカード"
},
"TriggerType": {
"includeAllTrigger": "모든 트리거",
"includeColorTrigger": "COLOR",
"includeSpecialTrigger": "SPECIAL",
"includeFinalTrigger": "FINAL"
},
"makeDeckList": "덱 리스트 만들기"
},
"Result": {
"preview": "プレビュー",
"back": "戻る",
"saveAsImage": "画像として保存"
}
}

142
i18n/translations.ko.json Normal file
View File

@@ -0,0 +1,142 @@
{
"mainTitle": "Master Your Strategy",
"mainDesc": "유니온 아레나의 모든 시리즈를 한눈에 확인하고,<br class='hidden sm:block' /> 당신의 전략에 맞는 최고의 덱을 구축해 보세요.",
"back": "← 뒤로 가기",
"searchResults": "검색 결과",
"resetSelection": "선택 초기화",
"searchPlaceholder": "카드 번호 입력",
"color": "색상",
"seriesFilter": "시리즈 필터",
"type": "유형",
"trigger": "트리거",
"noSearchResults": "조건에 맞는 카드가 없습니다.",
"noSearchResultsDesc": "필터를 변경하거나 검색어를 확인해 주세요.",
"makeDeckList": "덱 리스트 생성",
"resetModalTitle": "선택 초기화",
"resetModalDesc": "선택한 모든 카드가 목록에서 제거됩니다.<br />정말 초기화하시겠습니까?",
"cancel": "취소",
"confirm": "초기화 실행",
"editDeck": "← EDIT DECK",
"deckPreview": "Deck Preview",
"deckNameLabel": "Deck Name",
"deckNamePlaceholder": "미입력 시 시리즈명 사용",
"exportStyle": "Export Style",
"type1Title": "Type 1: Signature",
"type1Desc": "키 카드를 강조하는 기본 스타일",
"type2Title": "Type 2: Classic",
"type2Desc": "5열 균등 배치 스타일",
"highQuality": "High Quality",
"hqDesc": "용량 2배, 선명도 4배 증가",
"exportNotice1": "* 이미지가 생성되지 않을 경우 브라우저의 '팝업 허용'을 확인해주세요.",
"exportNotice2": "* iOS 환경에서는 이미지를 길게 눌러 저장해야 할 수도 있습니다.",
"generatingImage": "이미지 생성 중...",
"downloadImage": "DOWNLOAD IMAGE",
"keyCardLabel": "KEY CARD",
"back": "BACK",
"saveError": "이미지 저장 실패",
"all": "전체",
"metaDeck": "메타 덱",
"titlePrompt": "작품을 선택해주세요",
"selectTitle": "작품 선택",
"deckName": "덱 이름",
"deckNamePlaceHolder": "이름을 입력해주세요",
"myDeckHistory": "나의 덱 기록",
"notHistory": "저장된 기록이 없습니다",
"close": "닫기",
"reset": "초기화",
"dataBackup": "데이터 백업",
"dataRecovery": "데이터 복구",
"selectDeckTemplate": "덱 템플릿 선택",
"nowSaving": "이미지로 저장 중 ...",
"search": "검색",
"searchPlaceHolder": "예) 008 ...",
"noSearchResults": "검색 결과가 없습니다.",
"scrollingNotice": "본 사이트는 PC(크롬 브라우저)에 최적화 되어있습니다.",
"cardTypeTitle": "카드 종류",
"onlyParallel": "패러렐 카드만 표시",
"onlyActionPoint": "액션 포인트 카드만 표시",
"triggerTypeTitle": "트리거 효과",
"resetCountConfirm": "이 세트에 기록된 모든 내용은 삭제됩니다. 그래도 초기화하시겠습니까?",
"limitOverAlert": "제한 수량을 초과하는 카드가 있습니다. 이대로 계속 진행할까요?",
"highQualityTitle": "고화질로 저장하기",
"highQualityDesc": "※ 주의: 품질은 2배, 용량은 4배!",
"previewMessage1": "모바일에서는 미리보기를 지원하지 않습니다.",
"previewMessage2": "이미지를 저장한 후 확인해주세요.",
"series": {
"ua01": "코드 기어스: 반역의 를르슈",
"ua02": "주술회전",
"ua03": "HUNTER×HUNTER",
"ua04": "아이돌 마스터 샤이니 컬러즈",
"ua05": "귀멸의칼날",
"ua06": "Tales of ARISE",
"ua07": "전생했더니 슬라임이었던 건에 대하여",
"ua08": "BLEACH 천년혈전 편",
"ua09": "나와 로보코",
"ua10": "나의 히어로 아카데미아",
"ua11": "은혼",
"ua12": "블루 록",
"ua13": "철권7",
"ua14": "Dr.STONE",
"ua15": "소드 아트 온라인",
"ua16": "신듀얼리티 느와르",
"ua17": "토리코",
"ua18": "승리의여신NIKKE",
"ua19": "하이큐ー‼",
"ua20": "블랙 클로버",
"ua21": "유유백서",
"ua22": "가메라 -리버스-",
"ua23": "진격의 거인",
"ua24": "SHY",
"ua25": "언데드 언럭",
"ua26": "너를 너무너무너무너무 좋아하는 100명의 그녀",
"ua27": "학원 아이돌 마스터",
"ua28": "괴수 8호",
"ua29": "가면 라이더",
"ua30": "명일방주",
"ua31": "마법소녀 마도카☆마기카",
"ua32": "샹그릴라 프론티어",
"ua33": "2.5차원의 유혹",
"ua34": "코드기어스 탈환의로제",
"ua35": "원펀맨",
"ua36": "마크로스",
"ua37": "강철의 연금술사",
"ua38": "WIND BREAKER",
"ua39": "근육맨",
"ua40": "Re: 제로부터 시작하는 이세계 생활",
"ua41": "바람의 검심 -메이지 검객 낭만기-",
"ua42": "모노가타리 시리즈",
"ua43": "사카모토 데이즈",
"ua44": "에반게리온 신극장판",
"ua45": "To LOVE 트러블",
"ua46": "카구라바치",
"ua47": "도쿄 구울",
"ua48": "킹덤",
"ua49": "마도정병의 슬레이브"
},
"uapr": "UNION ARENA",
"CardList": {
"ResultsCounter": {
"SearchResults": "검색결과",
"SearchResultsCounter": "건"
},
"CardType": {
"includeBasic": "기본 카드",
"includeParallel": "패러렐 카드",
"includeActionPoint": "액션 포인트(AP)"
},
"TriggerType": {
"includeAllTrigger": "모든 트리거",
"includeColorTrigger": "COLOR",
"includeSpecialTrigger": "SPECIAL",
"includeFinalTrigger": "FINAL"
},
"makeDeckList": "덱 리스트 만들기"
},
"Result": {
"preview": "미리보기",
"back": "돌아가기",
"saveAsImage": "이미지로 저장"
}
}

Binary file not shown.

Binary file not shown.

BIN
images/assets/apple-touch-icon.png LFS Executable file

Binary file not shown.

BIN
images/assets/favicon-16x16.png LFS Executable file

Binary file not shown.

BIN
images/assets/favicon-32x32.png LFS Executable file

Binary file not shown.

BIN
images/logo_unionarena.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-001.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-002.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-003.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-004.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-005.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-006.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-007.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-008.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-009.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-010.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-011.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-012.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-013.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-014.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-015.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-016.png LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-017.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-018.png LFS Normal file

Binary file not shown.

BIN
images/ua01/EX02BT_CGH-2-019.png LFS Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More