- ? 버튼 클릭 시 7개 탭 도움말 모달 표시 - 개요, 툴바, 카드 유형, 뷰 모드, 사이드바, 단축키, 워크플로우 - 각 기능별 상세 설명 및 빠른 시작 가이드
2258 lines
108 KiB
PHP
2258 lines
108 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', '디자인 인사이트')
|
||
|
||
@section('content')
|
||
<style>
|
||
/* ===== Design Insight Core ===== */
|
||
:root {
|
||
--di-sidebar: 280px;
|
||
--di-toolbar: 48px;
|
||
--di-blue: #3b82f6;
|
||
--di-indigo: #6366f1;
|
||
--di-green: #10b981;
|
||
--di-amber: #f59e0b;
|
||
--di-red: #ef4444;
|
||
--di-purple: #8b5cf6;
|
||
--di-pink: #ec4899;
|
||
--di-cyan: #0ea5e9;
|
||
--di-slate: #64748b;
|
||
--di-bg: #f8fafc;
|
||
--di-card-bg: #ffffff;
|
||
--di-border: #e2e8f0;
|
||
--di-text: #1e293b;
|
||
--di-text-secondary: #64748b;
|
||
--di-radius: 10px;
|
||
--di-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
||
--di-shadow-lg: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);
|
||
}
|
||
|
||
/* Wrap */
|
||
.di-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 56px);
|
||
background: var(--di-bg);
|
||
overflow: hidden;
|
||
font-family: 'Pretendard', -apple-system, sans-serif;
|
||
}
|
||
|
||
/* Toolbar */
|
||
.di-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
height: var(--di-toolbar);
|
||
padding: 0 16px;
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--di-border);
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
z-index: 20;
|
||
}
|
||
.di-toolbar .di-title-input {
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
min-width: 200px;
|
||
}
|
||
.di-toolbar .di-title-input:hover { background: #f1f5f9; }
|
||
.di-toolbar .di-title-input:focus { outline: 2px solid var(--di-blue); background: #fff; }
|
||
.di-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: 1px solid var(--di-border);
|
||
background: #fff;
|
||
color: var(--di-text);
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
}
|
||
.di-btn:hover { background: #f1f5f9; border-color: #cbd5e1; }
|
||
.di-btn.primary { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
|
||
.di-btn.primary:hover { background: #2563eb; }
|
||
.di-btn.sm { padding: 4px 8px; font-size: 11.5px; }
|
||
.di-btn.ghost { border: none; background: transparent; }
|
||
.di-btn.ghost:hover { background: #f1f5f9; }
|
||
.di-view-tabs {
|
||
display: flex;
|
||
gap: 2px;
|
||
background: #f1f5f9;
|
||
border-radius: 8px;
|
||
padding: 3px;
|
||
}
|
||
.di-view-tab {
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
color: var(--di-text-secondary);
|
||
transition: all .15s;
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
.di-view-tab:hover { color: var(--di-text); }
|
||
.di-view-tab.active { background: #fff; color: var(--di-text); box-shadow: 0 1px 2px rgba(0,0,0,.08); font-weight: 600; }
|
||
|
||
/* Body */
|
||
.di-body {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Sidebar */
|
||
.di-sidebar {
|
||
width: var(--di-sidebar);
|
||
background: #fff;
|
||
border-right: 1px solid var(--di-border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
overflow-y: auto;
|
||
transition: width .2s;
|
||
}
|
||
.di-sidebar.collapsed { width: 0; overflow: hidden; padding: 0; border-right: none; }
|
||
.di-sidebar-section {
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--di-border);
|
||
}
|
||
.di-sidebar-section h4 {
|
||
font-size: 10.5px;
|
||
font-weight: 700;
|
||
color: var(--di-text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-sidebar-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 7px 10px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
color: var(--di-text);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
}
|
||
.di-sidebar-item:hover { background: #f1f5f9; }
|
||
.di-sidebar-item.active { background: #eff6ff; color: var(--di-blue); font-weight: 600; }
|
||
.di-sidebar-item .cnt {
|
||
margin-left: auto;
|
||
font-size: 11px;
|
||
color: var(--di-text-secondary);
|
||
background: #f1f5f9;
|
||
padding: 1px 7px;
|
||
border-radius: 10px;
|
||
}
|
||
.di-tag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 3px 10px;
|
||
border-radius: 20px;
|
||
font-size: 11.5px;
|
||
cursor: pointer;
|
||
border: 1px solid var(--di-border);
|
||
background: #fff;
|
||
color: var(--di-text-secondary);
|
||
margin: 2px;
|
||
transition: all .12s;
|
||
}
|
||
.di-tag-chip:hover { border-color: var(--di-blue); color: var(--di-blue); }
|
||
.di-tag-chip.active { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
|
||
.di-search-input {
|
||
width: 100%;
|
||
padding: 7px 10px 7px 32px;
|
||
border: 1px solid var(--di-border);
|
||
border-radius: 8px;
|
||
font-size: 12.5px;
|
||
background: #f8fafc;
|
||
transition: all .15s;
|
||
}
|
||
.di-search-input:focus { outline: none; border-color: var(--di-blue); background: #fff; box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
|
||
|
||
/* Main */
|
||
.di-main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Category Tabs */
|
||
.di-cat-tabs {
|
||
display: flex;
|
||
gap: 1px;
|
||
padding: 10px 20px 0;
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--di-border);
|
||
overflow-x: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-cat-tab {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
color: var(--di-text-secondary);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
background: none;
|
||
border-top: none;
|
||
border-left: none;
|
||
border-right: none;
|
||
}
|
||
.di-cat-tab:hover { color: var(--di-text); }
|
||
.di-cat-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
|
||
|
||
/* Content */
|
||
.di-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* Board View */
|
||
.di-board {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
/* Card */
|
||
.di-card {
|
||
background: var(--di-card-bg);
|
||
border-radius: var(--di-radius);
|
||
border: 1px solid var(--di-border);
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
position: relative;
|
||
}
|
||
.di-card:hover { box-shadow: var(--di-shadow-lg); border-color: #cbd5e1; transform: translateY(-1px); }
|
||
.di-card .card-img {
|
||
width: 100%;
|
||
height: 180px;
|
||
object-fit: cover;
|
||
background: #f1f5f9;
|
||
display: block;
|
||
}
|
||
.di-card .card-img-placeholder {
|
||
width: 100%;
|
||
height: 180px;
|
||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #94a3b8;
|
||
font-size: 32px;
|
||
}
|
||
.di-card .card-body {
|
||
padding: 14px;
|
||
}
|
||
.di-card .card-type {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10.5px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
margin-bottom: 6px;
|
||
}
|
||
.di-card .card-type.ref { background: #eff6ff; color: #2563eb; }
|
||
.di-card .card-type.analysis { background: #fef3c7; color: #d97706; }
|
||
.di-card .card-type.pattern { background: #ecfdf5; color: #059669; }
|
||
.di-card .card-type.comparison { background: #fae8ff; color: #a855f7; }
|
||
.di-card .card-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
margin-bottom: 6px;
|
||
line-height: 1.4;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
.di-card .card-memo {
|
||
font-size: 12.5px;
|
||
color: var(--di-text-secondary);
|
||
line-height: 1.5;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-card .card-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-card .card-tag {
|
||
font-size: 10.5px;
|
||
padding: 2px 7px;
|
||
border-radius: 4px;
|
||
background: #f1f5f9;
|
||
color: var(--di-text-secondary);
|
||
}
|
||
.di-card .card-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
.di-card .card-rating { color: #f59e0b; font-size: 12px; letter-spacing: 1px; }
|
||
.di-card .card-date { font-size: 11px; color: #94a3b8; }
|
||
.di-card .card-pin {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: rgba(255,255,255,.9);
|
||
border-radius: 50%;
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity .15s;
|
||
border: 1px solid var(--di-border);
|
||
}
|
||
.di-card:hover .card-pin { opacity: 1; }
|
||
.di-card .card-pin.pinned { opacity: 1; color: var(--di-amber); }
|
||
|
||
/* Add Card Button */
|
||
.di-add-card {
|
||
background: var(--di-card-bg);
|
||
border-radius: var(--di-radius);
|
||
border: 2px dashed var(--di-border);
|
||
min-height: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
gap: 8px;
|
||
color: var(--di-text-secondary);
|
||
}
|
||
.di-add-card:hover { border-color: var(--di-blue); color: var(--di-blue); background: #f0f7ff; }
|
||
.di-add-card i { font-size: 28px; }
|
||
.di-add-card span { font-size: 13px; font-weight: 500; }
|
||
|
||
/* Gallery View */
|
||
.di-gallery {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.di-gallery .di-card .card-img { height: 220px; }
|
||
.di-gallery .di-card .card-body { padding: 10px; }
|
||
|
||
/* List View */
|
||
.di-list { display: flex; flex-direction: column; gap: 4px; }
|
||
.di-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 10px 14px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--di-border);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
}
|
||
.di-list-item:hover { background: #f8fafc; box-shadow: var(--di-shadow); }
|
||
.di-list-item .li-thumb {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 6px;
|
||
object-fit: cover;
|
||
background: #f1f5f9;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-list-item .li-thumb-empty {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 6px;
|
||
background: #f1f5f9;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #94a3b8;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-list-item .li-info { flex: 1; min-width: 0; }
|
||
.di-list-item .li-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.di-list-item .li-memo { font-size: 12px; color: var(--di-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.di-list-item .li-tags { display: flex; gap: 4px; flex-shrink: 0; }
|
||
.di-list-item .li-date { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
|
||
|
||
/* Modal */
|
||
.di-modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,.4);
|
||
z-index: 100;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.di-modal {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
width: 680px;
|
||
max-width: 95vw;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||
}
|
||
.di-modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--di-border);
|
||
}
|
||
.di-modal-header h3 { font-size: 16px; font-weight: 700; color: var(--di-text); }
|
||
.di-modal-body { padding: 20px; }
|
||
.di-modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding: 14px 20px;
|
||
border-top: 1px solid var(--di-border);
|
||
}
|
||
.di-field { margin-bottom: 16px; }
|
||
.di-field label {
|
||
display: block;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
margin-bottom: 5px;
|
||
}
|
||
.di-field input[type="text"],
|
||
.di-field textarea,
|
||
.di-field select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--di-border);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
transition: all .15s;
|
||
background: #fff;
|
||
}
|
||
.di-field input:focus, .di-field textarea:focus, .di-field select:focus {
|
||
outline: none;
|
||
border-color: var(--di-blue);
|
||
box-shadow: 0 0 0 3px rgba(59,130,246,.1);
|
||
}
|
||
.di-field textarea { min-height: 80px; resize: vertical; }
|
||
|
||
/* Image Drop Zone */
|
||
.di-drop-zone {
|
||
border: 2px dashed var(--di-border);
|
||
border-radius: 10px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
background: #fafbfc;
|
||
}
|
||
.di-drop-zone:hover, .di-drop-zone.dragover {
|
||
border-color: var(--di-blue);
|
||
background: #eff6ff;
|
||
}
|
||
.di-drop-zone img {
|
||
max-width: 100%;
|
||
max-height: 250px;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Before/After comparison */
|
||
.di-comparison {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
.di-comparison .comp-side {
|
||
border: 2px dashed var(--di-border);
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
background: #fafbfc;
|
||
min-height: 150px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.di-comparison .comp-side:hover { border-color: var(--di-blue); background: #eff6ff; }
|
||
.di-comparison .comp-side img { max-width: 100%; max-height: 200px; object-fit: contain; border-radius: 8px; }
|
||
.di-comparison .comp-label {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 3px 10px;
|
||
border-radius: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-comparison .comp-before .comp-label { background: #fef2f2; color: #dc2626; }
|
||
.di-comparison .comp-after .comp-label { background: #ecfdf5; color: #059669; }
|
||
|
||
/* CRAP Checklist */
|
||
.di-crap-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.di-crap-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--di-border);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
font-size: 12.5px;
|
||
}
|
||
.di-crap-item:hover { background: #f8fafc; }
|
||
.di-crap-item.pass { background: #ecfdf5; border-color: #a7f3d0; }
|
||
.di-crap-item.fail { background: #fef2f2; border-color: #fecaca; }
|
||
.di-crap-item.warn { background: #fffbeb; border-color: #fde68a; }
|
||
|
||
/* Rating Stars */
|
||
.di-stars {
|
||
display: flex;
|
||
gap: 2px;
|
||
}
|
||
.di-star {
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
color: #e2e8f0;
|
||
transition: color .1s;
|
||
}
|
||
.di-star.filled { color: #f59e0b; }
|
||
.di-star:hover { color: #fbbf24; }
|
||
|
||
/* Help Modal */
|
||
.di-help-modal { width: 740px; }
|
||
.di-help-modal .di-modal-body { padding: 0; }
|
||
.di-help-tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--di-border);
|
||
padding: 0 20px;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
}
|
||
.di-help-tab {
|
||
padding: 12px 16px;
|
||
font-size: 13px;
|
||
color: var(--di-text-secondary);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
white-space: nowrap;
|
||
background: none;
|
||
border-top: none; border-left: none; border-right: none;
|
||
transition: all .15s;
|
||
}
|
||
.di-help-tab:hover { color: var(--di-text); }
|
||
.di-help-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
|
||
.di-help-content { padding: 24px; max-height: 60vh; overflow-y: auto; }
|
||
.di-help-content h4 { font-size: 15px; font-weight: 700; color: var(--di-text); margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }
|
||
.di-help-content h4 i { font-size: 18px; color: var(--di-blue); }
|
||
.di-help-content p { font-size: 13px; color: var(--di-text-secondary); line-height: 1.7; margin: 0 0 16px; }
|
||
.di-help-section { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #f1f5f9; }
|
||
.di-help-section:last-child { border-bottom: none; margin-bottom: 0; }
|
||
.di-help-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.di-help-item {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
border: 1px solid #f1f5f9;
|
||
}
|
||
.di-help-item .h-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-help-item .h-body { flex: 1; }
|
||
.di-help-item .h-title { font-size: 13px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
|
||
.di-help-item .h-desc { font-size: 11.5px; color: var(--di-text-secondary); line-height: 1.5; }
|
||
.di-help-kbd {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px 8px;
|
||
background: #f1f5f9;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 4px;
|
||
font-size: 11.5px;
|
||
font-family: monospace;
|
||
color: var(--di-text);
|
||
margin: 2px;
|
||
}
|
||
.di-help-step {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
.di-help-step .step-num {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: var(--di-blue);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-help-step .step-body { flex: 1; padding-top: 3px; }
|
||
.di-help-step .step-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
|
||
.di-help-step .step-desc { font-size: 12.5px; color: var(--di-text-secondary); line-height: 1.6; }
|
||
|
||
/* Status Bar */
|
||
.di-statusbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 6px 20px;
|
||
background: #fff;
|
||
border-top: 1px solid var(--di-border);
|
||
font-size: 11.5px;
|
||
color: var(--di-text-secondary);
|
||
flex-shrink: 0;
|
||
}
|
||
.di-statusbar .stat { display: flex; align-items: center; gap: 4px; }
|
||
|
||
/* Toast */
|
||
.di-toast {
|
||
position: fixed;
|
||
bottom: 24px;
|
||
right: 24px;
|
||
background: #1e293b;
|
||
color: #fff;
|
||
padding: 10px 18px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
z-index: 200;
|
||
box-shadow: 0 8px 20px rgba(0,0,0,.2);
|
||
animation: diToastIn .3s ease;
|
||
}
|
||
@keyframes diToastIn { from { opacity: 0; transform: translateY(10px); } }
|
||
|
||
/* Empty State */
|
||
.di-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60px 20px;
|
||
color: var(--di-text-secondary);
|
||
text-align: center;
|
||
}
|
||
.di-empty i { font-size: 48px; margin-bottom: 16px; color: #cbd5e1; }
|
||
.di-empty h3 { font-size: 16px; font-weight: 600; color: var(--di-text); margin-bottom: 6px; }
|
||
.di-empty p { font-size: 13px; margin-bottom: 16px; }
|
||
|
||
/* Paste hint */
|
||
.di-paste-hint {
|
||
position: fixed;
|
||
bottom: 80px;
|
||
right: 24px;
|
||
background: #1e293b;
|
||
color: #fff;
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
z-index: 50;
|
||
opacity: .8;
|
||
}
|
||
.di-paste-hint kbd {
|
||
background: rgba(255,255,255,.2);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* Projects Modal */
|
||
.di-proj-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
border: 1px solid transparent;
|
||
}
|
||
.di-proj-item:hover { background: #f8fafc; border-color: var(--di-border); }
|
||
.di-proj-item.active { background: #eff6ff; border-color: var(--di-blue); }
|
||
.di-proj-item .proj-title { font-size: 13.5px; font-weight: 600; flex: 1; }
|
||
.di-proj-item .proj-count { font-size: 11px; color: var(--di-text-secondary); }
|
||
.di-proj-item .proj-date { font-size: 11px; color: #94a3b8; }
|
||
</style>
|
||
|
||
<!-- ===== Alpine.js App ===== -->
|
||
<div class="di-wrap" x-data="designInsight()" x-init="init()" @paste.window="handlePaste($event)" @keydown.window="handleKeydown($event)">
|
||
|
||
<!-- Toolbar -->
|
||
<div class="di-toolbar">
|
||
<button class="di-btn ghost" @click="sidebarOpen = !sidebarOpen" title="사이드바 토글">
|
||
<i class="ri-layout-left-line" style="font-size: 16px;"></i>
|
||
</button>
|
||
|
||
<input type="text" class="di-title-input"
|
||
x-model="currentProject.title"
|
||
@change="saveProject()"
|
||
placeholder="프로젝트 제목">
|
||
|
||
<div style="flex: 1;"></div>
|
||
|
||
<button class="di-btn sm" @click="saveProject()" title="저장 (Ctrl+S)">
|
||
<i class="ri-save-line"></i> 저장
|
||
</button>
|
||
<button class="di-btn sm" @click="showExportMenu = !showExportMenu" title="내보내기">
|
||
<i class="ri-download-line"></i> 내보내기
|
||
</button>
|
||
<template x-if="showExportMenu">
|
||
<div style="position: absolute; top: 44px; right: 80px; background: #fff; border: 1px solid var(--di-border); border-radius: 8px; box-shadow: var(--di-shadow-lg); z-index: 30; padding: 4px;"
|
||
@click.outside="showExportMenu = false">
|
||
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="exportJSON(); showExportMenu=false">
|
||
<i class="ri-code-s-slash-line"></i> JSON 내보내기
|
||
</button>
|
||
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="importJSON(); showExportMenu=false">
|
||
<i class="ri-upload-line"></i> JSON 가져오기
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="di-view-tabs">
|
||
<button class="di-view-tab" :class="viewMode === 'board' && 'active'" @click="viewMode = 'board'" title="보드 뷰">
|
||
<i class="ri-layout-grid-line"></i>
|
||
</button>
|
||
<button class="di-view-tab" :class="viewMode === 'gallery' && 'active'" @click="viewMode = 'gallery'" title="갤러리 뷰">
|
||
<i class="ri-image-line"></i>
|
||
</button>
|
||
<button class="di-view-tab" :class="viewMode === 'list' && 'active'" @click="viewMode = 'list'" title="리스트 뷰">
|
||
<i class="ri-list-unordered"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<button class="di-btn sm" @click="showProjectsModal = true" title="프로젝트 관리">
|
||
<i class="ri-folder-line"></i>
|
||
</button>
|
||
<button class="di-btn sm" @click="showHelpModal = true" title="도움말 (?)">
|
||
<i class="ri-question-line"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Category Tabs -->
|
||
<div class="di-cat-tabs">
|
||
<button class="di-cat-tab" :class="categoryFilter === 'all' && 'active'" @click="categoryFilter = 'all'">
|
||
전체 <span x-text="'(' + filteredCards.length + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
|
||
</button>
|
||
<template x-for="type in cardTypes" :key="type.code">
|
||
<button class="di-cat-tab" :class="categoryFilter === type.code && 'active'" @click="categoryFilter = type.code">
|
||
<span x-text="type.icon + ' ' + type.label"></span>
|
||
<span x-text="'(' + getCardCountByType(type.code) + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="di-body">
|
||
|
||
<!-- Sidebar -->
|
||
<div class="di-sidebar" :class="!sidebarOpen && 'collapsed'">
|
||
<!-- Search -->
|
||
<div class="di-sidebar-section">
|
||
<div style="position: relative;">
|
||
<i class="ri-search-line" style="position: absolute; left: 10px; top: 8px; font-size: 14px; color: #94a3b8;"></i>
|
||
<input type="text" class="di-search-input" x-model="searchQuery" placeholder="검색..." @keyup.escape="searchQuery = ''">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Categories -->
|
||
<div class="di-sidebar-section">
|
||
<h4>카테고리</h4>
|
||
<div class="di-sidebar-item" :class="screenFilter === 'all' && 'active'" @click="screenFilter = 'all'">
|
||
<span>전체</span>
|
||
<span class="cnt" x-text="currentProject.cards?.length || 0"></span>
|
||
</div>
|
||
<template x-for="cat in categories" :key="cat.code">
|
||
<div class="di-sidebar-item" :class="screenFilter === cat.code && 'active'" @click="screenFilter = cat.code">
|
||
<span x-text="cat.icon + ' ' + cat.label"></span>
|
||
<span class="cnt" x-text="getCardCountByCat(cat.code)"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Tags -->
|
||
<div class="di-sidebar-section">
|
||
<h4>태그</h4>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 2px;">
|
||
<template x-for="tag in allTags" :key="tag">
|
||
<span class="di-tag-chip" :class="selectedTags.includes(tag) && 'active'"
|
||
@click="toggleTag(tag)" x-text="tag"></span>
|
||
</template>
|
||
<template x-if="allTags.length === 0">
|
||
<span style="font-size: 12px; color: #94a3b8;">태그 없음</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sort -->
|
||
<div class="di-sidebar-section">
|
||
<h4>정렬</h4>
|
||
<select style="width: 100%; padding: 6px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12px;" x-model="sortBy" @change="$nextTick()">
|
||
<option value="newest">최신순</option>
|
||
<option value="oldest">오래된순</option>
|
||
<option value="rating">평점순</option>
|
||
<option value="title">이름순</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="di-main">
|
||
<div class="di-content">
|
||
|
||
<!-- Empty State -->
|
||
<template x-if="filteredCards.length === 0 && !searchQuery && selectedTags.length === 0 && screenFilter === 'all' && categoryFilter === 'all'">
|
||
<div class="di-empty">
|
||
<i class="ri-palette-line"></i>
|
||
<h3>인사이트를 수집해보세요</h3>
|
||
<p><kbd>Ctrl+V</kbd>로 스크린샷을 붙여넣거나, 아래 버튼으로 시작하세요</p>
|
||
<button class="di-btn primary" @click="openNewCardModal('reference')">
|
||
<i class="ri-add-line"></i> 첫 번째 카드 추가
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- No Results -->
|
||
<template x-if="filteredCards.length === 0 && (searchQuery || selectedTags.length > 0 || screenFilter !== 'all' || categoryFilter !== 'all')">
|
||
<div class="di-empty">
|
||
<i class="ri-search-line"></i>
|
||
<h3>검색 결과 없음</h3>
|
||
<p>필터를 변경하거나 검색어를 수정하세요</p>
|
||
<button class="di-btn" @click="clearFilters()">
|
||
<i class="ri-refresh-line"></i> 필터 초기화
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Board View -->
|
||
<template x-if="viewMode === 'board' && filteredCards.length > 0">
|
||
<div class="di-board">
|
||
<template x-for="card in filteredCards" :key="card.id">
|
||
<div class="di-card" @click="openEditCardModal(card)">
|
||
<!-- Pin -->
|
||
<div class="card-pin" :class="card.pinned && 'pinned'"
|
||
@click.stop="togglePin(card)" x-text="card.pinned ? '📌' : '📌'"></div>
|
||
|
||
<!-- Image -->
|
||
<template x-if="card.type === 'comparison'">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; height: 180px;">
|
||
<template x-if="card.beforeImage">
|
||
<img :src="card.beforeImage" style="width: 100%; height: 180px; object-fit: cover; border-right: 1px solid var(--di-border);">
|
||
</template>
|
||
<template x-if="!card.beforeImage">
|
||
<div class="card-img-placeholder" style="border-right: 1px solid var(--di-border);"><i class="ri-arrow-left-line"></i></div>
|
||
</template>
|
||
<template x-if="card.afterImage">
|
||
<img :src="card.afterImage" style="width: 100%; height: 180px; object-fit: cover;">
|
||
</template>
|
||
<template x-if="!card.afterImage">
|
||
<div class="card-img-placeholder"><i class="ri-arrow-right-line"></i></div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
<template x-if="card.type !== 'comparison' && card.image">
|
||
<img :src="card.image" class="card-img">
|
||
</template>
|
||
<template x-if="card.type !== 'comparison' && !card.image">
|
||
<div class="card-img-placeholder">
|
||
<i :class="getTypeIcon(card.type)"></i>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Body -->
|
||
<div class="card-body">
|
||
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type">
|
||
<span x-text="getTypeLabel(card.type)"></span>
|
||
</span>
|
||
<div class="card-title" x-text="card.title || '(제목 없음)'"></div>
|
||
<div class="card-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
|
||
<div class="card-tags" x-show="card.tags && card.tags.length > 0">
|
||
<template x-for="tag in (card.tags || []).slice(0, 4)" :key="tag">
|
||
<span class="card-tag" x-text="tag"></span>
|
||
</template>
|
||
<template x-if="(card.tags || []).length > 4">
|
||
<span class="card-tag" x-text="'+' + ((card.tags || []).length - 4)"></span>
|
||
</template>
|
||
</div>
|
||
<div class="card-footer">
|
||
<div class="card-rating" x-text="getRatingStars(card.rating || 0)"></div>
|
||
<div class="card-date" x-text="formatDate(card.createdAt)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Add New Card -->
|
||
<div class="di-add-card" @click="openNewCardModal('reference')">
|
||
<i class="ri-add-circle-line"></i>
|
||
<span>새 카드 추가</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Gallery View -->
|
||
<template x-if="viewMode === 'gallery' && filteredCards.length > 0">
|
||
<div class="di-gallery">
|
||
<template x-for="card in filteredCards" :key="card.id">
|
||
<div class="di-card" @click="openEditCardModal(card)">
|
||
<template x-if="card.image">
|
||
<img :src="card.image" class="card-img">
|
||
</template>
|
||
<template x-if="!card.image && card.beforeImage">
|
||
<img :src="card.beforeImage" class="card-img">
|
||
</template>
|
||
<template x-if="!card.image && !card.beforeImage">
|
||
<div class="card-img-placeholder"><i :class="getTypeIcon(card.type)"></i></div>
|
||
</template>
|
||
<div class="card-body">
|
||
<div class="card-title" x-text="card.title || '(제목 없음)'"></div>
|
||
<div class="card-rating" x-text="getRatingStars(card.rating || 0)"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div class="di-add-card" @click="openNewCardModal('reference')" style="min-height: 260px;">
|
||
<i class="ri-add-circle-line"></i>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- List View -->
|
||
<template x-if="viewMode === 'list' && filteredCards.length > 0">
|
||
<div class="di-list">
|
||
<template x-for="card in filteredCards" :key="card.id">
|
||
<div class="di-list-item" @click="openEditCardModal(card)">
|
||
<template x-if="card.image">
|
||
<img :src="card.image" class="li-thumb">
|
||
</template>
|
||
<template x-if="!card.image">
|
||
<div class="li-thumb-empty"><i :class="getTypeIcon(card.type)"></i></div>
|
||
</template>
|
||
<div class="li-info">
|
||
<div class="li-title">
|
||
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type"
|
||
x-text="getTypeLabel(card.type)" style="font-size: 10px; margin-right: 6px;"></span>
|
||
<span x-text="card.title || '(제목 없음)'"></span>
|
||
</div>
|
||
<div class="li-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
|
||
</div>
|
||
<div class="li-tags">
|
||
<template x-for="tag in (card.tags || []).slice(0, 3)" :key="tag">
|
||
<span class="card-tag" x-text="tag"></span>
|
||
</template>
|
||
</div>
|
||
<div class="card-rating" x-text="getRatingStars(card.rating || 0)" style="flex-shrink: 0;"></div>
|
||
<div class="li-date" x-text="formatDate(card.createdAt)"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Bar -->
|
||
<div class="di-statusbar">
|
||
<span class="stat"><i class="ri-sticky-note-line"></i> 카드 <strong x-text="currentProject.cards?.length || 0"></strong>개</span>
|
||
<span class="stat"><i class="ri-price-tag-3-line"></i> 태그 <strong x-text="allTags.length"></strong>개</span>
|
||
<span class="stat"><i class="ri-time-line"></i> 마지막 저장 <span x-text="lastSaved || '-'"></span></span>
|
||
<div style="flex:1;"></div>
|
||
<span class="di-paste-hint" style="position: static; opacity: .6; background: transparent; color: var(--di-text-secondary); padding: 0;">
|
||
<kbd>Ctrl+V</kbd> 붙여넣기
|
||
</span>
|
||
</div>
|
||
|
||
<!-- ===== Card Edit/Create Modal ===== -->
|
||
<template x-if="showCardModal">
|
||
<div class="di-modal-overlay" @click.self="closeCardModal()">
|
||
<div class="di-modal">
|
||
<div class="di-modal-header">
|
||
<h3 x-text="editingCard.id ? '카드 편집' : '새 카드'"></h3>
|
||
<div style="display: flex; gap: 6px; align-items: center;">
|
||
<template x-if="editingCard.id">
|
||
<button class="di-btn sm" style="color: var(--di-red); border-color: #fecaca;" @click="deleteCard(editingCard.id)">
|
||
<i class="ri-delete-bin-line"></i> 삭제
|
||
</button>
|
||
</template>
|
||
<button class="di-btn ghost" @click="closeCardModal()"><i class="ri-close-line" style="font-size: 18px;"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="di-modal-body">
|
||
<!-- Card Type -->
|
||
<div class="di-field">
|
||
<label>카드 유형</label>
|
||
<div style="display: flex; gap: 6px;">
|
||
<template x-for="type in cardTypes" :key="type.code">
|
||
<button class="di-btn sm" :class="editingCard.type === type.code && 'primary'"
|
||
@click="editingCard.type = type.code">
|
||
<span x-text="type.icon + ' ' + type.label"></span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Title -->
|
||
<div class="di-field">
|
||
<label>제목</label>
|
||
<input type="text" x-model="editingCard.title" placeholder="인사이트 제목">
|
||
</div>
|
||
|
||
<!-- Image (Reference / Analysis / Pattern) -->
|
||
<template x-if="editingCard.type !== 'comparison'">
|
||
<div class="di-field">
|
||
<label>이미지</label>
|
||
<div class="di-drop-zone"
|
||
@click="$refs.fileInput.click()"
|
||
@drop.prevent="handleDrop($event)"
|
||
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
|
||
@dragleave="$event.currentTarget.classList.remove('dragover')">
|
||
<template x-if="editingCard.image">
|
||
<img :src="editingCard.image">
|
||
</template>
|
||
<template x-if="!editingCard.image">
|
||
<div>
|
||
<i class="ri-image-add-line" style="font-size: 32px; color: #94a3b8; display: block; margin-bottom: 8px;"></i>
|
||
<span style="font-size: 13px; color: var(--di-text-secondary);">클릭 또는 드래그로 이미지 추가</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<input type="file" x-ref="fileInput" accept="image/*" style="display: none;" @change="handleFileSelect($event)">
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Before/After Images (Comparison) -->
|
||
<template x-if="editingCard.type === 'comparison'">
|
||
<div class="di-field">
|
||
<label>Before / After 이미지</label>
|
||
<div class="di-comparison">
|
||
<div class="comp-side comp-before"
|
||
@click="$refs.beforeInput.click()"
|
||
@drop.prevent="handleCompDrop($event, 'before')"
|
||
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
|
||
@dragleave="$event.currentTarget.classList.remove('dragover')">
|
||
<span class="comp-label">Before</span>
|
||
<template x-if="editingCard.beforeImage">
|
||
<img :src="editingCard.beforeImage">
|
||
</template>
|
||
<template x-if="!editingCard.beforeImage">
|
||
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
|
||
</template>
|
||
</div>
|
||
<div class="comp-side comp-after"
|
||
@click="$refs.afterInput.click()"
|
||
@drop.prevent="handleCompDrop($event, 'after')"
|
||
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
|
||
@dragleave="$event.currentTarget.classList.remove('dragover')">
|
||
<span class="comp-label">After</span>
|
||
<template x-if="editingCard.afterImage">
|
||
<img :src="editingCard.afterImage">
|
||
</template>
|
||
<template x-if="!editingCard.afterImage">
|
||
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<input type="file" x-ref="beforeInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'before')">
|
||
<input type="file" x-ref="afterInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'after')">
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Memo (Reference) -->
|
||
<template x-if="editingCard.type === 'reference'">
|
||
<div>
|
||
<div class="di-field">
|
||
<label>인사이트 메모</label>
|
||
<textarea x-model="editingCard.memo" placeholder="이 화면/패턴이 왜 좋은가? (또는 나쁜가?)"></textarea>
|
||
</div>
|
||
<div class="di-field">
|
||
<label>출처</label>
|
||
<input type="text" x-model="editingCard.source" placeholder="URL, 앱 이름, 서비스명 등">
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Analysis Fields -->
|
||
<template x-if="editingCard.type === 'analysis'">
|
||
<div>
|
||
<div class="di-field">
|
||
<label>CRAP 디자인 원칙 체크</label>
|
||
<div class="di-crap-grid">
|
||
<template x-for="p in designPrinciples" :key="p.key">
|
||
<div class="di-crap-item"
|
||
:class="getPrincipleStatus(p.key)"
|
||
@click="cyclePrinciple(p.key)">
|
||
<span x-text="p.icon" style="font-size: 16px;"></span>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 600; font-size: 12px;" x-text="p.label"></div>
|
||
<div style="font-size: 10.5px; color: var(--di-text-secondary);" x-text="p.desc"></div>
|
||
</div>
|
||
<span x-text="getPrincipleIcon(p.key)" style="font-size: 14px;"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="di-field">
|
||
<label>개선 제안</label>
|
||
<textarea x-model="editingCard.suggestion" placeholder="어떻게 개선할 수 있는가?"></textarea>
|
||
</div>
|
||
<div class="di-field">
|
||
<label>심각도</label>
|
||
<div style="display: flex; gap: 6px;">
|
||
<button class="di-btn sm" :class="editingCard.severity === 'info' && 'primary'" @click="editingCard.severity = 'info'">ℹ️ 정보</button>
|
||
<button class="di-btn sm" :class="editingCard.severity === 'warning' && 'primary'" @click="editingCard.severity = 'warning'" style="--di-blue: #f59e0b;">⚠️ 경고</button>
|
||
<button class="di-btn sm" :class="editingCard.severity === 'critical' && 'primary'" @click="editingCard.severity = 'critical'" style="--di-blue: #ef4444;">🔴 심각</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Pattern Fields -->
|
||
<template x-if="editingCard.type === 'pattern'">
|
||
<div>
|
||
<div class="di-field">
|
||
<label>사용처</label>
|
||
<input type="text" x-model="editingCard.usedInText" placeholder="수주 목록, 거래처 목록 (콤마 구분)">
|
||
</div>
|
||
<div class="di-field">
|
||
<label>구성 요소</label>
|
||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||
<template x-for="(comp, ci) in (editingCard.components || [])" :key="ci">
|
||
<div style="display: flex; gap: 6px; align-items: center;">
|
||
<input type="checkbox" x-model="comp.required">
|
||
<input type="text" x-model="comp.name" style="flex: 1; padding: 5px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12.5px;" placeholder="구성 요소명">
|
||
<button class="di-btn ghost sm" @click="editingCard.components.splice(ci, 1)"><i class="ri-close-line"></i></button>
|
||
</div>
|
||
</template>
|
||
<button class="di-btn sm" @click="editingCard.components = [...(editingCard.components || []), {name: '', required: true}]">
|
||
<i class="ri-add-line"></i> 구성 요소 추가
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="di-field">
|
||
<label>사용 가이드라인</label>
|
||
<textarea x-model="editingCard.guidelines" placeholder="이 패턴을 사용할 때 주의사항이나 가이드"></textarea>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Comparison Fields -->
|
||
<template x-if="editingCard.type === 'comparison'">
|
||
<div>
|
||
<div class="di-field">
|
||
<label>변경 포인트</label>
|
||
<textarea x-model="editingCard.changesText" placeholder="1. 탭 구조 → 섹션 접기/펼치기 변경 2. 좌우 2컬럼 → 단일 컬럼" style="min-height: 100px;"></textarea>
|
||
</div>
|
||
<div class="di-field">
|
||
<label>개선 효과</label>
|
||
<input type="text" x-model="editingCard.effect" placeholder="스크롤 40% 감소, 작업 완료 시간 단축">
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Common Fields -->
|
||
<div class="di-field">
|
||
<label>카테고리</label>
|
||
<select x-model="editingCard.category">
|
||
<template x-for="cat in categories" :key="cat.code">
|
||
<option :value="cat.code" x-text="cat.icon + ' ' + cat.label"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="di-field">
|
||
<label>태그 (콤마 구분)</label>
|
||
<input type="text" x-model="editingCard.tagsText" placeholder="대시보드, 카드, 레이아웃"
|
||
@keydown.enter.prevent>
|
||
</div>
|
||
|
||
<div class="di-field">
|
||
<label>평점</label>
|
||
<div class="di-stars">
|
||
<template x-for="s in [1,2,3,4,5]" :key="s">
|
||
<span class="di-star" :class="s <= (editingCard.rating || 0) && 'filled'"
|
||
@click="editingCard.rating = editingCard.rating === s ? 0 : s"
|
||
x-text="s <= (editingCard.rating || 0) ? '\u2605' : '\u2606'"></span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-modal-footer">
|
||
<button class="di-btn" @click="closeCardModal()">취소</button>
|
||
<button class="di-btn primary" @click="saveCard()">
|
||
<i class="ri-check-line"></i> <span x-text="editingCard.id ? '수정' : '추가'"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ===== Projects Modal ===== -->
|
||
<template x-if="showProjectsModal">
|
||
<div class="di-modal-overlay" @click.self="showProjectsModal = false">
|
||
<div class="di-modal" style="width: 500px;">
|
||
<div class="di-modal-header">
|
||
<h3>프로젝트 관리</h3>
|
||
<button class="di-btn ghost" @click="showProjectsModal = false"><i class="ri-close-line" style="font-size: 18px;"></i></button>
|
||
</div>
|
||
<div class="di-modal-body">
|
||
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
|
||
<input type="text" x-model="newProjectTitle" placeholder="새 프로젝트 이름"
|
||
style="flex: 1; padding: 8px 12px; border: 1px solid var(--di-border); border-radius: 8px; font-size: 13px;"
|
||
@keydown.enter="createProject()">
|
||
<button class="di-btn primary" @click="createProject()"><i class="ri-add-line"></i> 생성</button>
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||
<template x-for="proj in projects" :key="proj.id">
|
||
<div class="di-proj-item" :class="proj.id === currentProject.id && 'active'" @click="switchProject(proj.id)">
|
||
<i class="ri-folder-line" style="font-size: 16px; color: var(--di-blue);"></i>
|
||
<span class="proj-title" x-text="proj.title"></span>
|
||
<span class="proj-count" x-text="(proj.cards?.length || 0) + '개'"></span>
|
||
<span class="proj-date" x-text="formatDate(proj.createdAt)"></span>
|
||
<button class="di-btn ghost sm" @click.stop="deleteProject(proj.id)" x-show="projects.length > 1"
|
||
title="삭제"><i class="ri-delete-bin-line" style="color: var(--di-red);"></i></button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ===== Help Modal ===== -->
|
||
<template x-if="showHelpModal">
|
||
<div class="di-modal-overlay" @click.self="showHelpModal = false">
|
||
<div class="di-modal di-help-modal">
|
||
<div class="di-modal-header">
|
||
<h3><i class="ri-question-line" style="color: var(--di-blue);"></i> 디자인 인사이트 도움말</h3>
|
||
<button class="di-btn ghost" @click="showHelpModal = false"><i class="ri-close-line" style="font-size: 18px;"></i></button>
|
||
</div>
|
||
<div class="di-help-tabs">
|
||
<button class="di-help-tab" :class="helpTab === 'overview' && 'active'" @click="helpTab = 'overview'">개요</button>
|
||
<button class="di-help-tab" :class="helpTab === 'toolbar' && 'active'" @click="helpTab = 'toolbar'">툴바</button>
|
||
<button class="di-help-tab" :class="helpTab === 'cards' && 'active'" @click="helpTab = 'cards'">카드 유형</button>
|
||
<button class="di-help-tab" :class="helpTab === 'views' && 'active'" @click="helpTab = 'views'">뷰 모드</button>
|
||
<button class="di-help-tab" :class="helpTab === 'sidebar' && 'active'" @click="helpTab = 'sidebar'">사이드바</button>
|
||
<button class="di-help-tab" :class="helpTab === 'shortcuts' && 'active'" @click="helpTab = 'shortcuts'">단축키</button>
|
||
<button class="di-help-tab" :class="helpTab === 'workflow' && 'active'" @click="helpTab = 'workflow'">워크플로우</button>
|
||
</div>
|
||
<div class="di-help-content">
|
||
|
||
<!-- 개요 -->
|
||
<template x-if="helpTab === 'overview'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-palette-line"></i> 디자인 인사이트란?</h4>
|
||
<p>SAM ERP 화면을 만들 때 참고할 <strong>UI/UX 디자인 레퍼런스를 수집하고, 분석하고, 패턴으로 축적</strong>하는 연구 도구입니다. 외부 서비스(Dribbble, Mobbin 등)나 기존 SAM 화면의 스크린샷을 캡처하여 인사이트를 기록하고, 팀과 공유할 수 있습니다.</p>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-lightbulb-line"></i> 핵심 가치</h4>
|
||
<div class="di-help-grid">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;">📷</div>
|
||
<div class="h-body">
|
||
<div class="h-title">레퍼런스 수집</div>
|
||
<div class="h-desc">좋은 화면을 스크린샷으로 수집하고 왜 좋은지 메모</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fef3c7;">🔍</div>
|
||
<div class="h-body">
|
||
<div class="h-title">화면 분석</div>
|
||
<div class="h-desc">CRAP 디자인 원칙으로 화면의 장단점 분석</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #ecfdf5;">📐</div>
|
||
<div class="h-body">
|
||
<div class="h-title">패턴 라이브러리</div>
|
||
<div class="h-desc">반복 사용할 UI 패턴을 템플릿으로 등록</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fae8ff;">🔄</div>
|
||
<div class="h-body">
|
||
<div class="h-title">Before/After 비교</div>
|
||
<div class="h-desc">개선 전후를 비교하여 디자인 결정 근거 기록</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-rocket-line"></i> 빠른 시작</h4>
|
||
<div class="di-help-step">
|
||
<div class="step-num">1</div>
|
||
<div class="step-body">
|
||
<div class="step-title">스크린샷 캡처</div>
|
||
<div class="step-desc">참고할 화면을 <span class="di-help-kbd">Win + Shift + S</span> 또는 캡처 도구로 스크린샷을 찍으세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">2</div>
|
||
<div class="step-body">
|
||
<div class="step-title">Ctrl+V 붙여넣기</div>
|
||
<div class="step-desc">이 화면에서 <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span>를 누르면 자동으로 새 카드가 생성되고 이미지가 붙여넣어집니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">3</div>
|
||
<div class="step-body">
|
||
<div class="step-title">정보 입력</div>
|
||
<div class="step-desc">제목, 인사이트 메모, 출처, 태그, 카테고리, 평점을 입력하고 저장하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">4</div>
|
||
<div class="step-body">
|
||
<div class="step-title">분류 및 검색</div>
|
||
<div class="step-desc">카테고리 탭, 태그 필터, 검색으로 원하는 인사이트를 빠르게 찾으세요.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 툴바 -->
|
||
<template x-if="helpTab === 'toolbar'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-tools-line"></i> 상단 툴바 기능</h4>
|
||
<p>화면 최상단의 도구 모음입니다. 프로젝트 관리, 저장, 뷰 전환 등 주요 기능에 접근합니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-layout-left-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">사이드바 토글 (좌측 첫 번째 아이콘)</div>
|
||
<div class="h-desc">좌측 사이드바(카테고리, 태그, 검색, 정렬)를 접거나 펼칩니다. 넓은 화면이 필요할 때 접으세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;">📝</div>
|
||
<div class="h-body">
|
||
<div class="h-title">프로젝트 제목 (입력 필드)</div>
|
||
<div class="h-desc">현재 프로젝트의 이름입니다. 클릭하여 직접 수정할 수 있습니다. 예: "SAM ERP v2 디자인 연구"</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;"><i class="ri-save-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">저장 버튼</div>
|
||
<div class="h-desc">현재 프로젝트를 브라우저 localStorage에 저장합니다. <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">S</span>로도 가능합니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;"><i class="ri-download-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">내보내기 버튼</div>
|
||
<div class="h-desc"><strong>JSON 내보내기</strong>: 프로젝트 전체를 JSON 파일로 다운로드 (백업용)<br><strong>JSON 가져오기</strong>: 이전에 내보낸 JSON 파일을 불러와 복원</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-layout-grid-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">뷰 전환 탭 (보드 / 갤러리 / 리스트)</div>
|
||
<div class="h-desc">카드 표시 방식을 변경합니다. 보드(격자), 갤러리(이미지 중심), 리스트(테이블형) 중 선택하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-folder-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">프로젝트 관리 버튼</div>
|
||
<div class="h-desc">여러 연구 프로젝트를 만들고 전환할 수 있습니다. 예: "대시보드 연구", "목록 화면 연구" 등 주제별 분리.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #dbeafe;"><i class="ri-question-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">도움말 버튼 (지금 보고 있는 화면)</div>
|
||
<div class="h-desc">이 도움말 모달을 엽니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-layout-top-line"></i> 카테고리 탭 (툴바 아래)</h4>
|
||
<p>카드의 <strong>유형별 필터</strong>입니다. "전체"를 누르면 모든 카드를, 특정 유형을 누르면 해당 유형만 표시합니다. 각 탭 옆의 숫자는 해당 유형의 카드 수입니다.</p>
|
||
<div class="di-help-grid">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;">📷</div>
|
||
<div class="h-body"><div class="h-title">레퍼런스</div><div class="h-desc">외부/내부 화면 스크린샷 수집</div></div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fef3c7;">🔍</div>
|
||
<div class="h-body"><div class="h-title">분석</div><div class="h-desc">화면 분석 + CRAP 디자인 원칙 체크</div></div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #ecfdf5;">📐</div>
|
||
<div class="h-body"><div class="h-title">패턴</div><div class="h-desc">반복 사용할 UI 패턴 등록</div></div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fae8ff;">🔄</div>
|
||
<div class="h-body"><div class="h-title">Before/After</div><div class="h-desc">개선 전후 비교</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 카드 유형 -->
|
||
<template x-if="helpTab === 'cards'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4 style="color: #2563eb;"><i class="ri-camera-line"></i> 1. 레퍼런스 카드</h4>
|
||
<p>외부 서비스나 내부 화면의 <strong>스크린샷을 수집</strong>하고 왜 좋은지(또는 나쁜지) 메모를 남깁니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-body">
|
||
<div class="h-title">입력 항목</div>
|
||
<div class="h-desc">
|
||
<strong>이미지</strong> — 스크린샷 (클릭/드래그/Ctrl+V)<br>
|
||
<strong>인사이트 메모</strong> — "카드형 레이아웃이 정보 밀도를 유지하면서도 깔끔"<br>
|
||
<strong>출처</strong> — 출처 URL이나 앱 이름 (예: notion.so, Figma)<br>
|
||
<strong>태그</strong> — 자유 태그 (콤마 구분)<br>
|
||
<strong>카테고리</strong> — 대시보드, 목록, 상세/폼 등 화면 유형<br>
|
||
<strong>평점</strong> — 1~5점 (참고 가치 평가)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4 style="color: #d97706;"><i class="ri-search-eye-line"></i> 2. 분석 카드</h4>
|
||
<p>화면을 <strong>디자인 원칙(CRAP)으로 분석</strong>하고 개선 제안을 기록합니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-body">
|
||
<div class="h-title">CRAP 디자인 원칙</div>
|
||
<div class="h-desc">
|
||
각 원칙을 클릭하면 상태가 순환합니다: <strong>— → ✅ 통과 → ⚠️ 주의 → ❌ 미달</strong><br><br>
|
||
<strong>C</strong>ontrast (대비) — 중요 요소가 시각적으로 구분되는가?<br>
|
||
<strong>R</strong>epetition (반복) — 일관된 스타일이 반복 적용되는가?<br>
|
||
<strong>A</strong>lignment (정렬) — 요소들이 논리적으로 정렬되어 있는가?<br>
|
||
<strong>P</strong>roximity (근접성) — 관련 요소가 가까이 그룹핑되어 있는가?<br>
|
||
+ 여백, 계층, 일관성, 접근성 체크
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-body">
|
||
<div class="h-title">추가 입력</div>
|
||
<div class="h-desc">
|
||
<strong>개선 제안</strong> — "검색 영역을 접을 수 있게 하고 버튼 그룹을 우측 정렬"<br>
|
||
<strong>심각도</strong> — 정보(ℹ️) / 경고(⚠️) / 심각(🔴)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4 style="color: #059669;"><i class="ri-layout-masonry-line"></i> 3. 패턴 카드</h4>
|
||
<p>반복 사용할 <strong>UI 패턴을 템플릿으로 등록</strong>하여 새 화면 설계 시 재사용합니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-body">
|
||
<div class="h-title">입력 항목</div>
|
||
<div class="h-desc">
|
||
<strong>사용처</strong> — 이 패턴이 사용된 화면 (예: "수주 목록, 거래처 목록")<br>
|
||
<strong>구성 요소</strong> — 패턴을 이루는 요소 체크리스트 (검색바, 필터 칩, 테이블 등)<br>
|
||
<strong>사용 가이드라인</strong> — 이 패턴 사용 시 주의사항
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4 style="color: #a855f7;"><i class="ri-arrow-left-right-line"></i> 4. Before/After 카드</h4>
|
||
<p>디자인 <strong>개선 전후를 나란히 비교</strong>하여 변경 근거와 효과를 기록합니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-body">
|
||
<div class="h-title">입력 항목</div>
|
||
<div class="h-desc">
|
||
<strong>Before 이미지</strong> — 개선 전 스크린샷<br>
|
||
<strong>After 이미지</strong> — 개선 후 스크린샷<br>
|
||
<strong>변경 포인트</strong> — 무엇을 어떻게 바꿨는지 (줄 단위 입력)<br>
|
||
<strong>개선 효과</strong> — "스크롤 40% 감소, 작업 완료 시간 단축"
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 뷰 모드 -->
|
||
<template x-if="helpTab === 'views'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-layout-grid-line"></i> 뷰 모드 (3종)</h4>
|
||
<p>우측 상단의 아이콘 탭으로 카드 표시 방식을 전환합니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;"><i class="ri-layout-grid-line" style="font-size: 20px; color: var(--di-blue);"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">보드 뷰 (기본)</div>
|
||
<div class="h-desc">카드를 격자(그리드)로 배열합니다. 이미지 썸네일 + 제목 + 태그 + 평점이 한눈에 보여 <strong>전체 현황 파악</strong>에 적합합니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #ecfdf5;"><i class="ri-image-line" style="font-size: 20px; color: #059669;"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">갤러리 뷰</div>
|
||
<div class="h-desc">이미지를 크게 표시하는 뷰입니다. <strong>시각적 비교</strong>나 <strong>레퍼런스 브라우징</strong>에 최적화되어 있습니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fef3c7;"><i class="ri-list-unordered" style="font-size: 20px; color: #d97706;"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">리스트 뷰</div>
|
||
<div class="h-desc">테이블 형태로 한 줄씩 표시합니다. <strong>대량 데이터 관리</strong>, 빠른 스캔, 태그 확인에 효율적입니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 사이드바 -->
|
||
<template x-if="helpTab === 'sidebar'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-layout-left-line"></i> 좌측 사이드바</h4>
|
||
<p>카드를 <strong>필터링하고 검색</strong>하는 패널입니다. 사이드바 토글 버튼으로 접거나 펼 수 있습니다.</p>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-search-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">검색</div>
|
||
<div class="h-desc">제목, 메모, 태그, 출처에서 키워드를 검색합니다. <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">F</span>로 포커스 가능.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;">📊</div>
|
||
<div class="h-body">
|
||
<div class="h-title">카테고리 (화면 유형)</div>
|
||
<div class="h-desc">대시보드, 목록, 상세/폼, 모달, 네비게이션, 로그인, 보고서, 기타 — 화면 유형별로 카드를 필터링합니다. 숫자는 해당 카테고리의 카드 수.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;">🏷️</div>
|
||
<div class="h-body">
|
||
<div class="h-title">태그 필터</div>
|
||
<div class="h-desc">카드에 추가한 태그 목록이 표시됩니다. <strong>태그를 클릭하면 해당 태그가 포함된 카드만</strong> 표시합니다. 여러 태그를 선택하면 OR 조건으로 필터링됩니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-sort-asc"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">정렬</div>
|
||
<div class="h-desc">최신순 / 오래된순 / 평점순 / 이름순으로 카드를 정렬합니다. 핀(📌) 고정된 카드는 항상 최상단에 표시됩니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 단축키 -->
|
||
<template x-if="helpTab === 'shortcuts'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-keyboard-line"></i> 키보드 단축키</h4>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
|
||
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span></div>
|
||
<div class="h-desc"><strong>클립보드 이미지 붙여넣기</strong> — 스크린샷 캡처 후 이 화면에서 Ctrl+V 하면 자동으로 새 레퍼런스 카드가 생성됩니다. 가장 핵심적인 기능!</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
|
||
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">S</span></div>
|
||
<div class="h-desc"><strong>프로젝트 저장</strong> — 현재 상태를 localStorage에 저장합니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
|
||
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">N</span></div>
|
||
<div class="h-desc"><strong>새 카드 추가</strong> — 새 레퍼런스 카드 생성 모달을 엽니다.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
|
||
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">F</span></div>
|
||
<div class="h-desc"><strong>검색 포커스</strong> — 사이드바 검색 입력란으로 포커스를 이동합니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-mouse-line"></i> 이미지 입력 방법 (3가지)</h4>
|
||
<div class="di-help-grid" style="grid-template-columns: 1fr;">
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #eff6ff;"><i class="ri-clipboard-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">1. 클립보드 붙여넣기 (가장 빠름)</div>
|
||
<div class="h-desc">메인 화면에서 <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span> → 새 카드 자동 생성</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #ecfdf5;"><i class="ri-upload-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">2. 파일 업로드</div>
|
||
<div class="h-desc">카드 편집 모달에서 이미지 영역을 클릭 → 파일 선택</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-item">
|
||
<div class="h-icon" style="background: #fef3c7;"><i class="ri-drag-drop-line"></i></div>
|
||
<div class="h-body">
|
||
<div class="h-title">3. 드래그 앤 드롭</div>
|
||
<div class="h-desc">카드 편집 모달에서 이미지 영역에 파일을 끌어다 놓기</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 워크플로우 -->
|
||
<template x-if="helpTab === 'workflow'">
|
||
<div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-flow-chart"></i> 추천 워크플로우</h4>
|
||
<p>디자인 인사이트를 효과적으로 활용하는 단계별 흐름입니다.</p>
|
||
<div class="di-help-step">
|
||
<div class="step-num">1</div>
|
||
<div class="step-body">
|
||
<div class="step-title">프로젝트 생성</div>
|
||
<div class="step-desc">연구 주제별로 프로젝트를 생성합니다. 예: "SAM 대시보드 리뉴얼", "목록 화면 개선"</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">2</div>
|
||
<div class="step-body">
|
||
<div class="step-title">레퍼런스 수집</div>
|
||
<div class="step-desc">Dribbble, Mobbin, 경쟁 서비스 등에서 좋은 화면을 스크린샷으로 수집합니다. Ctrl+V로 빠르게 추가하고, 태그와 카테고리를 분류하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">3</div>
|
||
<div class="step-body">
|
||
<div class="step-title">화면 분석</div>
|
||
<div class="step-desc">SAM 기존 화면을 분석 카드로 만들어 CRAP 원칙을 체크합니다. 어떤 부분이 부족한지 개선 제안을 기록하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">4</div>
|
||
<div class="step-body">
|
||
<div class="step-title">패턴 추출</div>
|
||
<div class="step-desc">레퍼런스에서 반복되는 좋은 패턴을 발견하면 패턴 카드로 등록합니다. 구성 요소와 사용 가이드라인을 정리하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">5</div>
|
||
<div class="step-body">
|
||
<div class="step-title">Before/After 기록</div>
|
||
<div class="step-desc">화면을 개선한 후 전후 비교 카드를 만들어 변경 포인트와 효과를 기록합니다. 팀 회의에서 근거 자료로 활용하세요.</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-step">
|
||
<div class="step-num">6</div>
|
||
<div class="step-body">
|
||
<div class="step-title">기획디자인 연계</div>
|
||
<div class="step-desc">축적된 패턴과 인사이트를 참고하여 기획디자인 메뉴에서 스토리보드를 작성합니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="di-help-section">
|
||
<h4><i class="ri-information-line"></i> 데이터 저장 안내</h4>
|
||
<p>
|
||
모든 데이터는 <strong>브라우저 localStorage</strong>에 저장됩니다.<br>
|
||
브라우저 데이터를 삭제하면 인사이트도 함께 삭제되므로, 중요한 프로젝트는 <strong>JSON 내보내기</strong>로 백업해두세요.<br>
|
||
다른 PC에서 작업하려면 JSON 내보내기 → 가져오기로 이동할 수 있습니다.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Toast -->
|
||
<template x-if="toastMsg">
|
||
<div class="di-toast" x-text="toastMsg" x-init="setTimeout(() => toastMsg = '', 2500)"></div>
|
||
</template>
|
||
</div>
|
||
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
<script>
|
||
function designInsight() {
|
||
return {
|
||
// State
|
||
projects: [],
|
||
currentProject: { id: '', title: '', cards: [], createdAt: '', updatedAt: '' },
|
||
viewMode: 'board', // board | gallery | list
|
||
categoryFilter: 'all', // card type filter (tabs)
|
||
screenFilter: 'all', // screen category filter (sidebar)
|
||
searchQuery: '',
|
||
selectedTags: [],
|
||
sortBy: 'newest',
|
||
sidebarOpen: true,
|
||
showCardModal: false,
|
||
showProjectsModal: false,
|
||
showExportMenu: false,
|
||
showHelpModal: false,
|
||
helpTab: 'overview',
|
||
editingCard: {},
|
||
newProjectTitle: '',
|
||
toastMsg: '',
|
||
lastSaved: '',
|
||
undoStack: [],
|
||
redoStack: [],
|
||
|
||
// Constants
|
||
cardTypes: [
|
||
{ code: 'reference', label: '레퍼런스', icon: '📷' },
|
||
{ code: 'analysis', label: '분석', icon: '🔍' },
|
||
{ code: 'pattern', label: '패턴', icon: '📐' },
|
||
{ code: 'comparison', label: 'Before/After', icon: '🔄' },
|
||
],
|
||
categories: [
|
||
{ code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' },
|
||
{ code: 'list', label: '목록', icon: '📋', color: '#3b82f6' },
|
||
{ code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' },
|
||
{ code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' },
|
||
{ code: 'navigation', label: '네비게이션', icon: '🧭', color: '#8b5cf6' },
|
||
{ code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' },
|
||
{ code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' },
|
||
{ code: 'etc', label: '기타', icon: '📎', color: '#64748b' },
|
||
],
|
||
designPrinciples: [
|
||
{ key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' },
|
||
{ key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' },
|
||
{ key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' },
|
||
{ key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' },
|
||
{ key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' },
|
||
{ key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' },
|
||
{ key: 'consistency', label: '일관성 (Consistency)', icon: '🔗', desc: '다른 화면과의 일관성' },
|
||
{ key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' },
|
||
],
|
||
|
||
// Init
|
||
init() {
|
||
this.loadProjects();
|
||
if (this.projects.length === 0) {
|
||
this.createDefaultProject();
|
||
}
|
||
this.loadCurrentProject();
|
||
},
|
||
|
||
// ===== Projects =====
|
||
loadProjects() {
|
||
try {
|
||
const data = localStorage.getItem('di_projects');
|
||
this.projects = data ? JSON.parse(data) : [];
|
||
} catch { this.projects = []; }
|
||
},
|
||
|
||
saveProjects() {
|
||
localStorage.setItem('di_projects', JSON.stringify(this.projects));
|
||
},
|
||
|
||
createDefaultProject() {
|
||
const proj = {
|
||
id: 'diproj_' + Date.now(),
|
||
title: 'SAM ERP 디자인 연구',
|
||
description: '',
|
||
cards: [],
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
this.projects.push(proj);
|
||
localStorage.setItem('di_current', proj.id);
|
||
this.saveProjects();
|
||
},
|
||
|
||
loadCurrentProject() {
|
||
const currentId = localStorage.getItem('di_current');
|
||
const proj = this.projects.find(p => p.id === currentId) || this.projects[0];
|
||
if (proj) {
|
||
this.currentProject = proj;
|
||
localStorage.setItem('di_current', proj.id);
|
||
}
|
||
},
|
||
|
||
createProject() {
|
||
if (!this.newProjectTitle.trim()) return;
|
||
const proj = {
|
||
id: 'diproj_' + Date.now(),
|
||
title: this.newProjectTitle.trim(),
|
||
description: '',
|
||
cards: [],
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
this.projects.push(proj);
|
||
this.saveProjects();
|
||
this.switchProject(proj.id);
|
||
this.newProjectTitle = '';
|
||
this.toast('프로젝트 생성됨');
|
||
},
|
||
|
||
switchProject(id) {
|
||
this.saveProject();
|
||
const proj = this.projects.find(p => p.id === id);
|
||
if (proj) {
|
||
this.currentProject = proj;
|
||
localStorage.setItem('di_current', id);
|
||
this.clearFilters();
|
||
this.showProjectsModal = false;
|
||
}
|
||
},
|
||
|
||
deleteProject(id) {
|
||
if (this.projects.length <= 1) return;
|
||
if (!confirm('프로젝트를 삭제하시겠습니까?')) return;
|
||
this.projects = this.projects.filter(p => p.id !== id);
|
||
if (this.currentProject.id === id) {
|
||
this.currentProject = this.projects[0];
|
||
localStorage.setItem('di_current', this.currentProject.id);
|
||
}
|
||
this.saveProjects();
|
||
this.toast('프로젝트 삭제됨');
|
||
},
|
||
|
||
saveProject() {
|
||
this.currentProject.updatedAt = new Date().toISOString();
|
||
const idx = this.projects.findIndex(p => p.id === this.currentProject.id);
|
||
if (idx >= 0) this.projects[idx] = { ...this.currentProject };
|
||
this.saveProjects();
|
||
this.lastSaved = this.formatTime(new Date());
|
||
},
|
||
|
||
// ===== Cards =====
|
||
openNewCardModal(type) {
|
||
this.editingCard = {
|
||
id: '',
|
||
type: type || 'reference',
|
||
title: '',
|
||
image: '',
|
||
memo: '',
|
||
source: '',
|
||
tags: [],
|
||
tagsText: '',
|
||
category: 'etc',
|
||
rating: 0,
|
||
pinned: false,
|
||
archived: false,
|
||
// Analysis
|
||
principles: {},
|
||
suggestion: '',
|
||
severity: 'info',
|
||
// Pattern
|
||
usedIn: [],
|
||
usedInText: '',
|
||
components: [{ name: '', required: true }],
|
||
guidelines: '',
|
||
frequency: 0,
|
||
// Comparison
|
||
beforeImage: '',
|
||
afterImage: '',
|
||
changes: [],
|
||
changesText: '',
|
||
effect: '',
|
||
};
|
||
this.showCardModal = true;
|
||
},
|
||
|
||
openEditCardModal(card) {
|
||
this.editingCard = {
|
||
...card,
|
||
tagsText: (card.tags || []).join(', '),
|
||
usedInText: (card.usedIn || []).join(', '),
|
||
changesText: (card.changes || []).join('\n'),
|
||
components: card.components ? [...card.components.map(c => ({...c}))] : [{ name: '', required: true }],
|
||
principles: card.principles ? { ...card.principles } : {},
|
||
};
|
||
this.showCardModal = true;
|
||
},
|
||
|
||
closeCardModal() {
|
||
this.showCardModal = false;
|
||
this.editingCard = {};
|
||
},
|
||
|
||
saveCard() {
|
||
const card = this.editingCard;
|
||
|
||
// Parse text fields
|
||
card.tags = card.tagsText ? card.tagsText.split(',').map(t => t.trim()).filter(Boolean) : [];
|
||
card.usedIn = card.usedInText ? card.usedInText.split(',').map(t => t.trim()).filter(Boolean) : [];
|
||
card.changes = card.changesText ? card.changesText.split('\n').map(t => t.trim()).filter(Boolean) : [];
|
||
|
||
// Clean temp fields
|
||
const { tagsText, usedInText, changesText, ...cleanCard } = card;
|
||
|
||
// Remove empty components
|
||
if (cleanCard.components) {
|
||
cleanCard.components = cleanCard.components.filter(c => c.name.trim());
|
||
}
|
||
|
||
if (!cleanCard.id) {
|
||
// New card
|
||
cleanCard.id = 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
|
||
cleanCard.createdAt = new Date().toISOString();
|
||
cleanCard.updatedAt = new Date().toISOString();
|
||
if (!this.currentProject.cards) this.currentProject.cards = [];
|
||
this.currentProject.cards.unshift(cleanCard);
|
||
this.toast('카드 추가됨');
|
||
} else {
|
||
// Update card
|
||
cleanCard.updatedAt = new Date().toISOString();
|
||
const idx = this.currentProject.cards.findIndex(c => c.id === cleanCard.id);
|
||
if (idx >= 0) this.currentProject.cards[idx] = cleanCard;
|
||
this.toast('카드 수정됨');
|
||
}
|
||
|
||
this.saveProject();
|
||
this.closeCardModal();
|
||
},
|
||
|
||
deleteCard(id) {
|
||
if (!confirm('이 카드를 삭제하시겠습니까?')) return;
|
||
this.currentProject.cards = this.currentProject.cards.filter(c => c.id !== id);
|
||
this.saveProject();
|
||
this.closeCardModal();
|
||
this.toast('카드 삭제됨');
|
||
},
|
||
|
||
togglePin(card) {
|
||
card.pinned = !card.pinned;
|
||
this.saveProject();
|
||
},
|
||
|
||
// ===== Images =====
|
||
handlePaste(e) {
|
||
if (this.showCardModal || this.showProjectsModal) return;
|
||
const items = e.clipboardData?.items;
|
||
if (!items) return;
|
||
for (const item of items) {
|
||
if (item.type.startsWith('image/')) {
|
||
e.preventDefault();
|
||
const file = item.getAsFile();
|
||
this.readImageFile(file, (dataUrl) => {
|
||
this.openNewCardModal('reference');
|
||
this.$nextTick(() => { this.editingCard.image = dataUrl; });
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
handleDrop(e) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
const file = e.dataTransfer?.files?.[0];
|
||
if (file && file.type.startsWith('image/')) {
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
|
||
}
|
||
},
|
||
|
||
handleFileSelect(e) {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
|
||
}
|
||
e.target.value = '';
|
||
},
|
||
|
||
handleCompDrop(e, side) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
const file = e.dataTransfer?.files?.[0];
|
||
if (file && file.type.startsWith('image/')) {
|
||
const key = side === 'before' ? 'beforeImage' : 'afterImage';
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
|
||
}
|
||
},
|
||
|
||
handleCompFileSelect(e, side) {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
const key = side === 'before' ? 'beforeImage' : 'afterImage';
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
|
||
}
|
||
e.target.value = '';
|
||
},
|
||
|
||
readImageFile(file, callback) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => callback(e.target.result);
|
||
reader.readAsDataURL(file);
|
||
},
|
||
|
||
// ===== CRAP Principles =====
|
||
cyclePrinciple(key) {
|
||
if (!this.editingCard.principles) this.editingCard.principles = {};
|
||
const current = this.editingCard.principles[key] || '';
|
||
const cycle = { '': 'pass', 'pass': 'warn', 'warn': 'fail', 'fail': '' };
|
||
this.editingCard.principles[key] = cycle[current] || '';
|
||
},
|
||
|
||
getPrincipleStatus(key) {
|
||
return (this.editingCard.principles || {})[key] || '';
|
||
},
|
||
|
||
getPrincipleIcon(key) {
|
||
const status = this.getPrincipleStatus(key);
|
||
return { pass: '✅', warn: '⚠️', fail: '❌', '': '—' }[status] || '—';
|
||
},
|
||
|
||
// ===== Filters =====
|
||
get filteredCards() {
|
||
let cards = this.currentProject.cards || [];
|
||
|
||
// Card type filter (tabs)
|
||
if (this.categoryFilter !== 'all') {
|
||
cards = cards.filter(c => c.type === this.categoryFilter);
|
||
}
|
||
|
||
// Screen category filter (sidebar)
|
||
if (this.screenFilter !== 'all') {
|
||
cards = cards.filter(c => c.category === this.screenFilter);
|
||
}
|
||
|
||
// Tag filter
|
||
if (this.selectedTags.length > 0) {
|
||
cards = cards.filter(c => {
|
||
const cardTags = c.tags || [];
|
||
return this.selectedTags.some(t => cardTags.includes(t));
|
||
});
|
||
}
|
||
|
||
// Search
|
||
if (this.searchQuery.trim()) {
|
||
const q = this.searchQuery.toLowerCase();
|
||
cards = cards.filter(c =>
|
||
(c.title || '').toLowerCase().includes(q) ||
|
||
(c.memo || '').toLowerCase().includes(q) ||
|
||
(c.suggestion || '').toLowerCase().includes(q) ||
|
||
(c.effect || '').toLowerCase().includes(q) ||
|
||
(c.source || '').toLowerCase().includes(q) ||
|
||
(c.tags || []).some(t => t.toLowerCase().includes(q))
|
||
);
|
||
}
|
||
|
||
// Sort — pinned first
|
||
cards = [...cards].sort((a, b) => {
|
||
if (a.pinned && !b.pinned) return -1;
|
||
if (!a.pinned && b.pinned) return 1;
|
||
return 0;
|
||
});
|
||
|
||
// Sort by
|
||
cards.sort((a, b) => {
|
||
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
||
switch (this.sortBy) {
|
||
case 'newest': return new Date(b.createdAt) - new Date(a.createdAt);
|
||
case 'oldest': return new Date(a.createdAt) - new Date(b.createdAt);
|
||
case 'rating': return (b.rating || 0) - (a.rating || 0);
|
||
case 'title': return (a.title || '').localeCompare(b.title || '');
|
||
default: return 0;
|
||
}
|
||
});
|
||
|
||
return cards;
|
||
},
|
||
|
||
get allTags() {
|
||
const tags = new Set();
|
||
(this.currentProject.cards || []).forEach(c => {
|
||
(c.tags || []).forEach(t => tags.add(t));
|
||
});
|
||
return [...tags].sort();
|
||
},
|
||
|
||
toggleTag(tag) {
|
||
const idx = this.selectedTags.indexOf(tag);
|
||
if (idx >= 0) this.selectedTags.splice(idx, 1);
|
||
else this.selectedTags.push(tag);
|
||
},
|
||
|
||
clearFilters() {
|
||
this.categoryFilter = 'all';
|
||
this.screenFilter = 'all';
|
||
this.searchQuery = '';
|
||
this.selectedTags = [];
|
||
this.sortBy = 'newest';
|
||
},
|
||
|
||
getCardCountByType(type) {
|
||
return (this.currentProject.cards || []).filter(c => c.type === type).length;
|
||
},
|
||
|
||
getCardCountByCat(cat) {
|
||
return (this.currentProject.cards || []).filter(c => c.category === cat).length;
|
||
},
|
||
|
||
// ===== Export / Import =====
|
||
exportJSON() {
|
||
const data = JSON.stringify(this.currentProject, null, 2);
|
||
const blob = new Blob([data], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = (this.currentProject.title || 'design-insight') + '.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
this.toast('JSON 내보내기 완료');
|
||
},
|
||
|
||
importJSON() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.json';
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
try {
|
||
const data = JSON.parse(ev.target.result);
|
||
if (data.id && data.cards) {
|
||
// Check if project already exists
|
||
const existIdx = this.projects.findIndex(p => p.id === data.id);
|
||
if (existIdx >= 0) {
|
||
if (confirm('동일 ID 프로젝트가 있습니다. 덮어쓰시겠습니까?')) {
|
||
this.projects[existIdx] = data;
|
||
} else return;
|
||
} else {
|
||
this.projects.push(data);
|
||
}
|
||
this.saveProjects();
|
||
this.switchProject(data.id);
|
||
this.toast('프로젝트 가져오기 완료');
|
||
}
|
||
} catch { this.toast('JSON 파일 오류'); }
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
input.click();
|
||
},
|
||
|
||
// ===== Keyboard =====
|
||
handleKeydown(e) {
|
||
if (this.showCardModal || this.showProjectsModal) return;
|
||
|
||
// Ctrl+S — Save
|
||
if (e.ctrlKey && e.key === 's') {
|
||
e.preventDefault();
|
||
this.saveProject();
|
||
this.toast('저장됨');
|
||
}
|
||
// Ctrl+N — New card
|
||
if (e.ctrlKey && e.key === 'n') {
|
||
e.preventDefault();
|
||
this.openNewCardModal('reference');
|
||
}
|
||
// Ctrl+F — Focus search
|
||
if (e.ctrlKey && e.key === 'f') {
|
||
e.preventDefault();
|
||
document.querySelector('.di-search-input')?.focus();
|
||
}
|
||
},
|
||
|
||
// ===== Helpers =====
|
||
getTypeLabel(type) {
|
||
const t = this.cardTypes.find(ct => ct.code === type);
|
||
return t ? t.icon + ' ' + t.label : type;
|
||
},
|
||
|
||
getTypeIcon(type) {
|
||
const map = {
|
||
reference: 'ri-camera-line',
|
||
analysis: 'ri-search-eye-line',
|
||
pattern: 'ri-layout-masonry-line',
|
||
comparison: 'ri-arrow-left-right-line',
|
||
};
|
||
return map[type] || 'ri-file-line';
|
||
},
|
||
|
||
getRatingStars(rating) {
|
||
return '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
|
||
},
|
||
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const d = new Date(dateStr);
|
||
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||
},
|
||
|
||
formatTime(d) {
|
||
return String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
|
||
},
|
||
|
||
toast(msg) {
|
||
this.toastMsg = msg;
|
||
setTimeout(() => { this.toastMsg = ''; }, 2500);
|
||
},
|
||
};
|
||
}
|
||
</script>
|
||
@endpush
|