Files
chakmate/scene_gamification.html
yenru0 3735240eed refactor: apply Tailwind CSS and Heroicons to all HTML files
- Replace custom CSS with Tailwind utility classes
- Convert all inline SVGs to Heroicons sprite system
- Add consistent Tailwind config with design tokens
- Improve responsive layout for onboarding screen
2026-05-18 18:04:47 +09:00

564 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>내 진행 상황 - Chakmate</title>
<!-- Heroicons SVG Sprite -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
<symbol id="icon-arrow-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</symbol>
</svg>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#3b82f6', light: '#60a5fa', dark: '#1d4ed8' },
secondary: { DEFAULT: '#06b6d4', light: '#22d3ee' },
accent: { DEFAULT: '#0ea5e9', warn: '#38bdf8', danger: '#f472b6' },
surface: { primary: '#f8fafc', secondary: '#e2e8f0', card: '#ffffff' },
text: { primary: '#0f172a', secondary: '#475569', muted: '#94a3b8' },
},
fontFamily: {
sans: ['Noto Sans KR', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
display: ['Outfit', 'sans-serif'],
},
boxShadow: {
'sm': '0 2px 8px rgba(14, 165, 233, 0.06)',
'md': '0 4px 20px rgba(14, 165, 233, 0.08)',
'lg': '0 8px 40px rgba(14, 165, 233, 0.12)',
'glow': '0 0 30px rgba(14, 165, 233, 0.25)',
'blue': '0 4px 20px rgba(59, 130, 246, 0.3)',
},
animation: {
'shimmer': 'shimmer 3s ease-in-out infinite',
'pulse-slow': 'pulse 1.5s ease-in-out infinite',
'unlock-pulse': 'unlockPulse 0.6s ease-out',
'sparkle': 'sparkle 0.6s ease-out forwards',
'confetti-fall': 'confettiFall 2s ease-out forwards',
'fire-flicker': 'fireFlicker 0.3s ease-in-out infinite',
},
keyframes: {
shimmer: {
'0%, 100%': { transform: 'translate(-10%, -10%) rotate(0deg)' },
'50%': { transform: 'translate(10%, 10%) rotate(180deg)' },
},
unlockPulse: {
'0%': { transform: 'scale(1)', boxShadow: '0 0 0 0 rgba(251, 191, 36, 0.4)' },
'50%': { transform: 'scale(1.1)', boxShadow: '0 0 0 15px rgba(251, 191, 36, 0)' },
'100%': { transform: 'scale(1)', boxShadow: '0 0 0 0 rgba(251, 191, 36, 0)' },
},
sparkle: {
'0%, 100%': { opacity: '0', transform: 'scale(0) rotate(0deg)' },
'50%': { opacity: '1', transform: 'scale(1) rotate(180deg)' },
},
confettiFall: {
'0%': { opacity: '1', transform: 'translateY(-100px) rotate(0deg)' },
'100%': { opacity: '0', transform: 'translateY(100vh) rotate(720deg)' },
},
fireFlicker: {
'0%, 100%': { transform: 'scaleY(1) scaleX(1)' },
'25%': { transform: 'scaleY(1.1) scaleX(0.95)' },
'50%': { transform: 'scaleY(0.95) scaleX(1.05)' },
'75%': { transform: 'scaleY(1.05) scaleX(0.98)' },
},
},
},
},
}
</script>
<style>
.confetti {
position: absolute;
width: 10px;
height: 10px;
opacity: 0;
}
.achievement.just-unlocked {
animation: unlockPulse 0.6s ease-out;
}
.sparkle {
position: absolute;
font-size: 1rem;
animation: sparkle 0.6s ease-out forwards;
}
.toast.show {
opacity: 1 !important;
transform: translateX(-50%) translateY(0) !important;
}
.toggle-switch {
position: relative;
width: 56px;
height: 32px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #e2e8f0;
border-radius: 9999px;
transition: background 250ms ease;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 24px;
width: 24px;
left: 4px;
bottom: 4px;
background: white;
border-radius: 50%;
transition: transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.06);
}
.toggle-switch input:checked + .toggle-slider {
background: linear-gradient(90deg, #3b82f6 0%, #0ea5e9 100%);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(24px);
}
.achievement-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(0);
background: #0f172a;
color: #ffffff;
padding: 8px 12px;
border-radius: 8px;
font-size: 0.7rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 150ms ease;
z-index: 10;
}
.achievement-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #0f172a;
}
.achievement.locked:hover .achievement-tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-10px);
}
.achievement.unlocked .achievement-tooltip {
display: none;
}
.week-day-indicator {
width: 36px;
height: 36px;
border-radius: 12px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
background: #e2e8f0;
transition: all 250ms ease;
}
.week-day-indicator.completed {
background: linear-gradient(135deg, #0ea5e9 0%, #10b981 100%);
color: white;
box-shadow: 0 4px 12px rgba(52, 211, 153, 0.3);
}
.week-day-indicator.today {
border: 2px solid #3b82f6;
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.08);
}
.achievement {
position: relative;
aspect-ratio: 1;
border-radius: 12px;
background: #e2e8f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px;
transition: transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 250ms ease;
cursor: pointer;
}
.achievement.unlocked {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
box-shadow: 0 4px 20px rgba(251, 191, 36, 0.3);
}
.achievement.unlocked.gold {
background: linear-gradient(135deg, #fef3c7 0%, #fcd34d 100%);
}
.achievement.unlocked.silver {
background: linear-gradient(135deg, #f3f4f6 0%, #d1d5db 100%);
box-shadow: 0 4px 20px rgba(156, 163, 175, 0.3);
}
.achievement.unlocked.bronze {
background: linear-gradient(135deg, #fed7aa 0%, #fdba74 100%);
box-shadow: 0 4px 20px rgba(251, 146, 60, 0.3);
}
.achievement.locked {
opacity: 0.6;
}
.achievement:hover:not(.locked) {
transform: scale(1.05) translateY(-4px);
}
.achievement.locked:hover {
transform: scale(1.02);
}
.streak-icon.animate {
animation: fireFlicker 0.3s ease-in-out infinite;
}
</style>
</head>
<body class="font-sans bg-surface-primary text-text-primary min-h-screen overflow-x-hidden antialiased">
<div class="max-w-[480px] mx-auto p-4 pb-[100px]">
<header class="flex items-center justify-between p-4 bg-surface-card shadow-sm sticky top-0 z-[100] mb-6 rounded-xl">
<div class="flex items-center gap-3">
<div class="relative w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-xl flex items-center justify-center shadow-blue">
<svg viewBox="0 0 24 24" fill="none" class="w-6 h-6 fill-white drop-shadow-md">
<defs>
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4"/>
<stop offset="50%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient>
</defs>
<path d="M6 3a3 3 0 013 3v2a3 3 0 01-3 3H4a2 2 0 00-2 2v6a2 2 0 002 2h2a3 3 0 013 3v2a3 3 0 01-3 3H6a3 3 0 01-3-3v-2a3 3 0 013-3h2a2 2 0 002-2V8a2 2 0 00-2-2H6a3 3 0 01-3-3V6a3 3 0 013-3z" fill="url(#logoGrad)"/>
<circle cx="12" cy="12" r="3" fill="white" opacity="0.3"/>
</svg>
</div>
<h1 class="font-display text-xl font-semibold">내 진행 상황</h1>
</div>
<div class="flex items-center gap-3">
<button class="w-11 h-11 rounded-xl bg-surface-secondary flex items-center justify-center cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 transition-all duration-150" onclick="window.location.href='index.html'">
<svg class="w-5 h-5 stroke-text-primary"><use href="#icon-arrow-left"></use></svg>
뒤로
</button>
</div>
</header>
<div class="bg-gradient-to-br from-primary to-secondary rounded-[28px] p-8 text-center relative overflow-hidden shadow-glow mb-6">
<div class="streak-icon text-5xl mb-2 animate-pulse-slow" id="streakIcon">🔥</div>
<div class="font-display text-7xl font-extrabold text-white leading-none drop-shadow-lg" id="streakCount">0</div>
<div class="text-xl font-bold text-white/95 uppercase tracking-[2px] mt-1">STREAK!</div>
<div class="text-white/90 text-sm mt-4" id="streakMessage">오늘 시작하세요!</div>
</div>
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
주간 목표
</div>
<div class="mb-4">
<div class="flex justify-between items-center mb-3">
<span class="text-sm text-text-primary font-medium">이번 주 완료된 일수</span>
<span class="text-sm text-primary font-semibold"><span id="daysCompleted">0</span>/7</span>
</div>
<div class="h-3 bg-surface-secondary rounded-full overflow-hidden relative">
<div class="h-full bg-gradient-to-r from-primary to-accent rounded-full w-0 transition-all duration-1000 relative" id="progressFill"></div>
</div>
</div>
<div class="grid grid-cols-7 gap-2 mt-4" id="weekGrid">
</div>
</div>
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
업적
</div>
<div class="grid grid-cols-4 gap-3" id="achievementsGrid">
</div>
</div>
<div class="bg-surface-card rounded-[20px] p-5 mb-5 shadow-sm hover:-translate-y-0.5 hover:shadow-md transition-all duration-250">
<div class="text-xs font-semibold text-text-secondary uppercase tracking-wider mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-gradient-to-b from-primary to-secondary rounded-sm"></span>
오늘의 팁
</div>
<div class="bg-gradient-to-br from-surface-secondary to-surface-card rounded-xl p-4 flex gap-3 items-start">
<span class="text-2xl shrink-0">💡</span>
<div class="flex-1">
<p class="text-sm text-text-primary leading-relaxed" id="tipText">작은 걸음이 큰 변화를 만듭니다! 오늘 5분만 투자하세요.</p>
<p class="text-xs text-text-muted mt-2">— 오늘의 동기</p>
</div>
</div>
</div>
<div class="flex items-center justify-between p-4 bg-surface-card rounded-[20px] shadow-sm">
<div class="flex items-center gap-3">
<span class="text-2xl">🔔</span>
<div>
<div class="text-sm font-medium text-text-primary">습관 알림</div>
<div class="text-xs text-text-muted mt-0.5">매일 알림 받기</div>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" id="habitToggle">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="fixed bottom-[100px] left-1/2 -translate-x-1/2 translate-y-[100px] bg-text-primary text-surface-card px-6 py-4 rounded-full text-sm font-medium shadow-lg opacity-0 transition-all duration-500 z-[100] flex items-center gap-2" id="toast">
<span id="toastIcon">🎉</span>
<span id="toastText">업적 달성!</span>
</div>
<div class="fixed top-0 left-0 w-full h-full pointer-events-none z-[1000] overflow-hidden" id="confettiContainer"></div>
<script>
// Data Management
const STORAGE_KEY = 'chackly_gamification';
const defaultData = {
streak: 12,
weeklyProgress: [true, true, true, true, false, false, false],
achievements: [
{ id: 'first_sort', name: '첫 정리', icon: '🏆', unlocked: true, tier: 'gold', requirement: '첫 정리 세션 완료' },
{ id: 'week_warrior', name: '주간 챌린저', icon: '🌟', unlocked: true, tier: 'gold', requirement: '7일 연속 완료' },
{ id: 'streak_7', name: '7일 스트릭', icon: '🔥', unlocked: true, tier: 'silver', requirement: '7일 스트릭 유지' },
{ id: 'diamond', name: '다이아몬드', icon: '💎', unlocked: true, tier: 'gold', requirement: '30일 스트릭 유지' },
{ id: 'organizer', name: '정리 달인', icon: '📦', unlocked: false, tier: 'silver', requirement: '100개 파일 정리' },
{ id: 'minimalist', name: '미니멀리스트', icon: '✨', unlocked: false, tier: 'bronze', requirement: '50개 파일 삭제' },
{ id: 'speed_demon', name: '스피드 데몬', icon: '⚡', unlocked: false, tier: 'gold', requirement: '하루에 50개 파일 정리' },
{ id: 'collector', name: '수집가', icon: '🗂️', unlocked: false, tier: 'silver', requirement: '5개 커스텀 폴더 생성' }
],
habitReminderEnabled: true,
lastUpdated: new Date().toISOString()
};
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return { ...defaultData, ...JSON.parse(stored) };
}
return defaultData;
}
function saveData(data) {
data.lastUpdated = new Date().toISOString();
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
// UI Elements
const streakCount = document.getElementById('streakCount');
const streakIcon = document.getElementById('streakIcon');
const streakMessage = document.getElementById('streakMessage');
const progressFill = document.getElementById('progressFill');
const daysCompleted = document.getElementById('daysCompleted');
const weekGrid = document.getElementById('weekGrid');
const achievementsGrid = document.getElementById('achievementsGrid');
const habitToggle = document.getElementById('habitToggle');
const toast = document.getElementById('toast');
const toastIcon = document.getElementById('toastIcon');
const toastText = document.getElementById('toastText');
const confettiContainer = document.getElementById('confettiContainer');
const motivationalMessages = [
"불을 이어가세요!",
"오늘 불이 나고 있어요!",
"놀라운 일관성!",
"아무도 당신을 막을 수 없어요!",
"챔피언의 행동!"
];
const dailyTips = [
{ text: "작은 걸음이 큰 변화를 만듭니다! 오늘 5분만 투자하세요.", author: "일일 동기" },
{ text: "정돈된 공간은 정돈된 마음을 만듭니다. 작게 시작하세요!", author: "정리의 지혜" },
{ text: "정리된 모든 파일이 진보입니다.庆祝하세요!", author: "업적 달성" },
{ text: "미래의 당신이 오늘 정리한 것에 감사할 것입니다.", author: "타임 트래블러" },
{ text: "일관성이 완벽함을 이깁니다. 매일来吧!", author: "습관 마스터" }
];
// Animate streak count
function animateCount(element, target, duration = 1500) {
const start = 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOut = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(start + (target - start) * easeOut);
element.textContent = current;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// Render week grid
function renderWeekGrid(progress) {
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
const today = new Date().getDay();
const mondayIndex = today === 0 ? 6 : today - 1;
weekGrid.innerHTML = days.map((day, i) => {
const completed = progress[i];
const isToday = i === mondayIndex;
return `
<div class="week-day">
<div class="week-day-label">${day}</div>
<div class="week-day-indicator ${completed ? 'completed' : ''} ${isToday ? 'today' : ''}">
${completed ? '✅' : (isToday ? '◉' : '')}
</div>
</div>
`;
}).join('');
}
// Render achievements
function renderAchievements(achievements) {
achievementsGrid.innerHTML = achievements.map(ach => `
<div class="achievement ${ach.unlocked ? `unlocked ${ach.tier}` : 'locked'}" data-id="${ach.id}">
<span class="achievement-icon">${ach.icon}</span>
<span class="achievement-name">${ach.name}</span>
<div class="achievement-tooltip">${ach.requirement}</div>
</div>
`).join('');
}
// Show toast notification
function showToast(icon, message) {
toastIcon.textContent = icon;
toastText.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Confetti celebration
function triggerConfetti() {
const colors = ['#6366f1', '#f472b6', '#34d399', '#fbbf24', '#818cf8', '#34d399'];
const confettiCount = 50;
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
confetti.style.width = Math.random() * 10 + 5 + 'px';
confetti.style.height = confetti.style.width;
confetti.style.animation = `confettiFall ${Math.random() * 2 + 2}s ease-out forwards`;
confetti.style.animationDelay = Math.random() * 0.5 + 's';
confettiContainer.appendChild(confetti);
setTimeout(() => {
confetti.remove();
}, 4000);
}
}
// Trigger badge unlock animation
function animateBadgeUnlock(achievementId) {
const badge = document.querySelector(`.achievement[data-id="${achievementId}"]`);
if (badge) {
badge.classList.add('just-unlocked');
// Add sparkles
for (let i = 0; i < 6; i++) {
const sparkle = document.createElement('span');
sparkle.className = 'sparkle';
sparkle.textContent = '✨';
sparkle.style.left = Math.random() * 100 + '%';
sparkle.style.top = Math.random() * 100 + '%';
badge.appendChild(sparkle);
}
setTimeout(() => {
badge.classList.remove('just-unlocked');
badge.querySelectorAll('.sparkle').forEach(s => s.remove());
}, 600);
}
}
// Initialize page
function init() {
const data = loadData();
// Animate streak count
animateCount(streakCount, data.streak);
// Streak icon animation
if (data.streak > 0) {
streakIcon.classList.add('animate');
setTimeout(() => streakIcon.classList.remove('animate'), 1000);
}
// Update streak message
if (data.streak > 0) {
streakMessage.textContent = motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)];
}
// Animate progress bar
const completedDays = data.weeklyProgress.filter(Boolean).length;
setTimeout(() => {
progressFill.style.width = (completedDays / 7 * 100) + '%';
}, 300);
daysCompleted.textContent = completedDays;
// Render week grid
renderWeekGrid(data.weeklyProgress);
// Render achievements
renderAchievements(data.achievements);
// Set habit toggle state
habitToggle.checked = data.habitReminderEnabled;
// Random daily tip
const randomTip = dailyTips[Math.floor(Math.random() * dailyTips.length)];
document.getElementById('tipText').textContent = `"${randomTip.text}"`;
document.querySelector('.tip-author').textContent = `${randomTip.author}`;
// Habit toggle handler
habitToggle.addEventListener('change', (e) => {
data.habitReminderEnabled = e.target.checked;
saveData(data);
showToast(e.target.checked ? '🔔' : '🔕', e.target.checked ? 'Reminders enabled!' : 'Reminders disabled');
});
// Achievement click handler (for demo unlock)
achievementsGrid.addEventListener('click', (e) => {
const achievement = e.target.closest('.achievement');
if (achievement && !achievement.classList.contains('unlocked')) {
// Simulate unlock for demo
const achId = achievement.dataset.id;
const achData = data.achievements.find(a => a.id === achId);
if (achData) {
achData.unlocked = true;
saveData(data);
animateBadgeUnlock(achId);
triggerConfetti();
showToast(achData.icon, `${achData.name} unlocked!`);
renderAchievements(data.achievements);
}
}
});
}
// Run on load
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>