# 웹 카탄 게임 만들기 - 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점
- 🛤️ 최장 도로 (5개 이상): 2점
- ⚔️ 최대 기사단 (3장 이상): 2점
- 🏆 승점 개발 카드: 1점
🎲 턴 진행
- 주사위 굴리기: 나온 숫자의 타일에서 자원 생산
- 거래: 다른 플레이어나 은행과 자원 교환
- 건설: 도로, 정착지, 도시 건설 또는 개발 카드 구매
- 턴 종료: 다음 플레이어에게 턴 넘김
🌾 5가지 자원
🌾
밀
정착지, 도시, 개발카드
🧱
벽돌
도로, 정착지
🪵
목재
도로, 정착지
🐑
양모
정착지, 개발카드
�ite
광석
도시, 개발카드
🏗️ 건설 비용
🛤️ 도로
🧱×1 + 🪵×1
🏠 정착지
🧱×1 + 🪵×1 + 🌾×1 + 🐑×1
🏰 도시
🌾×2 + ⛰️×3
🃏 개발카드
🌾×1 + 🐑×1 + ⛰️×1
🥷 도둑 (주사위 7)
- 주사위 합이 7이면 자원 8장 이상 보유한 플레이어는 절반을 버립니다.
- 현재 플레이어가 도둑을 원하는 타일로 이동시킵니다.
- 도둑이 있는 타일은 자원을 생산하지 않습니다.
- 해당 타일의 다른 플레이어에게서 자원 1장을 훔칩니다.
💱 거래
- 은행 거래: 같은 자원 4개를 다른 자원 1개로 교환
- 항구: 3:1 또는 2:1 비율로 교환 가능
- 플레이어 거래: AI와 자유롭게 거래 제안
🃏 개발 카드
- 기사 (14장): 도둑 이동, 최대 기사단 경쟁
- 승점 (5장): 즉시 1점 획득
- 도로 건설 (2장): 무료 도로 2개
- 풍년 (2장): 원하는 자원 2개 획득
- 독점 (2장): 선택한 자원을 모든 플레이어에게서 가져옴
※ 구매한 턴에는 사용할 수 없습니다.
💡 팁
- 숫자 6, 8 타일은 자원 생산 확률이 가장 높습니다.
- 다양한 자원을 확보할 수 있는 위치에 정착지를 건설하세요.
- 도시는 정착지의 2배 자원을 생산합니다.
- 항구를 활용하면 효율적인 거래가 가능합니다.
`;
// 닫기 버튼
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)*