Files
chakmate/scene_swipe.html
2026-05-18 14:52:43 +09:00

991 lines
27 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">
<style>
:root {
/* Blue Light Puzzle Theme - Intelligent, Precise, Cool */
--primary: #3b82f6;
--primary-light: #60a5fa;
--primary-dark: #1d4ed8;
--secondary: #06b6d4;
--secondary-light: #22d3ee;
--accent: #0ea5e9;
--accent-warn: #38bdf8;
--accent-danger: #f472b6;
--bg-primary: #f8fafc;
--bg-secondary: #e2e8f0;
--bg-card: #ffffff;
--bg-overlay: rgba(14, 165, 233, 0.08);
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--shadow-sm: 0 2px 8px rgba(14, 165, 233, 0.06);
--shadow-md: 0 4px 20px rgba(14, 165, 233, 0.08);
--shadow-lg: 0 8px 40px rgba(14, 165, 233, 0.12);
--shadow-glow: 0 0 30px rgba(14, 165, 233, 0.25);
--shadow-blue: 0 4px 20px rgba(59, 130, 246, 0.3);
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-xl: 28px;
--radius-full: 9999px;
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 400ms ease;
--transition-bounce: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
[data-theme="dark"] {
--bg-primary: #0a0e1a;
--bg-secondary: #111827;
--bg-card: #1e293b;
--bg-overlay: rgba(14, 165, 233, 0.12);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.6);
--shadow-glow: 0 0 30px rgba(14, 165, 233, 0.3);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 { font-family: 'Outfit', sans-serif; }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-8);
border-radius: var(--radius-full);
font-size: 1rem;
font-weight: 600;
font-family: 'Outfit', sans-serif;
cursor: pointer;
border: none;
transition: all var(--transition-base);
min-height: 56px;
min-width: 160px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: var(--shadow-blue);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(59, 130, 246, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
.btn-secondary:hover {
background: var(--bg-secondary);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
min-width: auto;
padding: var(--space-3) var(--space-4);
}
.btn-icon {
width: 56px;
height: 56px;
padding: 0;
border-radius: var(--radius-lg);
}
.btn svg {
width: 20px;
height: 20px;
stroke: currentColor;
fill: none;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-6);
background: var(--bg-card);
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 100;
}
.back-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
background: var(--bg-secondary);
border: none;
border-radius: var(--radius-full);
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
}
.back-btn:hover { background: var(--primary-light); color: white; }
.header-title {
font-family: 'Outfit', sans-serif;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.undo-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border: none;
border-radius: var(--radius-full);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
box-shadow: var(--shadow-md), 0 4px 15px rgba(14, 165, 233, 0.3);
transition: var(--transition-base);
}
.undo-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg), 0 6px 20px rgba(14, 165, 233, 0.4);
}
.undo-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
/* Main Container */
.main-container {
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px);
padding: var(--space-6);
}
/* Progress Header */
.progress-header {
text-align: center;
margin-bottom: var(--space-8);
}
.progress-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.progress-header p {
color: var(--text-muted);
font-size: 15px;
}
/* Card Stack */
.card-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: var(--space-8) 0;
}
.card-stack {
position: relative;
width: 100%;
max-width: 320px;
height: 420px;
}
/* File Card */
.file-card {
position: absolute;
width: 100%;
height: 100%;
background: var(--bg-card);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
overflow: hidden;
touch-action: pan-y;
cursor: grab;
user-select: none;
transition: transform 0.1s ease, box-shadow 0.2s ease;
}
.file-card:active { cursor: grabbing; }
/* Card Visual Stack Effect */
.file-card::before {
content: '';
position: absolute;
top: 8px;
left: 8px;
right: 8px;
bottom: -8px;
background: var(--bg-secondary);
border-radius: var(--radius-xl);
z-index: -1;
opacity: 0.5;
}
.file-card::after {
content: '';
position: absolute;
top: 4px;
left: 4px;
right: 4px;
bottom: -4px;
background: var(--bg-secondary);
border-radius: var(--radius-xl);
z-index: -2;
opacity: 0.3;
}
/* Card Header with Type Badge */
.card-header {
padding: var(--space-5) var(--space-6);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
display: flex;
align-items: center;
justify-content: space-between;
}
.file-type-badge {
display: flex;
align-items: center;
gap: var(--space-2);
background: rgba(255,255,255,0.2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.file-type-badge .icon { font-size: 16px; }
.file-size-badge {
font-size: 12px;
opacity: 0.9;
}
/* Card Body */
.card-body {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-8);
}
.file-icon-container {
width: 120px;
height: 120px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-6);
position: relative;
overflow: hidden;
}
.file-icon-container::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent 40%, rgba(255,255,255,0.3) 50%, transparent 60%);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%); }
100% { transform: translateX(100%) translateY(100%); }
}
.file-icon-container .icon { font-size: 64px; }
.file-name {
font-family: 'Outfit', sans-serif;
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
text-align: center;
margin-bottom: var(--space-3);
line-height: 1.3;
word-break: break-word;
}
.file-date {
color: var(--text-muted);
font-size: 14px;
display: flex;
align-items: center;
gap: var(--space-2);
}
.file-date svg { width: 16px; height: 16px; }
/* Swipe Indicators */
.swipe-indicators {
position: absolute;
top: 50%;
left: var(--space-4);
right: var(--space-4);
display: flex;
justify-content: space-between;
transform: translateY(-50%);
pointer-events: none;
z-index: 10;
}
.swipe-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 50%;
opacity: 0;
transform: scale(0.5);
transition: all var(--transition-base);
}
.swipe-indicator.delete {
background: var(--accent-danger);
color: white;
}
.swipe-indicator.keep {
background: var(--accent);
color: white;
}
.swipe-indicator.active {
opacity: 1;
transform: scale(1);
}
.swipe-indicator svg { width: 28px; height: 28px; }
/* Card Footer */
.card-footer {
padding: var(--space-4) var(--space-6);
background: var(--bg-secondary);
display: flex;
justify-content: center;
gap: var(--space-6);
}
.swipe-hint {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--text-muted);
font-size: 13px;
}
.swipe-hint .arrow {
font-size: 18px;
color: var(--text-secondary);
}
/* Action Buttons Alternative */
.action-buttons {
display: flex;
justify-content: center;
gap: var(--space-6);
margin-top: var(--space-8);
}
.action-btn {
width: 80px;
height: 80px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition-bounce);
box-shadow: var(--shadow-md);
}
.action-btn:active {
transform: scale(0.9);
}
.action-btn.delete {
background: linear-gradient(135deg, var(--accent-danger), #ef4444);
color: white;
}
.action-btn.keep {
background: linear-gradient(135deg, var(--accent), #10b981);
color: white;
}
.action-btn svg { width: 36px; height: 36px; }
/* Swipe Overlay on Card */
.card-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.15s ease;
pointer-events: none;
border-radius: var(--radius-xl);
z-index: 5;
}
.card-overlay.delete {
background: linear-gradient(135deg, rgba(248,113,113,0.95), rgba(239,68,68,0.95));
}
.card-overlay.keep {
background: linear-gradient(135deg, rgba(52,211,153,0.95), rgba(16,185,129,0.95));
}
.card-overlay.show { opacity: 1; }
.card-overlay .action-label {
font-family: 'Outfit', sans-serif;
font-size: 28px;
font-weight: 700;
color: white;
text-transform: uppercase;
letter-spacing: 3px;
}
/* Progress Footer */
.progress-footer {
padding: var(--space-5);
background: var(--bg-card);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
box-shadow: 0 -4px 20px rgba(30, 27, 46, 0.08);
}
.progress-bar-container {
display: flex;
align-items: center;
gap: var(--space-4);
}
.progress-bar-wrapper {
flex: 1;
height: 8px;
background: var(--bg-secondary);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
border-radius: var(--radius-full);
transition: width 0.5s var(--transition-bounce);
}
.progress-text {
font-family: 'Outfit', sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--primary);
min-width: 60px;
text-align: right;
}
/* Empty State */
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--space-16);
}
.empty-state.show { display: flex; }
.empty-state .icon-wrapper {
width: 120px;
height: 120px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--space-6);
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.4); }
50% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(14, 165, 233, 0); }
}
.empty-state .icon-wrapper svg { width: 60px; height: 60px; color: white; }
.empty-state h2 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--space-3);
}
.empty-state p {
color: var(--text-muted);
font-size: 16px;
margin-bottom: var(--space-6);
}
.celebrate-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-6);
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border: none;
border-radius: var(--radius-full);
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: var(--shadow-md);
transition: var(--transition-base);
}
.celebrate-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* Toast */
.toast {
position: fixed;
top: 90px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--text-primary);
color: var(--bg-card);
padding: var(--space-3) var(--space-6);
border-radius: var(--radius-full);
font-size: 14px;
font-weight: 500;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: all var(--transition-base);
z-index: 200;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Animations */
.card-exit-left {
animation: cardExitLeft 0.4s ease forwards;
}
.card-exit-right {
animation: cardExitRight 0.4s ease forwards;
}
@keyframes cardExitLeft {
to { transform: translateX(-150%) rotate(-15deg); opacity: 0; }
}
@keyframes cardExitRight {
to { transform: translateX(150%) rotate(15deg); opacity: 0; }
}
.card-enter {
animation: cardEnter 0.4s var(--transition-bounce) forwards;
}
@keyframes cardEnter {
from { transform: scale(0.9) translateY(30px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
}
/* Responsive */
@media (max-width: 380px) {
.card-stack { height: 380px; }
.action-btn { width: 70px; height: 70px; }
.action-btn svg { width: 30px; height: 30px; }
}
</style>
</head>
<body>
<header class="header">
<button class="back-btn" onclick="window.location.href='index.html'">
<svg width="20" height="20" 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"/>
</svg>
뒤로
</button>
<h1 class="header-title">스와이프 모드</h1>
<button class="undo-btn" id="undoBtn" disabled>
<svg width="16" height="16" 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"/>
</svg>
실행 취소
</button>
</header>
<main class="main-container">
<div class="progress-header">
<h2 id="currentFileName">project_proposal.pdf</h2>
<p>왼쪽으로 스와이프하면 삭제, 오른쪽으로 스와이프하면 보관</p>
</div>
<div class="card-area">
<div class="card-stack" id="cardStack"></div>
<div class="empty-state" id="emptyState">
<div class="icon-wrapper">
<svg 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"/>
</svg>
</div>
<h2>모두 완료!</h2>
<p>모든 파일을 검토했습니다</p>
<button class="celebrate-btn" onclick="resetFiles()">
다시 시작
</button>
</div>
</div>
<div class="action-buttons" id="actionButtons">
<button class="action-btn delete" onclick="handleSwipe('delete')">
<svg 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"/>
</svg>
</button>
<button class="action-btn keep" onclick="handleSwipe('keep')">
<svg 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"/>
</svg>
</button>
</div>
</main>
<footer class="progress-footer">
<div class="progress-bar-container">
<div class="progress-bar-wrapper">
<div class="progress-bar-fill" id="progressFill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">0%</div>
</div>
</footer>
<div class="toast" 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" data-id="${file.id}" style="transform: translateX(0) rotate(0deg);">
<div class="card-overlay delete" id="overlayDelete">
<span class="action-label">삭제</span>
</div>
<div class="card-overlay keep" id="overlayKeep">
<span class="action-label">보관</span>
</div>
<div class="card-header" style="background: linear-gradient(135deg, ${typeInfo.color}, ${typeInfo.color}dd)">
<div class="file-type-badge">
<span class="icon">${file.icon}</span>
<span>${typeInfo.label}</span>
</div>
<span class="file-size-badge">${file.size}</span>
</div>
<div class="card-body">
<div class="file-icon-container">
<span class="icon">${file.icon}</span>
</div>
<div class="file-name">${file.name}</div>
<div class="file-date">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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"/>
</svg>
${file.date}
</div>
</div>
<div class="card-footer">
<span class="swipe-hint">
<span class="arrow">←</span> 삭제
</span>
<span class="swipe-hint">
보관 <span class="arrow">→</span>
</span>
</div>
</div>
`;
}
function renderCard() {
if (files.length === 0) {
cardStack.style.display = 'none';
emptyState.classList.add('show');
actionButtons.style.display = 'none';
return;
}
cardStack.style.display = 'block';
emptyState.classList.remove('show');
actionButtons.style.display = 'flex';
const file = files[0];
cardStack.innerHTML = createCard(file);
currentFileName.textContent = file.name;
setupSwipeGestures();
updateProgress();
}
function setupSwipeGestures() {
const card = cardStack.querySelector('.file-card');
if (!card) return;
let startX = 0;
let currentX = 0;
let isDragging = false;
const overlayDelete = card.querySelector('#overlayDelete');
const overlayKeep = card.querySelector('#overlayKeep');
const handleStart = (e) => {
startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
isDragging = true;
card.style.transition = 'none';
};
const handleMove = (e) => {
if (!isDragging) return;
e.preventDefault();
const x = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
currentX = x - startX;
const rotation = currentX * 0.05;
card.style.transform = `translateX(${currentX}px) rotate(${rotation}deg)`;
const threshold = 100;
const progress = Math.min(Math.abs(currentX) / threshold, 1);
if (currentX < 0) {
overlayDelete.style.opacity = progress;
overlayKeep.style.opacity = 0;
} else {
overlayKeep.style.opacity = progress;
overlayDelete.style.opacity = 0;
}
};
const handleEnd = () => {
if (!isDragging) return;
isDragging = false;
card.style.transition = 'transform 0.3s ease';
const threshold = 100;
if (currentX < -threshold) {
handleSwipe('delete');
} else if (currentX > threshold) {
handleSwipe('keep');
} else {
card.style.transform = 'translateX(0) rotate(0deg)';
overlayDelete.style.opacity = 0;
overlayKeep.style.opacity = 0;
}
currentX = 0;
};
card.addEventListener('mousedown', handleStart);
card.addEventListener('touchstart', handleStart, { passive: true });
document.addEventListener('mousemove', handleMove);
document.addEventListener('touchmove', handleMove, { passive: false });
document.addEventListener('mouseup', handleEnd);
document.addEventListener('touchend', handleEnd);
}
function handleSwipe(action) {
if (files.length === 0) return;
const card = cardStack.querySelector('.file-card');
const file = files.shift();
historyStack.push({ file, action });
undoBtn.disabled = false;
if (action === 'delete') {
card.classList.add('card-exit-left');
showToast('파일 삭제됨');
} else {
card.classList.add('card-exit-right');
showToast('파일 보관됨');
}
setTimeout(() => {
renderCard();
}, 400);
}
function undo() {
if (historyStack.length === 0) return;
const last = historyStack.pop();
files.unshift(last.file);
if (historyStack.length === 0) {
undoBtn.disabled = true;
}
showToast(`${last.file.name} 복원됨`);
renderCard();
}
function showToast(message) {
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
function updateProgress() {
const total = mockFiles.length;
const completed = total - files.length;
const percent = Math.round((completed / total) * 100);
progressFill.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
}
function resetFiles() {
files = [...mockFiles];
historyStack = [];
undoBtn.disabled = true;
renderCard();
}
undoBtn.addEventListener('click', undo);
renderCard();
</script>
</body>
</html>