Initial commit
This commit is contained in:
commit
e6ec2f2800
8 files changed
+7119
No files matched your search
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_1de4df0c6ffemBkTUjFd6dlnA5",
|
||||
"updatedAt": "2026-05-13T14:19:04.173Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-05-13T14:19:04.173Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_1df226e5affeTE1dOqBFb3E5DO",
|
||||
"updatedAt": "2026-05-13T14:14:01.763Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-05-13T14:14:01.763Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
[제품 기획안] 지능형 디지털 자산 관리 솔루션 v3
|
||||
AI 기반 능동적 구조 제안 및 직관적 UX를 통한 데이터 정리 혁신
|
||||
1. 제품 핵심 가치
|
||||
본 서비스는 단순히 쌓여있는 파일을 지우는 소극적 정리를 넘어, 사용자의 작업 맥락을 분석하여 최적의
|
||||
디지털 환경을 선제적으로 구축해주는 지능형 솔루션입니다.
|
||||
2. 핵심 기능 상세
|
||||
① 로컬 AI 기반 지능형 분류 및 보안
|
||||
· 기기 내 단독 처리(On-device): 모든 학습과 분석이 기기 내에서만 작동하여 완벽한 개인정보 보호를
|
||||
실현합니다.
|
||||
· 패턴 학습: 파일 사용 시간, 빈도, 확장자 등을 학습하여 개인 맞춤형 폴더 구조를 생성합니다.
|
||||
② 분류 구조 시각화 및 능동적 구조 제시 (Key Update)
|
||||
· 사용자 편의 중심 가이드: AI가 분석한 파일 패턴을 바탕으로 추천 폴더 구조를 시각적으로 제시합니
|
||||
다. 사용자는 제안 내용을 확인 후 원하는 항목만 선택적으로 적용할 수 있습니다.
|
||||
· 시각적 맥락 연결: 파일 간 연관 관계를 한눈에 파악할 수 있도록 구조화된 뷰를 제공합니다.
|
||||
· 단계적 적용: 제시된 구조를 사용자가 검토 후 항목별로 승인하여 파일 시스템을 정리할 수 있습니다.
|
||||
"AI가 제안하는 구조를 참고하고, 사용자가 최종 결정하는 방식으로 안전하게 정리가 완료됩니다."
|
||||
③ 직관적 스와이프 UX 및 안전장치
|
||||
· 단순한 조작: 좌/우 스와이프만으로 보관과 삭제를 결정하는 직관적인 인터페이스를 제공합니다.
|
||||
· 실수 방지: 고령층 등 IT 취약계층도 안심하고 쓸 수 있도록 강력한 복구(되돌리기) 기능을 탑재했습니
|
||||
다.
|
||||
④ 게이미피케이션 기반 습관 형성 알림
|
||||
· 지속적 동기부여: 듀오링고 방식의 알림 시스템을 통해 일상 속에서 정리 습관이 자연스럽게 형성되도
|
||||
록 유도합니다.
|
||||
3. 기능 요약표
|
||||
구분 핵심 기능 기대 효과 (Value)
|
||||
시각화/편의 능동적 구조 제안 및 시각화 정리 구조 설계 고민 해소, 관리 편의성 극대
|
||||
화
|
||||
보안/지능 기기 내 단독 처리 AI 개인정보 완벽 보호 및 오프라인 환경 작동
|
||||
사용성 스와이프 UX 낮은 학습 곡선, 전 연령층 접근성 확보
|
||||
지속성 정리 습관 알림 디지털 공간의 청결 상태 상시 유지
|
||||
+2651
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,924 @@
|
||||
<!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>스와이프 모드 - Chackly</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 {
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #f472b6;
|
||||
--secondary-light: #f9a8d4;
|
||||
--accent: #34d399;
|
||||
--accent-warn: #fbbf24;
|
||||
--accent-danger: #f87171;
|
||||
|
||||
--bg-primary: #faf9fb;
|
||||
--bg-secondary: #f3f2f7;
|
||||
--bg-card: #ffffff;
|
||||
--bg-overlay: rgba(99, 102, 241, 0.1);
|
||||
|
||||
--text-primary: #1e1b2e;
|
||||
--text-secondary: #6b6880;
|
||||
--text-muted: #9d99a8;
|
||||
|
||||
--shadow-sm: 0 2px 8px rgba(30, 27, 46, 0.06);
|
||||
--shadow-md: 0 4px 20px rgba(30, 27, 46, 0.08);
|
||||
--shadow-lg: 0 8px 40px rgba(30, 27, 46, 0.12);
|
||||
--shadow-glow: 0 0 30px rgba(99, 102, 241, 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: #0f0e17;
|
||||
--bg-secondary: #1a1825;
|
||||
--bg-card: #252336;
|
||||
--bg-overlay: rgba(99, 102, 241, 0.15);
|
||||
--text-primary: #f3f2f7;
|
||||
--text-secondary: #a8a4b8;
|
||||
--text-muted: #6b6880;
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
* { 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; }
|
||||
|
||||
/* 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(99, 102, 241, 0.3);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.undo-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg), 0 6px 20px rgba(99, 102, 241, 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(99, 102, 241, 0.4); }
|
||||
50% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(99, 102, 241, 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>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user