# 웹 카탄 게임 만들기 - Part 9: 게임 설정 및 UI 완성 ## 이번 파트에서 구현할 내용 - 게임 설정 화면 완성 - 튜토리얼 시스템 - 게임 레이아웃 완성 - UI 폴리싱 - 게임 로그 시스템 개선 --- ## Step 1: 게임 설정 화면 완성 ```javascript // js/ui/SetupScreen.js class SetupScreen { constructor() { this.selectedAICount = 2; this.selectedDifficulty = AI_DIFFICULTY.MEDIUM; this.bindEvents(); } bindEvents() { // AI 수 선택 document.querySelectorAll('#ai-count button').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('#ai-count button').forEach(b => b.classList.remove('selected') ); e.target.classList.add('selected'); this.selectedAICount = parseInt(e.target.dataset.value); }); }); // 난이도 선택 document.querySelectorAll('#ai-difficulty button').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('#ai-difficulty button').forEach(b => b.classList.remove('selected') ); e.target.classList.add('selected'); this.selectedDifficulty = e.target.dataset.value; }); }); // 게임 시작 document.getElementById('start-game').addEventListener('click', () => { this.startGame(); }); // 튜토리얼 document.getElementById('show-tutorial').addEventListener('click', () => { this.showTutorial(); }); } startGame() { // 설정 화면 숨기기 document.getElementById('setup-screen').classList.remove('active'); document.getElementById('game-screen').classList.add('active'); // 게임 시작 const game = new Game(); game.startGame(this.selectedAICount + 1, this.selectedDifficulty); // UI 매니저 초기화 const uiManager = new UIManager(game); game.uiManager = uiManager; uiManager.initialize(); } showTutorial() { const modal = document.getElementById('tutorial-modal'); modal.classList.add('active'); this.loadTutorialContent(); } loadTutorialContent() { const content = document.getElementById('tutorial-content'); content.innerHTML = `

🎯 게임 목표

10점을 먼저 획득한 플레이어가 승리합니다!

📊 점수 획득 방법

🎲 턴 진행

  1. 주사위 굴리기: 나온 숫자의 타일에서 자원 생산
  2. 거래: 다른 플레이어나 은행과 자원 교환
  3. 건설: 도로, 정착지, 도시 건설 또는 개발 카드 구매
  4. 턴 종료: 다음 플레이어에게 턴 넘김

🌾 5가지 자원

🌾 정착지, 도시, 개발카드
🧱 벽돌 도로, 정착지
🪵 목재 도로, 정착지
🐑 양모 정착지, 개발카드
�ite 광석 도시, 개발카드

🏗️ 건설 비용

🛤️ 도로 🧱×1 + 🪵×1
🏠 정착지 🧱×1 + 🪵×1 + 🌾×1 + 🐑×1
🏰 도시 🌾×2 + ⛰️×3
🃏 개발카드 🌾×1 + 🐑×1 + ⛰️×1

🥷 도둑 (주사위 7)

💱 거래

🃏 개발 카드

※ 구매한 턴에는 사용할 수 없습니다.

💡 팁

`; // 닫기 버튼 document.querySelector('#tutorial-modal .close-btn').onclick = () => { document.getElementById('tutorial-modal').classList.remove('active'); }; } } ``` --- ## Step 2: 설정 화면 CSS ```css /* css/style.css에 추가 */ /* 설정 화면 */ #setup-screen { justify-content: center; align-items: center; background: linear-gradient(135deg, #1a252f 0%, #2c3e50 100%); } .setup-container { text-align: center; padding: 40px; background: var(--panel-color); border-radius: 20px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); max-width: 500px; } .setup-container h1 { font-size: 42px; margin-bottom: 10px; color: var(--text-color); } .setup-container .subtitle { font-size: 18px; color: var(--text-muted); margin-bottom: 30px; } .setup-options { margin-bottom: 30px; } .option-group { margin-bottom: 20px; } .option-group label { display: block; margin-bottom: 10px; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; } .button-group { display: flex; gap: 10px; justify-content: center; } .button-group button { flex: 1; max-width: 100px; padding: 12px; background: var(--background-color); color: var(--text-color); border: 2px solid transparent; transition: all 0.2s; } .button-group button:hover { border-color: var(--secondary-color); } .button-group button.selected { background: var(--secondary-color); border-color: var(--secondary-color); } #start-game { margin-bottom: 15px; width: 100%; } #show-tutorial { width: 100%; } /* 튜토리얼 모달 */ #tutorial-modal .modal-content { max-width: 700px; max-height: 80vh; overflow-y: auto; } .tutorial-section { margin-bottom: 25px; padding-bottom: 20px; border-bottom: 1px solid var(--background-color); } .tutorial-section:last-child { border-bottom: none; } .tutorial-section h3 { color: var(--secondary-color); margin-bottom: 12px; font-size: 18px; } .tutorial-section ul, .tutorial-section ol { padding-left: 20px; line-height: 1.8; } .tutorial-section li { margin-bottom: 5px; } .resource-guide { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; } .resource-guide .resource-item { text-align: center; padding: 10px; background: var(--background-color); border-radius: 8px; } .resource-guide .emoji { display: block; font-size: 28px; margin-bottom: 5px; } .resource-guide .name { display: block; font-weight: bold; margin-bottom: 3px; } .resource-guide .use { display: block; font-size: 10px; color: var(--text-muted); } .build-costs { display: grid; gap: 8px; } .cost-item { display: flex; justify-content: space-between; padding: 10px; background: var(--background-color); border-radius: 6px; } .cost-item .building { font-weight: bold; } .tutorial-section .note { margin-top: 10px; font-size: 12px; color: var(--text-muted); font-style: italic; } ``` --- ## Step 3: 게임 레이아웃 완성 ```css /* css/ui.css에 추가 */ /* 게임 레이아웃 */ #game-screen { flex-direction: column; } .game-layout { display: grid; grid-template-columns: 250px 1fr 280px; gap: 15px; padding: 15px; flex: 1; min-height: 0; } /* 왼쪽 패널: 플레이어 정보 */ .player-panel { background: var(--panel-color); border-radius: 10px; padding: 15px; overflow-y: auto; } .player-panel h2 { font-size: 16px; margin-bottom: 15px; color: var(--text-muted); } /* 중앙: 게임 보드 */ .board-container { display: flex; flex-direction: column; align-items: center; justify-content: center; background: var(--panel-color); border-radius: 10px; padding: 15px; } /* 오른쪽 패널: 액션 */ .action-panel { background: var(--panel-color); border-radius: 10px; padding: 15px; display: flex; flex-direction: column; gap: 15px; overflow-y: auto; } .current-turn { text-align: center; padding: 15px; background: var(--background-color); border-radius: 8px; } .current-turn h3 { font-size: 12px; color: var(--text-muted); margin-bottom: 5px; } #current-player-name { font-size: 20px; font-weight: bold; } .action-buttons { display: flex; flex-direction: column; gap: 8px; } /* 게임 로그 */ #game-log { background: var(--panel-color); border-radius: 10px; margin: 0 15px 15px; max-height: 150px; overflow: hidden; display: flex; flex-direction: column; } .log-header { padding: 10px 15px; background: var(--background-color); font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; } #log-content { flex: 1; overflow-y: auto; padding: 10px 15px; font-size: 13px; line-height: 1.6; } .log-entry { margin-bottom: 5px; padding: 3px 0; } .log-entry.log-info { color: var(--text-color); } .log-entry.log-success { color: var(--success-color); } .log-entry.log-warning { color: var(--warning-color); } .log-entry.log-error { color: var(--accent-color); } .log-entry.log-turn { color: var(--secondary-color); font-weight: bold; border-top: 1px solid var(--background-color); padding-top: 8px; margin-top: 5px; } ``` --- ## Step 4: 로그 시스템 개선 ```javascript // helpers.js의 logMessage 개선 function logMessage(message, type = 'info') { const logContent = document.getElementById('log-content'); if (!logContent) return; const entry = document.createElement('div'); entry.className = `log-entry log-${type}`; // 시간 제거하고 간결하게 entry.textContent = message; // 턴 구분 메시지 감지 if (message.includes('━━━')) { entry.classList.add('log-turn'); } logContent.appendChild(entry); // 스크롤을 최신 로그로 logContent.scrollTop = logContent.scrollHeight; // 로그 개수 제한 (최대 100개) while (logContent.children.length > 100) { logContent.removeChild(logContent.firstChild); } } // 타입별 헬퍼 함수 function logSuccess(message) { logMessage(message, 'success'); } function logWarning(message) { logMessage(message, 'warning'); } function logError(message) { logMessage(message, 'error'); } ``` --- ## Step 5: UI 피드백 개선 ```javascript // UIManager에 추가 class UIManager { // 토스트 메시지 표시 showToast(message, type = 'info', duration = 3000) { const container = document.getElementById('toast-container') || this.createToastContainer(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); // 애니메이션 setTimeout(() => toast.classList.add('show'), 10); // 자동 제거 setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } createToastContainer() { const container = document.createElement('div'); container.id = 'toast-container'; document.body.appendChild(container); return container; } // 주사위 업데이트 updateDice(total) { const dice1 = document.getElementById('dice1'); const dice2 = document.getElementById('dice2'); const emojis = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅']; dice1.textContent = emojis[this.game.diceManager.dice1 - 1]; dice2.textContent = emojis[this.game.diceManager.dice2 - 1]; // 7이면 경고 효과 if (total === 7) { dice1.classList.add('robber-roll'); dice2.classList.add('robber-roll'); setTimeout(() => { dice1.classList.remove('robber-roll'); dice2.classList.remove('robber-roll'); }, 1000); } } // 자원 변화 애니메이션 animateResourceChange(resource, amount, isGain) { const container = document.getElementById('my-resources'); const resourceItems = container.querySelectorAll('.resource-item'); resourceItems.forEach(item => { if (item.querySelector('.resource-emoji').textContent === getResourceEmoji(resource)) { item.classList.add(isGain ? 'resource-gain' : 'resource-loss'); setTimeout(() => { item.classList.remove('resource-gain', 'resource-loss'); }, 500); } }); } } ``` --- ## Step 6: 토스트 및 애니메이션 CSS ```css /* css/ui.css에 추가 */ /* 토스트 메시지 */ #toast-container { position: fixed; top: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column; gap: 10px; } .toast { padding: 12px 20px; border-radius: 8px; color: white; font-size: 14px; opacity: 0; transform: translateX(100px); transition: all 0.3s ease; max-width: 300px; } .toast.show { opacity: 1; transform: translateX(0); } .toast-info { background: var(--secondary-color); } .toast-success { background: var(--success-color); } .toast-warning { background: var(--warning-color); } .toast-error { background: var(--accent-color); } /* 자원 변화 애니메이션 */ .resource-item.resource-gain { animation: resource-gain 0.5s ease; } .resource-item.resource-loss { animation: resource-loss 0.5s ease; } @keyframes resource-gain { 0% { transform: scale(1); } 50% { transform: scale(1.3); background: rgba(39, 174, 96, 0.3); } 100% { transform: scale(1); } } @keyframes resource-loss { 0% { transform: scale(1); } 50% { transform: scale(0.8); background: rgba(231, 76, 60, 0.3); } 100% { transform: scale(1); } } /* 주사위 7 효과 */ .dice.robber-roll { animation: robber-shake 0.5s ease; background: #e74c3c; color: white; } @keyframes robber-shake { 0%, 100% { transform: rotate(0deg); } 20% { transform: rotate(-10deg); } 40% { transform: rotate(10deg); } 60% { transform: rotate(-10deg); } 80% { transform: rotate(10deg); } } /* 버튼 호버 효과 */ .action-btn:not(:disabled):hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); } /* 승리 근접 경고 */ .player-info.near-victory { border: 2px solid var(--warning-color); animation: victory-pulse 2s infinite; } @keyframes victory-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(243, 156, 18, 0.4); } 50% { box-shadow: 0 0 20px 5px rgba(243, 156, 18, 0.2); } } ``` --- ## 다음 단계 예고 Part 10 (최종)에서는 **테스트 및 완성**을 진행합니다: - 종합 테스트 및 버그 수정 - 재시작 및 기록 기능 - 피드백 시스템 - 최종 완성 및 배포 --- *이전 파트: [Part 8 - AI 구현](make-web-katan-game-v8.md)* *다음 파트: [Part 10 - 테스트 및 최종 완성](make-web-katan-game-v10.md)*