diff --git a/pages/index.tsx b/pages/index.tsx
index f2b621f..c2e0061 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -6,6 +6,7 @@ import { ElementsScreen } from '../src/components/screens/ElementsScreen';
import { EvolutionScreen } from '../src/components/screens/EvolutionScreen';
import { FusionScreen } from '../src/components/screens/FusionScreen';
import { ShopScreen } from '../src/components/screens/ShopScreen';
+import { SettingsScreen } from '../src/components/screens/SettingsScreen';
import { useIdleTick } from '../src/hooks/useIdleTick';
import { useGameStore } from '../src/store/useGameStore';
import { trackGameEvent } from '../src/analytics';
@@ -54,6 +55,7 @@ export default function IndexPage() {
{activeTab === 'evolution' && }
{activeTab === 'fusion' && }
{activeTab === 'shop' && }
+ {activeTab === 'settings' && }
diff --git a/src/components/BottomTabBar.tsx b/src/components/BottomTabBar.tsx
new file mode 100644
index 0000000..84acef2
--- /dev/null
+++ b/src/components/BottomTabBar.tsx
@@ -0,0 +1,71 @@
+import { css } from '@emotion/react';
+import type { TabName } from '../store/useGameStore';
+
+interface TabItem {
+ key: TabName;
+ label: string;
+ icon: string;
+}
+
+const TABS: TabItem[] = [
+ { key: 'elements', label: '원소', icon: '⚗️' },
+ { key: 'evolution', label: '강화', icon: '⬆️' },
+ { key: 'fusion', label: '합성', icon: '✨' },
+ { key: 'shop', label: '상점', icon: '🛒' },
+ { key: 'settings', label: '설정', icon: '⚙️' },
+];
+
+interface BottomTabBarProps {
+ activeTab: TabName;
+ onTabChange: (tab: TabName) => void;
+}
+
+const containerStyle = css`
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ background-color: #ffffff;
+ border-top: 1px solid #e8e8e8;
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ z-index: 100;
+`;
+
+const tabItemStyle = css`
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 0 10px;
+ cursor: pointer;
+ background: none;
+ border: none;
+ gap: 4px;
+`;
+
+const iconStyle = css`
+ font-size: 22px;
+ line-height: 1;
+`;
+
+const labelStyle = (active: boolean) => css`
+ font-size: 10px;
+ font-weight: ${active ? 600 : 400};
+ color: ${active ? '#3182F6' : '#8b8b8b'};
+ letter-spacing: -0.2px;
+`;
+
+export function BottomTabBar({ activeTab, onTabChange }: BottomTabBarProps) {
+ return (
+
+ );
+}
diff --git a/src/components/screens/SettingsScreen.tsx b/src/components/screens/SettingsScreen.tsx
new file mode 100644
index 0000000..9d55836
--- /dev/null
+++ b/src/components/screens/SettingsScreen.tsx
@@ -0,0 +1,344 @@
+import { css } from '@emotion/react';
+import { useState } from 'react';
+import { adaptive } from '@toss/tds-colors';
+import { useGameStore, type Language } from '../../store/useGameStore';
+
+const APP_VERSION = '1.0.0';
+const MAX_OFFLINE_HOURS = 24;
+
+const containerStyle = css`
+ padding: 24px 20px;
+ background: ${adaptive.background};
+ min-height: 100%;
+`;
+
+const titleStyle = css`
+ font-size: 20px;
+ font-weight: 700;
+ color: ${adaptive.grey900};
+ margin: 0 0 24px;
+`;
+
+const sectionStyle = css`
+ margin-bottom: 24px;
+`;
+
+const sectionTitleStyle = css`
+ font-size: 12px;
+ font-weight: 600;
+ color: ${adaptive.grey500};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin: 0 0 10px;
+`;
+
+const cardStyle = css`
+ background: ${adaptive.background};
+ border: 1px solid ${adaptive.grey200};
+ border-radius: 16px;
+ overflow: hidden;
+`;
+
+const rowStyle = css`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px;
+ border-bottom: 1px solid ${adaptive.grey100};
+ &:last-child {
+ border-bottom: none;
+ }
+`;
+
+const rowLabelStyle = css`
+ font-size: 15px;
+ font-weight: 500;
+ color: ${adaptive.grey900};
+`;
+
+const rowValueStyle = css`
+ font-size: 14px;
+ color: ${adaptive.grey500};
+`;
+
+const segmentContainerStyle = css`
+ display: flex;
+ gap: 6px;
+`;
+
+const segmentButtonStyle = (active: boolean) => css`
+ padding: 6px 14px;
+ border-radius: 8px;
+ border: 1.5px solid ${active ? adaptive.blue500 : adaptive.grey200};
+ background: ${active ? adaptive.blue500 : 'transparent'};
+ color: ${active ? '#ffffff' : adaptive.grey700};
+ font-size: 13px;
+ font-weight: ${active ? 600 : 400};
+ cursor: pointer;
+ transition: all 0.15s;
+`;
+
+const toggleContainerStyle = css`
+ position: relative;
+ width: 48px;
+ height: 28px;
+ cursor: pointer;
+`;
+
+const toggleTrackStyle = (enabled: boolean) => css`
+ width: 100%;
+ height: 100%;
+ border-radius: 14px;
+ background: ${enabled ? adaptive.blue500 : adaptive.grey300};
+ transition: background 0.2s;
+ border: none;
+ cursor: pointer;
+`;
+
+const toggleThumbStyle = (enabled: boolean) => css`
+ position: absolute;
+ top: 3px;
+ left: ${enabled ? '23px' : '3px'};
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: #ffffff;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
+ transition: left 0.2s;
+ pointer-events: none;
+`;
+
+const resetButtonStyle = css`
+ width: 100%;
+ padding: 14px;
+ border-radius: 12px;
+ border: 1.5px solid #ef5350;
+ background: transparent;
+ color: #ef5350;
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s;
+ &:active {
+ background: rgba(239, 83, 80, 0.08);
+ }
+`;
+
+const versionStyle = css`
+ font-size: 13px;
+ color: ${adaptive.grey400};
+ text-align: center;
+ margin-top: 32px;
+`;
+
+const offlineNoticeStyle = css`
+ font-size: 12px;
+ color: ${adaptive.grey400};
+ margin: 4px 0 0;
+`;
+
+// 2단계 확인 모달
+const overlayStyle = css`
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ z-index: 300;
+`;
+
+const modalStyle = css`
+ background: ${adaptive.background};
+ border-radius: 20px 20px 0 0;
+ padding: 28px 24px calc(28px + env(safe-area-inset-bottom, 0px));
+ width: 100%;
+ max-width: 480px;
+`;
+
+const modalTitleStyle = css`
+ font-size: 18px;
+ font-weight: 700;
+ color: ${adaptive.grey900};
+ margin: 0 0 8px;
+`;
+
+const modalDescStyle = css`
+ font-size: 14px;
+ color: ${adaptive.grey500};
+ margin: 0 0 24px;
+ line-height: 1.5;
+`;
+
+const modalButtonsStyle = css`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+const modalConfirmStyle = css`
+ padding: 15px;
+ border-radius: 12px;
+ border: none;
+ background: #ef5350;
+ color: #ffffff;
+ font-size: 16px;
+ font-weight: 700;
+ cursor: pointer;
+`;
+
+const modalCancelStyle = css`
+ padding: 15px;
+ border-radius: 12px;
+ border: 1px solid ${adaptive.grey200};
+ background: transparent;
+ color: ${adaptive.grey700};
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+`;
+
+const LANGUAGE_OPTIONS: { value: Language; label: string }[] = [
+ { value: 'ko', label: '한국어' },
+ { value: 'en', label: 'English' },
+];
+
+function Toggle({ enabled, onToggle }: { enabled: boolean; onToggle: () => void }) {
+ return (
+
+ );
+}
+
+type ResetStep = 'idle' | 'confirm1' | 'confirm2';
+
+export function SettingsScreen() {
+ const { language, bgmEnabled, lastTickAt, setLanguage, setBgmEnabled, resetGame } = useGameStore();
+ const [resetStep, setResetStep] = useState('idle');
+
+ const lastSavedDate = new Date(lastTickAt);
+ const lastSavedText = lastSavedDate.toLocaleString(language === 'ko' ? 'ko-KR' : 'en-US', {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ const handleResetPress = () => setResetStep('confirm1');
+ const handleConfirm1 = () => setResetStep('confirm2');
+ const handleCancel = () => setResetStep('idle');
+ const handleFinalConfirm = () => {
+ setResetStep('idle');
+ resetGame();
+ };
+
+ return (
+ <>
+
+
설정
+
+ {/* 언어 설정 */}
+
+
언어
+
+
+
언어 선택
+
+ {LANGUAGE_OPTIONS.map((opt) => (
+
+ ))}
+
+
+
+
+
+ {/* 사운드 설정 */}
+
+
사운드
+
+
+ BGM
+ setBgmEnabled(!bgmEnabled)} />
+
+
+
+
+ {/* 앱 정보 */}
+
+
앱 정보
+
+
+ 앱 버전
+ v{APP_VERSION}
+
+
+
+
마지막 저장
+
오프라인 보상은 최대 {MAX_OFFLINE_HOURS}시간까지 적용됩니다
+
+
{lastSavedText}
+
+
+
+
+ {/* 게임 데이터 초기화 */}
+
+
+
Archetype: FirstSpark v{APP_VERSION}
+
+
+ {/* 1단계 확인 모달 */}
+ {resetStep === 'confirm1' && (
+
+
e.stopPropagation()}>
+
게임 데이터를 초기화할까요?
+
+ 모든 원소, 골드, 강화 레벨이 삭제됩니다.{'\n'}이 작업은 되돌릴 수 없습니다.
+
+
+
+
+
+
+
+ )}
+
+ {/* 2단계 최종 확인 모달 */}
+ {resetStep === 'confirm2' && (
+
+
e.stopPropagation()}>
+
정말로 초기화하시겠습니까?
+
+ 예, 모두 삭제합니다{'\n'}앱이 재시작되고 처음부터 다시 시작됩니다.
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts
index f9357ef..682a246 100644
--- a/src/store/useGameStore.ts
+++ b/src/store/useGameStore.ts
@@ -41,7 +41,8 @@ const throttledStorage = createJSONStorage(() => ({
removeItem: (name: string): void => localStorage.removeItem(name),
}));
-export type TabName = 'elements' | 'evolution' | 'fusion' | 'shop';
+export type TabName = 'elements' | 'evolution' | 'fusion' | 'shop' | 'settings';
+export type Language = 'ko' | 'en';
const INITIAL_ELEMENTS: Record = {
fire: 5,
@@ -131,11 +132,26 @@ interface GameState {
// 상점 버프 시스템 (boostId -> 만료 시각 ms)
activeBoosts: Record;
activateBoost: (boostId: string, durationSec: number) => void;
+
+ // 설정
+ language: Language;
+ bgmEnabled: boolean;
+ setLanguage: (lang: Language) => void;
+ setBgmEnabled: (enabled: boolean) => void;
+ resetGame: () => void;
}
type PersistedState = Pick<
GameState,
- 'activeTab' | 'elements' | 'gold' | 'elementLevels' | 'lastTickAt' | 'activeBoosts' | 'spawnAccumulators'
+ | 'activeTab'
+ | 'elements'
+ | 'gold'
+ | 'elementLevels'
+ | 'lastTickAt'
+ | 'activeBoosts'
+ | 'spawnAccumulators'
+ | 'language'
+ | 'bgmEnabled'
>;
export const useGameStore = create()(
@@ -219,6 +235,17 @@ export const useGameStore = create()(
return { success: true, resultId: recipe.result, goldGained };
},
+ // 설정
+ language: 'ko',
+ bgmEnabled: false,
+ setLanguage: (lang) => set({ language: lang }),
+ setBgmEnabled: (enabled) => set({ bgmEnabled: enabled }),
+ resetGame: () => {
+ flushGameState();
+ localStorage.removeItem('archetype-game-state');
+ window.location.reload();
+ },
+
// 상점 버프 시스템
activateBoost: (boostId, durationSec) => {
set((state) => ({
@@ -336,6 +363,8 @@ export const useGameStore = create()(
lastTickAt: state.lastTickAt,
activeBoosts: state.activeBoosts,
spawnAccumulators: state.spawnAccumulators,
+ language: state.language,
+ bgmEnabled: state.bgmEnabled,
}),
onRehydrateStorage: () => (state) => {
// 앱 재시작 시 localStorage에서 복원된 만료 부스트를 자동 정리