Files
chakmate/scene_swipe.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

414 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>스와이프 모드 - Chakmate</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
<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: {
'float': 'float 3s ease-in-out infinite',
'fade-slide-up': 'fadeSlideUp 0.5s ease forwards',
'slide-in': 'slideIn 0.5s ease forwards',
'bounce-in': 'bounceIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards',
'pulse-slow': 'pulse 2s ease-in-out infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
fadeSlideUp: {
from: { opacity: '0', transform: 'translateY(20px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
slideIn: {
from: { opacity: '0', transform: 'translateY(30px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
bounceIn: {
'0%': { transform: 'scale(0)' },
'50%': { transform: 'scale(1.1)' },
'100%': { transform: 'scale(1)' },
},
pulse: {
'0%, 100%': { opacity: '0.5', transform: 'scale(1)' },
'50%': { opacity: '1', transform: 'scale(1.05)' },
},
shimmer: {
'0%': { transform: 'translateX(-100%) translateY(-100%)' },
'100%': { transform: 'translateX(100%) translateY(100%)' },
},
cardExitLeft: {
to: { transform: 'translateX(-150%) rotate(-15deg)', opacity: '0' },
},
cardExitRight: {
to: { transform: 'translateX(150%) rotate(15deg)', opacity: '0' },
},
cardEnter: {
from: { transform: 'scale(0.9) translateY(30px)', opacity: '0' },
to: { transform: 'scale(1) translateY(0)', opacity: '1' },
},
},
},
},
}
</script>
<style>
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%); }
100% { transform: translateX(100%) translateY(100%); }
}
@keyframes cardExitLeft {
to { transform: translateX(-150%) rotate(-15deg); opacity: 0; }
}
@keyframes cardExitRight {
to { transform: translateX(150%) rotate(15deg); opacity: 0; }
}
@keyframes cardEnter {
from { transform: scale(0.9) translateY(30px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
</style>
</head>
<body class="font-sans bg-surface-primary text-text-primary min-h-screen antialiased">
<!-- 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>
<symbol id="icon-arrow-uturn-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 7v6h6M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
</symbol>
<symbol id="icon-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</symbol>
<symbol id="icon-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</symbol>
</svg>
<header class="flex items-center justify-between px-6 py-4 bg-surface-card shadow-sm sticky top-0 z-50">
<button class="flex items-center gap-2 px-4 py-2 bg-surface-secondary rounded-full text-text-secondary text-sm font-medium transition-colors duration-150 hover:bg-primary-light hover:text-white" onclick="window.location.href='index.html'">
<svg class="w-5 h-5"><use href="#icon-arrow-left"></use></svg>
뒤로
</button>
<h1 class="font-display text-lg font-semibold text-text-primary">스와이프 모드</h1>
<button class="flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-primary to-primary-dark rounded-full text-white text-sm font-semibold shadow-md transition-all duration-250 hover:-translate-y-0.5 hover:shadow-lg disabled:opacity-40 disabled:cursor-not-allowed disabled:transform-none" id="undoBtn" disabled>
<svg class="w-4 h-4"><use href="#icon-arrow-uturn-left"></use></svg>
실행 취소
</button>
</header>
<main class="flex flex-col min-h-[calc(100vh-70px)] px-6">
<div class="text-center mb-8">
<h2 class="text-[28px] font-bold text-text-primary mb-1" id="currentFileName">project_proposal.pdf</h2>
<p class="text-text-muted text-sm">왼쪽으로 스와이프하면 삭제, 오른쪽으로 스와이프하면 보관</p>
</div>
<div class="flex-1 flex flex-col items-center justify-center relative py-8">
<div class="relative w-full max-w-[320px] h-[420px]" id="cardStack"></div>
<div class="hidden flex-col items-center justify-center text-center p-16" id="emptyState">
<div class="w-[120px] h-[120px] bg-gradient-to-br from-primary to-secondary rounded-full flex items-center justify-center mb-6 animate-pulse-slow shadow-glow">
<svg class="w-[60px] h-[60px] text-white"><use href="#icon-check"></use></svg>
</div>
<h2 class="text-[28px] font-bold text-text-primary mb-3">모두 완료!</h2>
<p class="text-text-muted text-base mb-6">모든 파일을 검토했습니다</p>
<button class="flex items-center gap-2 px-6 py-4 bg-gradient-to-r from-primary to-primary-dark rounded-full text-white text-base font-semibold shadow-md transition-all duration-250 hover:-translate-y-0.5 hover:shadow-lg" onclick="resetFiles()">
다시 시작
</button>
</div>
</div>
<div class="flex justify-center gap-6 mt-8" id="actionButtons">
<button class="w-[80px] h-[80px] rounded-full bg-gradient-to-br from-accent-danger to-red-500 text-white flex items-center justify-center shadow-md transition-transform duration-500 active:scale-90" onclick="handleSwipe('delete')">
<svg class="w-9 h-9"><use href="#icon-trash"></use></svg>
</button>
<button class="w-[80px] h-[80px] rounded-full bg-gradient-to-br from-accent to-emerald-500 text-white flex items-center justify-center shadow-md transition-transform duration-500 active:scale-90" onclick="handleSwipe('keep')">
<svg class="w-9 h-9"><use href="#icon-check"></use></svg>
</button>
</div>
</main>
<footer class="p-5 bg-surface-card rounded-t-3xl shadow-[0_-4px_20px_rgba(30,27,46,0.08)]">
<div class="flex items-center gap-4">
<div class="flex-1 h-2 bg-surface-secondary rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-secondary rounded-full transition-all duration-500" id="progressFill" style="width: 0%"></div>
</div>
<div class="font-display text-sm font-semibold text-primary min-w-[60px] text-right" id="progressText">0%</div>
</div>
</footer>
<div class="fixed top-[90px] left-1/2 -translate-x-1/2 bg-text-primary text-surface-card px-6 py-3 rounded-full text-sm font-medium shadow-lg opacity-0 pointer-events-none transition-all duration-250 z-[200]" id="toast">파일 보관됨</div>
<script>
const mockFiles = [
{ id: 1, name: 'project_proposal.pdf', type: 'pdf', size: '2.4 MB', date: '2024-01-15', icon: '📄' },
{ id: 2, name: 'meeting_notes.docx', type: 'doc', size: '156 KB', date: '2024-01-14', icon: '📝' },
{ id: 3, name: 'vacation_photo.jpg', type: 'image', size: '3.2 MB', date: '2024-01-13', icon: '🖼️' },
{ id: 4, name: 'budget_2024.xlsx', type: 'excel', size: '89 KB', date: '2024-01-12', icon: '📊' },
{ id: 5, name: 'presentation_final.pptx', type: 'ppt', size: '5.2 MB', date: '2024-01-11', icon: '📽️' },
{ id: 6, name: 'backup_2023.zip', type: 'archive', size: '156 MB', date: '2024-01-10', icon: '📦' },
{ id: 7, name: 'contract_signed.pdf', type: 'pdf', size: '1.1 MB', date: '2024-01-09', icon: '📋' },
];
let files = [...mockFiles];
let historyStack = [];
let currentIndex = 0;
const cardStack = document.getElementById('cardStack');
const emptyState = document.getElementById('emptyState');
const actionButtons = document.getElementById('actionButtons');
const undoBtn = document.getElementById('undoBtn');
const toast = document.getElementById('toast');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const currentFileName = document.getElementById('currentFileName');
function getFileTypeInfo(type) {
const types = {
pdf: { label: 'PDF', color: '#ef4444' },
doc: { label: 'Doc', color: '#3b82f6' },
image: { label: 'Image', color: '#8b5cf6' },
excel: { label: 'Excel', color: '#10b981' },
ppt: { label: 'PPT', color: '#f97316' },
archive: { label: 'Archive', color: '#6b7280' },
};
return types[type] || { label: 'File', color: '#6366f1' };
}
function createCard(file) {
const typeInfo = getFileTypeInfo(file.type);
return `
<div class="file-card absolute w-full h-full bg-surface-card rounded-[28px] shadow-lg flex flex-col overflow-hidden touch-pan-y cursor-grab select-none transition-transform duration-100" data-id="${file.id}" style="transform: translateX(0) rotate(0deg);">
<div class="card-overlay absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-150 pointer-events-none rounded-[28px] z-10" id="overlayDelete">
<span class="font-display text-[28px] font-bold text-white uppercase tracking-[3px]">삭제</span>
</div>
<div class="card-overlay absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-150 pointer-events-none rounded-[28px] z-10" id="overlayKeep">
<span class="font-display text-[28px] font-bold text-white uppercase tracking-[3px]">보관</span>
</div>
<div class="card-header px-6 py-5 bg-gradient-to-br from-primary to-primary-dark text-white flex items-center justify-between">
<div class="flex items-center gap-2 bg-white/20 px-3 py-2 rounded-full text-xs font-semibold uppercase">
<span class="text-base">${file.icon}</span>
<span>${typeInfo.label}</span>
</div>
<span class="text-xs opacity-90">${file.size}</span>
</div>
<div class="card-body flex-1 flex flex-col items-center justify-center px-8">
<div class="w-[120px] h-[120px] bg-surface-secondary rounded-3xl flex items-center justify-center mb-6 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-transparent via-white/30 to-transparent animate-shimmer"></div>
<span class="text-[64px] relative z-10">${file.icon}</span>
</div>
<h3 class="font-display text-xl font-semibold text-text-primary text-center mb-3 leading-tight break-all">${file.name}</h3>
<p class="text-text-muted text-sm flex items-center gap-2">
<svg class="w-4 h-4"><use href="#icon-calendar"></use></svg>
${file.date}
</p>
</div>
<div class="card-footer px-6 py-4 bg-surface-secondary flex justify-center gap-6">
<div class="flex items-center gap-2 text-text-muted text-xs">
<span class="text-lg text-text-secondary">←</span>
<span>삭제</span>
</div>
<div class="flex items-center gap-2 text-text-muted text-xs">
<span>보관</span>
<span class="text-lg text-text-secondary">→</span>
</div>
</div>
</div>
`;
}
function renderCards() {
if (files.length === 0) {
cardStack.innerHTML = '';
emptyState.classList.remove('hidden');
emptyState.classList.add('flex');
actionButtons.style.display = 'none';
currentFileName.textContent = '모든 파일 검토 완료';
return;
}
emptyState.classList.add('hidden');
emptyState.classList.remove('flex');
actionButtons.style.display = 'flex';
const topFile = files[0];
currentFileName.textContent = topFile.name;
cardStack.innerHTML = createCard(topFile);
const card = cardStack.querySelector('.file-card');
initSwipe(card);
}
let startX = 0;
let currentX = 0;
let isDragging = false;
function initSwipe(card) {
if (!card) return;
card.addEventListener('mousedown', (e) => startDrag(e));
card.addEventListener('touchstart', (e) => startDrag(e), { passive: true });
document.addEventListener('mousemove', (e) => moveDrag(e));
document.addEventListener('touchmove', (e) => moveDrag(e), { passive: true });
document.addEventListener('mouseup', () => endDrag(card));
document.addEventListener('touchend', () => endDrag(card));
}
function startDrag(e) {
isDragging = true;
startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
currentX = 0;
}
function moveDrag(e) {
if (!isDragging) return;
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
currentX = clientX - startX;
const card = cardStack.querySelector('.file-card');
if (!card) return;
const rotation = currentX * 0.05;
card.style.transform = `translateX(${currentX}px) rotate(${rotation}deg)`;
const overlayDelete = card.querySelector('#overlayDelete');
const overlayKeep = card.querySelector('#overlayKeep');
if (currentX < -50) {
overlayDelete.style.opacity = Math.min(Math.abs(currentX) / 100, 1);
overlayDelete.classList.add('bg-gradient-to-br', 'from-red-400/95', 'to-red-600/95');
overlayKeep.style.opacity = 0;
} else if (currentX > 50) {
overlayKeep.style.opacity = Math.min(currentX / 100, 1);
overlayKeep.classList.add('bg-gradient-to-br', 'from-emerald-400/95', 'to-emerald-600/95');
overlayDelete.style.opacity = 0;
} else {
overlayDelete.style.opacity = 0;
overlayKeep.style.opacity = 0;
}
}
function endDrag(card) {
if (!isDragging) return;
isDragging = false;
if (currentX < -150) {
animateCard(card, 'left');
} else if (currentX > 150) {
animateCard(card, 'right');
} else {
card.style.transform = 'translateX(0) rotate(0deg)';
const overlayDelete = card.querySelector('#overlayDelete');
const overlayKeep = card.querySelector('#overlayKeep');
overlayDelete.style.opacity = 0;
overlayKeep.style.opacity = 0;
}
currentX = 0;
}
function animateCard(card, direction) {
if (direction === 'left') {
card.classList.add('animate-card-exit-left');
card.style.opacity = '0';
} else {
card.classList.add('animate-card-exit-right');
card.style.opacity = '0';
}
setTimeout(() => {
handleSwipe(direction === 'left' ? 'delete' : 'keep');
}, 300);
}
function handleSwipe(action) {
if (files.length === 0) return;
const deletedFile = files.shift();
historyStack.push(deletedFile);
undoBtn.disabled = false;
updateProgress();
renderCards();
showToast(action === 'delete' ? '파일 삭제됨' : '파일 보관됨');
}
function undo() {
if (historyStack.length === 0) return;
const restoredFile = historyStack.pop();
files.unshift(restoredFile);
undoBtn.disabled = historyStack.length === 0;
updateProgress();
renderCards();
showToast('실행 취소됨');
}
function updateProgress() {
const total = mockFiles.length;
const current = files.length;
const progress = Math.round(((total - current) / total) * 100);
progressFill.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
}
function showToast(message) {
toast.textContent = message;
toast.classList.add('opacity-100', 'translate-y-0');
toast.classList.remove('opacity-0', '-translate-y-5');
setTimeout(() => {
toast.classList.remove('opacity-100', 'translate-y-0');
toast.classList.add('opacity-0', '-translate-y-5');
}, 2000);
}
function resetFiles() {
files = [...mockFiles];
historyStack = [];
undoBtn.disabled = true;
updateProgress();
renderCards();
showToast('초기화됨');
}
undoBtn.addEventListener('click', undo);
renderCards();
updateProgress();
</script>
</body>
</html>