v3.0.0: 저장소 재시작 및 전체 자산 반영
This commit is contained in:
37
.ai-rules.md
Normal file
37
.ai-rules.md
Normal 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
1
.cursorrules
Normal file
@@ -0,0 +1 @@
|
||||
모든 작업 시 프로젝트 루트의 .ai-rules.md 지침을 엄격히 준수하고, 작업 종료 시마다 docs/ 폴더 내 문서를 업데이트할 것.
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
images/** filter=lfs diff=lfs merge=lfs -text
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
14
.prettierrc
Normal 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
14
.prettierrc.json
Normal 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
893
cardList.html
Normal 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
2767
datas/ua01.js
Normal file
File diff suppressed because it is too large
Load Diff
2911
datas/ua02.js
Normal file
2911
datas/ua02.js
Normal file
File diff suppressed because it is too large
Load Diff
2749
datas/ua03.js
Normal file
2749
datas/ua03.js
Normal file
File diff suppressed because it is too large
Load Diff
4030
datas/ua04.js
Normal file
4030
datas/ua04.js
Normal file
File diff suppressed because it is too large
Load Diff
2871
datas/ua05.js
Normal file
2871
datas/ua05.js
Normal file
File diff suppressed because it is too large
Load Diff
1618
datas/ua06.js
Normal file
1618
datas/ua06.js
Normal file
File diff suppressed because it is too large
Load Diff
2730
datas/ua07.js
Normal file
2730
datas/ua07.js
Normal file
File diff suppressed because it is too large
Load Diff
2819
datas/ua08.js
Normal file
2819
datas/ua08.js
Normal file
File diff suppressed because it is too large
Load Diff
1640
datas/ua09.js
Normal file
1640
datas/ua09.js
Normal file
File diff suppressed because it is too large
Load Diff
2728
datas/ua10.js
Normal file
2728
datas/ua10.js
Normal file
File diff suppressed because it is too large
Load Diff
1689
datas/ua11.js
Normal file
1689
datas/ua11.js
Normal file
File diff suppressed because it is too large
Load Diff
1759
datas/ua12.js
Normal file
1759
datas/ua12.js
Normal file
File diff suppressed because it is too large
Load Diff
1608
datas/ua13.js
Normal file
1608
datas/ua13.js
Normal file
File diff suppressed because it is too large
Load Diff
1658
datas/ua14.js
Normal file
1658
datas/ua14.js
Normal file
File diff suppressed because it is too large
Load Diff
2954
datas/ua15.js
Normal file
2954
datas/ua15.js
Normal file
File diff suppressed because it is too large
Load Diff
1708
datas/ua16.js
Normal file
1708
datas/ua16.js
Normal file
File diff suppressed because it is too large
Load Diff
1648
datas/ua17.js
Normal file
1648
datas/ua17.js
Normal file
File diff suppressed because it is too large
Load Diff
2707
datas/ua18.js
Normal file
2707
datas/ua18.js
Normal file
File diff suppressed because it is too large
Load Diff
1710
datas/ua19.js
Normal file
1710
datas/ua19.js
Normal file
File diff suppressed because it is too large
Load Diff
1688
datas/ua20.js
Normal file
1688
datas/ua20.js
Normal file
File diff suppressed because it is too large
Load Diff
1726
datas/ua21.js
Normal file
1726
datas/ua21.js
Normal file
File diff suppressed because it is too large
Load Diff
1177
datas/ua22.js
Normal file
1177
datas/ua22.js
Normal file
File diff suppressed because it is too large
Load Diff
2777
datas/ua23.js
Normal file
2777
datas/ua23.js
Normal file
File diff suppressed because it is too large
Load Diff
1655
datas/ua24.js
Normal file
1655
datas/ua24.js
Normal file
File diff suppressed because it is too large
Load Diff
1196
datas/ua25.js
Normal file
1196
datas/ua25.js
Normal file
File diff suppressed because it is too large
Load Diff
1317
datas/ua26.js
Normal file
1317
datas/ua26.js
Normal file
File diff suppressed because it is too large
Load Diff
3160
datas/ua27.js
Normal file
3160
datas/ua27.js
Normal file
File diff suppressed because it is too large
Load Diff
1226
datas/ua28.js
Normal file
1226
datas/ua28.js
Normal file
File diff suppressed because it is too large
Load Diff
3109
datas/ua29.js
Normal file
3109
datas/ua29.js
Normal file
File diff suppressed because it is too large
Load Diff
2859
datas/ua30.js
Normal file
2859
datas/ua30.js
Normal file
File diff suppressed because it is too large
Load Diff
1699
datas/ua31.js
Normal file
1699
datas/ua31.js
Normal file
File diff suppressed because it is too large
Load Diff
1178
datas/ua32.js
Normal file
1178
datas/ua32.js
Normal file
File diff suppressed because it is too large
Load Diff
1276
datas/ua33.js
Normal file
1276
datas/ua33.js
Normal file
File diff suppressed because it is too large
Load Diff
1187
datas/ua34.js
Normal file
1187
datas/ua34.js
Normal file
File diff suppressed because it is too large
Load Diff
1716
datas/ua35.js
Normal file
1716
datas/ua35.js
Normal file
File diff suppressed because it is too large
Load Diff
1748
datas/ua36.js
Normal file
1748
datas/ua36.js
Normal file
File diff suppressed because it is too large
Load Diff
1688
datas/ua37.js
Normal file
1688
datas/ua37.js
Normal file
File diff suppressed because it is too large
Load Diff
1238
datas/ua38.js
Normal file
1238
datas/ua38.js
Normal file
File diff suppressed because it is too large
Load Diff
1217
datas/ua39.js
Normal file
1217
datas/ua39.js
Normal file
File diff suppressed because it is too large
Load Diff
1688
datas/ua40.js
Normal file
1688
datas/ua40.js
Normal file
File diff suppressed because it is too large
Load Diff
1698
datas/ua41.js
Normal file
1698
datas/ua41.js
Normal file
File diff suppressed because it is too large
Load Diff
1778
datas/ua42.js
Normal file
1778
datas/ua42.js
Normal file
File diff suppressed because it is too large
Load Diff
1198
datas/ua43.js
Normal file
1198
datas/ua43.js
Normal file
File diff suppressed because it is too large
Load Diff
1709
datas/ua44.js
Normal file
1709
datas/ua44.js
Normal file
File diff suppressed because it is too large
Load Diff
1397
datas/ua45.js
Normal file
1397
datas/ua45.js
Normal file
File diff suppressed because it is too large
Load Diff
1348
datas/ua46.js
Normal file
1348
datas/ua46.js
Normal file
File diff suppressed because it is too large
Load Diff
1779
datas/ua47.js
Normal file
1779
datas/ua47.js
Normal file
File diff suppressed because it is too large
Load Diff
1749
datas/ua48.js
Normal file
1749
datas/ua48.js
Normal file
File diff suppressed because it is too large
Load Diff
1297
datas/ua49.js
Normal file
1297
datas/ua49.js
Normal file
File diff suppressed because it is too large
Load Diff
1007
datas/ua99 sample mini.js
Normal file
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
1318
datas/ua99 sample.js
Normal file
File diff suppressed because it is too large
Load Diff
54
datas/uapr.js
Normal file
54
datas/uapr.js
Normal 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
43
docs/convention.md
Normal 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
44
docs/history.md
Normal 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
50
docs/map.md
Normal 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
101
docs/spec.md
Normal 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
19
docs/todo.md
Normal 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
40
docs/update.md
Normal 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
BIN
favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
60
i18n/i18n.js
Normal file
60
i18n/i18n.js
Normal 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
130
i18n/translations.en.json
Normal 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 VICTORY:NIKKE",
|
||||
"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
146
i18n/translations.ja.json
Normal 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
142
i18n/translations.ko.json
Normal 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": "이미지로 저장"
|
||||
}
|
||||
}
|
||||
BIN
images/assets/android-chrome-192x192.png
LFS
Executable file
BIN
images/assets/android-chrome-192x192.png
LFS
Executable file
Binary file not shown.
BIN
images/assets/android-chrome-512x512.png
LFS
Executable file
BIN
images/assets/android-chrome-512x512.png
LFS
Executable file
Binary file not shown.
BIN
images/assets/apple-touch-icon.png
LFS
Executable file
BIN
images/assets/apple-touch-icon.png
LFS
Executable file
Binary file not shown.
BIN
images/assets/favicon-16x16.png
LFS
Executable file
BIN
images/assets/favicon-16x16.png
LFS
Executable file
Binary file not shown.
BIN
images/assets/favicon-32x32.png
LFS
Executable file
BIN
images/assets/favicon-32x32.png
LFS
Executable file
Binary file not shown.
BIN
images/logo_unionarena.png
LFS
Normal file
BIN
images/logo_unionarena.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-001.png
LFS
Normal file
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
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
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
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
BIN
images/ua01/EX02BT_CGH-2-005.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-005_p1.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-005_p1.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-006.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-006.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-006_p1.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-006_p1.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-007.png
LFS
Normal file
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
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
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
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
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
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
BIN
images/ua01/EX02BT_CGH-2-013.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-013_p1.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-013_p1.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-014.png
LFS
Normal file
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
BIN
images/ua01/EX02BT_CGH-2-015.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-015_p1.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-015_p1.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-016.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-016.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-016_p1.png
LFS
Normal file
BIN
images/ua01/EX02BT_CGH-2-016_p1.png
LFS
Normal file
Binary file not shown.
BIN
images/ua01/EX02BT_CGH-2-017.png
LFS
Normal file
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
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
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
Reference in New Issue
Block a user