Initial commit
This commit is contained in:
@@ -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
CONCEPT.md
Normal file
30
CONCEPT.md
Normal file
@@ -0,0 +1,30 @@
|
||||
[제품 기획안] 지능형 디지털 자산 관리 솔루션 v3
|
||||
AI 기반 능동적 구조 제안 및 직관적 UX를 통한 데이터 정리 혁신
|
||||
1. 제품 핵심 가치
|
||||
본 서비스는 단순히 쌓여있는 파일을 지우는 소극적 정리를 넘어, 사용자의 작업 맥락을 분석하여 최적의
|
||||
디지털 환경을 선제적으로 구축해주는 지능형 솔루션입니다.
|
||||
2. 핵심 기능 상세
|
||||
① 로컬 AI 기반 지능형 분류 및 보안
|
||||
· 기기 내 단독 처리(On-device): 모든 학습과 분석이 기기 내에서만 작동하여 완벽한 개인정보 보호를
|
||||
실현합니다.
|
||||
· 패턴 학습: 파일 사용 시간, 빈도, 확장자 등을 학습하여 개인 맞춤형 폴더 구조를 생성합니다.
|
||||
② 분류 구조 시각화 및 능동적 구조 제시 (Key Update)
|
||||
· 사용자 편의 중심 가이드: AI가 분석한 파일 패턴을 바탕으로 추천 폴더 구조를 시각적으로 제시합니
|
||||
다. 사용자는 제안 내용을 확인 후 원하는 항목만 선택적으로 적용할 수 있습니다.
|
||||
· 시각적 맥락 연결: 파일 간 연관 관계를 한눈에 파악할 수 있도록 구조화된 뷰를 제공합니다.
|
||||
· 단계적 적용: 제시된 구조를 사용자가 검토 후 항목별로 승인하여 파일 시스템을 정리할 수 있습니다.
|
||||
"AI가 제안하는 구조를 참고하고, 사용자가 최종 결정하는 방식으로 안전하게 정리가 완료됩니다."
|
||||
③ 직관적 스와이프 UX 및 안전장치
|
||||
· 단순한 조작: 좌/우 스와이프만으로 보관과 삭제를 결정하는 직관적인 인터페이스를 제공합니다.
|
||||
· 실수 방지: 고령층 등 IT 취약계층도 안심하고 쓸 수 있도록 강력한 복구(되돌리기) 기능을 탑재했습니
|
||||
다.
|
||||
④ 게이미피케이션 기반 습관 형성 알림
|
||||
· 지속적 동기부여: 듀오링고 방식의 알림 시스템을 통해 일상 속에서 정리 습관이 자연스럽게 형성되도
|
||||
록 유도합니다.
|
||||
3. 기능 요약표
|
||||
구분 핵심 기능 기대 효과 (Value)
|
||||
시각화/편의 능동적 구조 제안 및 시각화 정리 구조 설계 고민 해소, 관리 편의성 극대
|
||||
화
|
||||
보안/지능 기기 내 단독 처리 AI 개인정보 완벽 보호 및 오프라인 환경 작동
|
||||
사용성 스와이프 UX 낮은 학습 곡선, 전 연령층 접근성 확보
|
||||
지속성 정리 습관 알림 디지털 공간의 청결 상태 상시 유지
|
||||
2651
index.html
Normal file
2651
index.html
Normal file
File diff suppressed because it is too large
Load Diff
1295
scene_ai_classification.html
Normal file
1295
scene_ai_classification.html
Normal file
File diff suppressed because it is too large
Load Diff
1020
scene_gamification.html
Normal file
1020
scene_gamification.html
Normal file
File diff suppressed because it is too large
Load Diff
924
scene_swipe.html
Normal file
924
scene_swipe.html
Normal file
@@ -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>
|
||||
1179
scene_visualization.html
Normal file
1179
scene_visualization.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user