Files
sam-manage/resources/views/rd/design-insight/index.blade.php
김보곤 2b64f41b39 feat: [rd] 디자인 인사이트 카드 미리보기 모달 + 와이어프레임 20종
- 카드 클릭 시 미리보기 모달 (좌: 와이어프레임, 우: 정보 패널)
- 패턴 카드 20종 CSS 와이어프레임 자동 생성
- KPI 대시보드, 데이터 테이블, 칸반, Command Palette,
  사이드바, 모달 폼, 설정, 타임라인, 트리 분할뷰,
  온보딩 스테퍼, 토스트, Empty State, 검색 자동완성,
  탭 레이아웃, 카드 그리드, 가격표, 캘린더, 채팅,
  파일 업로드, 브레드크럼
- 미리보기에서 편집 모달로 전환 가능
2026-03-08 10:27:26 +09:00

3327 lines
192 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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; }
/* Preview Modal */
.di-preview { width: 820px; max-width: 95vw; }
.di-preview .di-modal-body { padding: 0; }
.di-preview-layout { display: flex; gap: 0; }
.di-preview-left { flex: 1; min-width: 0; border-right: 1px solid var(--di-border); }
.di-preview-right { width: 280px; flex-shrink: 0; padding: 20px; overflow-y: auto; max-height: 70vh; }
.di-preview-wireframe {
background: #f8fafc;
min-height: 320px;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
overflow: hidden;
}
.di-preview-wireframe img {
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 6px;
}
.di-preview-info-item { margin-bottom: 16px; }
.di-preview-info-item label {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .5px;
color: var(--di-text-secondary);
margin-bottom: 4px;
display: block;
}
.di-preview-info-item .val { font-size: 13px; color: var(--di-text); line-height: 1.6; }
.di-preview-comp {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 5px 0;
font-size: 12.5px;
color: var(--di-text);
border-bottom: 1px solid #f1f5f9;
}
.di-preview-comp:last-child { border-bottom: none; }
.di-preview-comp .chk { color: #10b981; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
.di-preview-comp .opt { color: #94a3b8; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
/* Wireframe Styles */
.wf-wrap {
width: 100%;
max-width: 480px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e2e8f0;
background: #fff;
font-family: 'Pretendard', sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.wf-bar { height: 10px; border-radius: 3px; background: #e2e8f0; }
.wf-bar.sm { width: 40px; }
.wf-bar.md { width: 80px; }
.wf-bar.lg { width: 120px; }
.wf-bar.xl { width: 180px; }
.wf-bar.blue { background: #bfdbfe; }
.wf-bar.green { background: #bbf7d0; }
.wf-bar.amber { background: #fde68a; }
.wf-bar.red { background: #fecaca; }
.wf-bar.purple { background: #ddd6fe; }
.wf-bar.dark { background: #cbd5e1; }
.wf-circle { border-radius: 50%; background: #e2e8f0; flex-shrink: 0; }
.wf-box { border-radius: 6px; background: #f1f5f9; border: 1px solid #e2e8f0; }
.wf-text { font-size: 8px; color: #94a3b8; font-weight: 600; letter-spacing: .3px; white-space: nowrap; }
/* 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 style="border-top: 1px solid var(--di-border); margin: 4px 0;"></div>
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="loadPresetTemplates(); showExportMenu=false">
<i class="ri-magic-line"></i> 인기 UI 패턴 20
</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>
<div style="display: flex; gap: 8px;">
<button class="di-btn primary" @click="openNewCardModal('reference')">
<i class="ri-add-line"></i> 번째 카드 추가
</button>
<button class="di-btn" @click="loadPresetTemplates()" style="border-color: var(--di-indigo); color: var(--di-indigo);">
<i class="ri-magic-line"></i> 인기 UI 패턴 20 불러오기
</button>
</div>
</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="openPreviewModal(card)">
<!-- Pin -->
<div class="card-pin" :class="card.pinned && 'pinned'"
@click.stop="togglePin(card)" x-text="card.pinned ? '&#x1F4CC;' : '&#x1F4CC;'"></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="openPreviewModal(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="openPreviewModal(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. 탭 구조 → 섹션 접기/펼치기 변경&#10;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>
<!-- ===== Preview Modal ===== -->
<template x-if="showPreviewModal && previewCard">
<div class="di-modal-overlay" @click.self="showPreviewModal = false">
<div class="di-modal di-preview">
<div class="di-modal-header">
<div style="display: flex; align-items: center; gap: 8px;">
<span class="card-type" :class="previewCard.type === 'reference' ? 'ref' : previewCard.type"
x-text="getTypeLabel(previewCard.type)" style="font-size: 11px;"></span>
<h3 x-text="previewCard.title || '(제목 없음)'" style="font-size: 15px;"></h3>
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<button class="di-btn sm" @click="showPreviewModal = false; openEditCardModal(previewCard)">
<i class="ri-edit-line"></i> 편집
</button>
<button class="di-btn ghost" @click="showPreviewModal = false">
<i class="ri-close-line" style="font-size: 18px;"></i>
</button>
</div>
</div>
<div class="di-preview-layout">
<!-- Left: Wireframe / Image -->
<div class="di-preview-left">
<!-- User Image -->
<template x-if="previewCard.image">
<div class="di-preview-wireframe">
<img :src="previewCard.image">
</div>
</template>
<!-- Before/After Images -->
<template x-if="previewCard.type === 'comparison' && !previewCard.image">
<div style="display: grid; grid-template-columns: 1fr 1fr; min-height: 320px;">
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-right: 1px solid var(--di-border); background: #fef2f2;">
<span style="font-size: 10px; font-weight: 700; color: #dc2626; margin-bottom: 8px;">BEFORE</span>
<template x-if="previewCard.beforeImage"><img :src="previewCard.beforeImage" style="max-width: 100%; max-height: 260px; border-radius: 6px;"></template>
<template x-if="!previewCard.beforeImage"><span style="color: #94a3b8;">이미지 없음</span></template>
</div>
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; background: #ecfdf5;">
<span style="font-size: 10px; font-weight: 700; color: #059669; margin-bottom: 8px;">AFTER</span>
<template x-if="previewCard.afterImage"><img :src="previewCard.afterImage" style="max-width: 100%; max-height: 260px; border-radius: 6px;"></template>
<template x-if="!previewCard.afterImage"><span style="color: #94a3b8;">이미지 없음</span></template>
</div>
</div>
</template>
<!-- Auto Wireframe (no user image) -->
<template x-if="!previewCard.image && previewCard.type !== 'comparison'">
<div class="di-preview-wireframe" x-html="getWireframe(previewCard)"></div>
</template>
<!-- Guidelines (below wireframe) -->
<template x-if="previewCard.guidelines || previewCard.memo || previewCard.suggestion || previewCard.effect">
<div style="padding: 16px 20px; border-top: 1px solid var(--di-border); background: #fff;">
<template x-if="previewCard.memo">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">인사이트 메모</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7;" x-text="previewCard.memo"></div>
</div>
</template>
<template x-if="previewCard.guidelines">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">사용 가이드라인</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #f0fdf4; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #10b981;" x-text="previewCard.guidelines"></div>
</div>
</template>
<template x-if="previewCard.suggestion">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">개선 제안</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #fffbeb; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #f59e0b;" x-text="previewCard.suggestion"></div>
</div>
</template>
<template x-if="previewCard.effect">
<div>
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">개선 효과</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #eff6ff; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #3b82f6;" x-text="previewCard.effect"></div>
</div>
</template>
</div>
</template>
</div>
<!-- Right: Info Panel -->
<div class="di-preview-right">
<!-- Rating -->
<div class="di-preview-info-item">
<label>평점</label>
<div class="card-rating" style="font-size: 18px; letter-spacing: 2px;" x-text="getRatingStars(previewCard.rating || 0)"></div>
</div>
<!-- Category -->
<div class="di-preview-info-item">
<label>카테고리</label>
<div class="val" x-text="getCategoryLabel(previewCard.category)"></div>
</div>
<!-- Tags -->
<template x-if="(previewCard.tags || []).length > 0">
<div class="di-preview-info-item">
<label>태그</label>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
<template x-for="tag in previewCard.tags" :key="tag">
<span class="card-tag" x-text="tag" style="font-size: 11.5px; padding: 3px 8px;"></span>
</template>
</div>
</div>
</template>
<!-- Source -->
<template x-if="previewCard.source">
<div class="di-preview-info-item">
<label>출처</label>
<div class="val" x-text="previewCard.source"></div>
</div>
</template>
<!-- Used In (Pattern) -->
<template x-if="(previewCard.usedIn || []).length > 0">
<div class="di-preview-info-item">
<label>사용처</label>
<div style="display: flex; flex-direction: column; gap: 2px;">
<template x-for="u in previewCard.usedIn" :key="u">
<div style="font-size: 12.5px; color: var(--di-text); padding: 2px 0;"> <span x-text="u"></span></div>
</template>
</div>
</div>
</template>
<!-- Components (Pattern) -->
<template x-if="(previewCard.components || []).length > 0 && previewCard.components[0]?.name">
<div class="di-preview-info-item">
<label>구성 요소</label>
<div>
<template x-for="comp in previewCard.components" :key="comp.name">
<div class="di-preview-comp">
<span :class="comp.required ? 'chk' : 'opt'" x-text="comp.required ? '✓' : '○'"></span>
<span x-text="comp.name"></span>
</div>
</template>
</div>
</div>
</template>
<!-- CRAP Principles (Analysis) -->
<template x-if="previewCard.type === 'analysis' && previewCard.principles">
<div class="di-preview-info-item">
<label>디자인 원칙</label>
<div style="display: flex; flex-direction: column; gap: 3px;">
<template x-for="p in designPrinciples" :key="p.key">
<template x-if="previewCard.principles[p.key]">
<div style="font-size: 12px; display: flex; gap: 6px; align-items: center;">
<span x-text="({pass:'✅',warn:'⚠️',fail:'❌'})[previewCard.principles[p.key]] || '—'"></span>
<span x-text="p.label"></span>
</div>
</template>
</template>
</div>
</div>
</template>
<!-- Changes (Comparison) -->
<template x-if="(previewCard.changes || []).length > 0">
<div class="di-preview-info-item">
<label>변경 포인트</label>
<div style="display: flex; flex-direction: column; gap: 3px;">
<template x-for="(c, i) in previewCard.changes" :key="i">
<div style="font-size: 12.5px; color: var(--di-text); line-height: 1.5;" x-text="c"></div>
</template>
</div>
</div>
</template>
<!-- Date -->
<div class="di-preview-info-item" style="margin-top: auto; padding-top: 12px; border-top: 1px solid #f1f5f9;">
<label>생성일</label>
<div class="val" x-text="formatDate(previewCard.createdAt)"></div>
</div>
</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 화면을 만들 참고할 <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 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',
showPreviewModal: false,
previewCard: null,
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 디자인 연구',
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;
},
openPreviewModal(card) {
this.previewCard = card;
this.showPreviewModal = true;
},
getCategoryLabel(code) {
const c = this.categories.find(cat => cat.code === code);
return c ? c.icon + ' ' + c.label : code;
},
getWireframe(card) {
const t = (card.title || '').toLowerCase();
const tags = (card.tags || []).join(' ').toLowerCase();
const key = t + ' ' + tags;
// KPI 대시보드
if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<div class="wf-circle" style="width: 16px; height: 16px; background: #bfdbfe;"></div>
<div class="wf-bar md dark"></div><div style="flex:1;"></div>
<div class="wf-bar sm" style="background: #bfdbfe; border-radius: 4px; height: 14px;"></div>
</div>
<div style="padding: 12px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">매출</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">₩24.5M</div><div style="font-size: 8px; color: #10b981;">▲ 12.5%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">수주</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">148건</div><div style="font-size: 8px; color: #10b981;">▲ 8.2%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">미수금</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">₩3.2M</div><div style="font-size: 8px; color: #ef4444;">▲ 2.1%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">거래처</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">52개</div><div style="font-size: 8px; color: #10b981;">▲ 3건</div></div>
</div>
<div style="padding: 0 12px 12px; display: grid; grid-template-columns: 2fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 12px; height: 100px;">
<div class="wf-text" style="margin-bottom: 8px;">월별 매출 추이</div>
<svg width="100%" height="50" viewBox="0 0 200 50"><polyline points="10,40 40,30 70,35 100,20 130,25 160,10 190,15" fill="none" stroke="#3b82f6" stroke-width="2"/><polyline points="10,40 40,30 70,35 100,20 130,25 160,10 190,15" fill="url(#grad)" stroke="none"/><defs><linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3b82f6" stop-opacity="0.2"/><stop offset="100%" stop-color="#3b82f6" stop-opacity="0"/></linearGradient></defs></svg>
</div>
<div class="wf-box" style="padding: 12px; height: 100px;">
<div class="wf-text" style="margin-bottom: 8px;">최근 활동</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#10b981;"></div><div class="wf-bar lg"></div></div>
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#3b82f6;"></div><div class="wf-bar xl"></div></div>
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#f59e0b;"></div><div class="wf-bar md"></div></div>
</div>
</div>
</div>
</div>`;
// 데이터 테이블
if (key.includes('테이블') || key.includes('검색/필터') || key.includes('페이지네이션')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 10px 12px; border-bottom: 1px solid #e2e8f0; display: flex; gap: 8px; align-items: center;">
<div style="flex:1; height: 28px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; padding: 0 10px;"><span style="color: #94a3b8; font-size: 9px;">🔍 검색어를 입력하세요...</span></div>
<div style="display: flex; gap: 4px;">
<div style="padding: 4px 10px; background: #eff6ff; border-radius: 4px; font-size: 8px; color: #3b82f6;">상태 ▾</div>
<div style="padding: 4px 10px; background: #f1f5f9; border-radius: 4px; font-size: 8px; color: #64748b;">날짜 ▾</div>
</div>
</div>
<div style="padding: 0 12px;">
<div style="display: grid; grid-template-columns: 30px 2fr 1fr 1fr 80px; gap: 8px; padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-size: 8px; font-weight: 700; color: #94a3b8;">
<div>☐</div><div>이름 ↕</div><div>상태</div><div>날짜</div><div>액션</div>
</div>
${[1,2,3,4,5].map(i => `<div style="display: grid; grid-template-columns: 30px 2fr 1fr 1fr 80px; gap: 8px; padding: 7px 0; border-bottom: 1px solid #f1f5f9; font-size: 8px; color: #475569; align-items: center;">
<div></div><div class="wf-bar" style="width:${60+i*15}px;height:8px;"></div>
<div><span style="padding:2px 6px;border-radius:3px;background:${['#ecfdf5','#fef3c7','#eff6ff'][i%3]};font-size:7px;">${['완료','진행중','대기'][i%3]}</span></div>
<div style="color:#94a3b8;">2026-03-0${i}</div>
<div style="display:flex;gap:4px;"><span style="color:#3b82f6;font-size:7px;">보기</span><span style="color:#94a3b8;font-size:7px;">삭제</span></div>
</div>`).join('')}
</div>
<div style="padding: 8px 12px; border-top: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 8px; color: #94a3b8;">1-5 / 128건</span>
<div style="display: flex; gap: 3px;">
${['◀','1','2','3','...','26','▶'].map(p => `<span style="padding:2px 6px;border-radius:3px;background:${p==='1'?'#3b82f6':'#f1f5f9'};color:${p==='1'?'#fff':'#64748b'};font-size:8px;">${p}</span>`).join('')}
</div>
</div>
</div>`;
// 칸반 보드
if (key.includes('칸반') || key.includes('kanban')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<div class="wf-bar md dark"></div><div style="flex:1;"></div>
<div style="font-size: 8px; color: #94a3b8;">필터 </div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0; min-height: 200px;">
${['할 일|3|#94a3b8','진행 중|2|#3b82f6','검토|1|#f59e0b','완료|4|#10b981'].map(col => {
const [name,cnt,color] = col.split('|');
return `<div style="border-right: 1px solid #f1f5f9; padding: 8px 6px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:8px;font-weight:700;color:${color};">${name}</span>
<span style="font-size:7px;background:#f1f5f9;padding:1px 5px;border-radius:8px;color:#94a3b8;">${cnt}</span>
</div>
${Array.from({length:parseInt(cnt)},(_,i)=>`<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:6px 8px;margin-bottom:4px;box-shadow:0 1px 2px rgba(0,0,0,.04);">
<div class="wf-bar" style="width:${50+i*20}px;height:7px;margin-bottom:4px;"></div>
<div style="display:flex;gap:3px;"><span style="font-size:6px;padding:1px 4px;background:#f1f5f9;border-radius:2px;">태그</span></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:4px;">
<div class="wf-circle" style="width:12px;height:12px;background:${color};opacity:.3;"></div>
<span style="font-size:6px;color:#94a3b8;">3/8</span>
</div>
</div>`).join('')}
<div style="text-align:center;padding:4px;font-size:8px;color:#94a3b8;border:1px dashed #e2e8f0;border-radius:4px;cursor:pointer;">+ 추가</div>
</div>`;
}).join('')}
</div>
</div>`;
// Command Palette
if (key.includes('command') || key.includes('cmd+k') || key.includes('커맨드')) return `
<div style="width: 100%; max-width: 400px; background: rgba(0,0,0,.5); border-radius: 12px; padding: 40px 30px; display: flex; justify-content: center;">
<div style="width: 100%; background: #fff; border-radius: 10px; box-shadow: 0 20px 60px rgba(0,0,0,.3); overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid #e2e8f0;">
<div style="background: #f8fafc; border-radius: 6px; padding: 8px 12px; display: flex; align-items: center; gap: 8px;">
<span style="color: #94a3b8; font-size: 12px;">🔍</span>
<span style="font-size: 10px; color: #94a3b8;">명령어 또는 페이지 검색...</span>
</div>
</div>
<div style="padding: 6px;">
<div style="font-size: 8px; color: #94a3b8; padding: 4px 10px; font-weight: 600;">최근</div>
${['수주 관리|📋','거래처 목록|🏢','품목 기준관리|📦'].map(item => {
const [n,ic] = item.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;cursor:pointer;"><span>${ic}</span><span style="font-size:10px;flex:1;">${n}</span><span style="font-size:8px;color:#94a3b8;">↵</span></div>`;
}).join('')}
<div style="font-size: 8px; color: #94a3b8; padding: 4px 10px; font-weight: 600; margin-top: 4px;">액션</div>
${['새 수주 등록|','설정 열기|⚙️','도움말|❓'].map(item => {
const [n,ic] = item.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;cursor:pointer;"><span>${ic}</span><span style="font-size:10px;flex:1;">${n}</span></div>`;
}).join('')}
</div>
<div style="padding: 6px 12px; border-top: 1px solid #f1f5f9; display: flex; gap: 12px;">
<span style="font-size: 7px; color: #94a3b8;">↑↓ 이동</span>
<span style="font-size: 7px; color: #94a3b8;"> 선택</span>
<span style="font-size: 7px; color: #94a3b8;">ESC 닫기</span>
</div>
</div>
</div>`;
// 사이드바
if (key.includes('사이드바') && key.includes('네비게이션')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 240px;">
<div style="width: 160px; background: #1e293b; padding: 12px 8px; display: flex; flex-direction: column; gap: 2px; flex-shrink: 0;">
<div style="display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-bottom: 8px;"><div class="wf-circle" style="width: 20px; height: 20px; background: #3b82f6;"></div><span style="font-size: 10px; color: #fff; font-weight: 700;">SAM</span></div>
<div style="font-size: 7px; color: #64748b; padding: 4px 8px; font-weight: 600;">메인</div>
${['📊 대시보드|true','📋 수주관리|false','🏢 거래처|false','📦 품목관리|false'].map(m => {
const [n,a] = m.split('|');
return `<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:4px;font-size:9px;color:${a==='true'?'#fff':'#94a3b8'};background:${a==='true'?'rgba(59,130,246,.2)':'transparent'};${a==='true'?'border-left:2px solid #3b82f6;':''}">${n}</div>`;
}).join('')}
<div style="font-size: 7px; color: #64748b; padding: 4px 8px; font-weight: 600; margin-top: 6px;">관리</div>
${['⚙️ 설정','👥 사용자','🔔 알림'].map(n => `<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;font-size:9px;color:#94a3b8;">${n}</div>`).join('')}
<div style="flex:1;"></div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;border-top:1px solid #334155;margin-top:8px;"><div class="wf-circle" style="width:18px;height:18px;background:#64748b;"></div><span style="font-size:8px;color:#94a3b8;">홍길동</span></div>
</div>
<div style="flex: 1; padding: 16px; background: #f8fafc;">
<div class="wf-bar xl dark" style="height: 12px; margin-bottom: 12px;"></div>
<div class="wf-bar" style="width: 90%; height: 8px; margin-bottom: 6px;"></div>
<div class="wf-bar" style="width: 70%; height: 8px; margin-bottom: 16px;"></div>
<div class="wf-box" style="height: 120px; padding: 12px;"><div class="wf-text">콘텐츠 영역</div></div>
</div>
</div>`;
// 모달 폼
if (key.includes('모달') && (key.includes('폼') || key.includes('생성'))) return `
<div style="width: 100%; max-width: 420px; background: rgba(0,0,0,.4); border-radius: 12px; padding: 30px; display: flex; justify-content: center;">
<div style="width: 100%; background: #fff; border-radius: 10px; box-shadow: 0 20px 60px rgba(0,0,0,.2); overflow: hidden;">
<div style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 12px; font-weight: 700;"> 항목 등록</span><span style="color: #94a3b8; cursor: pointer;"></span>
</div>
<div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
${['이름 *','카테고리','설명'].map(f => `<div>
<div style="font-size: 9px; font-weight: 600; color: #475569; margin-bottom: 4px;">${f}</div>
<div style="height: ${f==='설명'?'48px':'28px'}; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;"></div>
</div>`).join('')}
</div>
<div style="padding: 12px 16px; border-top: 1px solid #e2e8f0; display: flex; justify-content: flex-end; gap: 6px;">
<div style="padding: 5px 14px; border-radius: 6px; font-size: 10px; color: #64748b; border: 1px solid #e2e8f0;">취소</div>
<div style="padding: 5px 14px; border-radius: 6px; font-size: 10px; color: #fff; background: #3b82f6;">저장</div>
</div>
</div>
</div>`;
// 설정
if (key.includes('설정') && key.includes('섹션')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 130px; border-right: 1px solid #e2e8f0; padding: 12px 6px; display: flex; flex-direction: column; gap: 1px;">
${['👤 프로필|true','🔔 알림','🔐 보안','🎨 테마','⚡ 연동','🗑️ 위험'].map(m => {
const [n,a] = m.split('|');
return `<div style="padding:5px 8px;border-radius:4px;font-size:9px;color:${a?'#3b82f6':'#64748b'};background:${a?'#eff6ff':'transparent'};font-weight:${a?'600':'400'};">${n}</div>`;
}).join('')}
</div>
<div style="flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 12px; overflow: hidden;">
<div style="font-size: 12px; font-weight: 700; color: #1e293b;">프로필 설정</div>
${['프로필 사진','이름','이메일'].map(f => `<div class="wf-box" style="padding: 10px 12px; display: flex; justify-content: space-between; align-items: center;">
<div><div style="font-size:9px;font-weight:600;color:#475569;">${f}</div><div class="wf-bar md" style="margin-top:3px;"></div></div>
<div style="font-size:8px;color:#3b82f6;">변경</div>
</div>`).join('')}
<div style="padding: 5px 12px; background: #3b82f6; color: #fff; border-radius: 6px; font-size: 9px; align-self: flex-start;">저장</div>
</div>
</div>`;
// 타임라인
if (key.includes('타임라인') || key.includes('활동') && key.includes('피드')) return `
<div class="wf-wrap" style="padding: 16px 16px 16px 32px;">
<div style="font-size: 10px; font-weight: 700; color: #1e293b; margin-bottom: 12px;">활동 기록</div>
<div style="position: relative; padding-left: 20px;">
<div style="position: absolute; left: 5px; top: 0; bottom: 0; width: 2px; background: #e2e8f0;"></div>
${[
{color:'#10b981',text:'수주 #1024 등록 완료',time:'10분 전',user:'김영업'},
{color:'#3b82f6',text:'거래처 "ABC상사" 정보 수정',time:'1시간 전',user:'이관리'},
{color:'#f59e0b',text:'견적서 #502 승인 대기',time:'3시간 전',user:'박대리'},
{color:'#8b5cf6',text:'품목 "BL-200" 단가 변경',time:'어제',user:'최기준'},
].map(e => `<div style="position:relative;margin-bottom:16px;">
<div style="position:absolute;left:-18px;top:2px;width:12px;height:12px;border-radius:50%;background:${e.color};border:2px solid #fff;"></div>
<div class="wf-box" style="padding:8px 12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
<span style="font-size:10px;font-weight:600;color:#1e293b;">${e.text}</span>
<span style="font-size:8px;color:#94a3b8;">${e.time}</span>
</div>
<span style="font-size:8px;color:#64748b;">${e.user}</span>
</div>
</div>`).join('')}
</div>
</div>`;
// 트리 + 상세
if (key.includes('트리') && key.includes('분할')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 150px; border-right: 1px solid #e2e8f0; padding: 10px 6px; font-size: 9px; overflow: hidden;">
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;font-weight:600;color:#1e293b;">📁 전체 메뉴</div>
<div style="padding-left:12px;">
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 영업관리</div>
<div style="padding-left:14px;">
<div style="padding:3px 4px;color:#3b82f6;background:#eff6ff;border-radius:3px;font-weight:600;">📄 수주관리</div>
<div style="padding:3px 4px;color:#64748b;">📄 거래처</div>
<div style="padding:3px 4px;color:#64748b;">📄 견적서</div>
</div>
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 생산관리</div>
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 품질관리</div>
</div>
</div>
<div style="flex: 1; padding: 14px;">
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:10px;">📄 수주관리</div>
<div style="display:flex;flex-direction:column;gap:8px;">
${['메뉴명|수주관리','URL|/sales/orders','아이콘|shopping-cart','정렬|3'].map(f => {
const [k,v] = f.split('|');
return `<div style="display:flex;gap:8px;align-items:center;"><span style="font-size:8px;color:#94a3b8;width:40px;">${k}</span><div style="flex:1;height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 8px;font-size:9px;line-height:24px;color:#475569;">${v}</div></div>`;
}).join('')}
</div>
</div>
</div>`;
// 캘린더
if (key.includes('캘린더') || key.includes('일정')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<span style="font-size:10px;color:#94a3b8;"></span>
<span style="font-size:11px;font-weight:700;color:#1e293b;">2026 3</span>
<span style="font-size:10px;color:#94a3b8;"></span>
<div style="flex:1;"></div>
<div style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">오늘</div>
<div style="display:flex;gap:2px;"><span style="font-size:8px;padding:2px 6px;background:#f1f5f9;border-radius:3px;"></span><span style="font-size:8px;padding:2px 6px;background:#eff6ff;color:#3b82f6;border-radius:3px;"></span></div>
</div>
<div style="display: grid; grid-template-columns: repeat(7, 1fr); font-size: 8px; text-align: center;">
${['일','월','화','수','목','금','토'].map(d => `<div style="padding:4px;color:#94a3b8;font-weight:600;border-bottom:1px solid #f1f5f9;">${d}</div>`).join('')}
${Array.from({length:35},(_, i) => {
const day = i - 5;
const isToday = day === 8;
const hasEvent = [3,8,12,15,22].includes(day);
return `<div style="padding:4px 2px;min-height:28px;border-bottom:1px solid #f8fafc;border-right:1px solid #f8fafc;${day<1||day>31?'color:#cbd5e1;':''}${isToday?'background:#eff6ff;':''}">
<div style="${isToday?'background:#3b82f6;color:#fff;width:16px;height:16px;border-radius:50%;margin:0 auto;line-height:16px;':'color:#475569;'}">${day<1?'':day>31?'':day}</div>
${hasEvent?`<div style="margin-top:1px;height:3px;background:${isToday?'#3b82f6':'#10b981'};border-radius:2px;"></div>`:''}
</div>`;
}).join('')}
</div>
</div>`;
// 카드 그리드
if (key.includes('카드') && key.includes('그리드')) return `
<div class="wf-wrap" style="padding: 12px;">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
${Array.from({length:6},(_,i) => `<div style="border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;">
<div style="height:60px;background:${['#bfdbfe','#bbf7d0','#fde68a','#ddd6fe','#fecaca','#e0e7ff'][i]};"></div>
<div style="padding:6px 8px;">
<div class="wf-bar" style="width:${50+i*10}px;height:7px;margin-bottom:4px;"></div>
<div class="wf-bar sm" style="height:6px;"></div>
</div>
</div>`).join('')}
</div>
</div>`;
// 가격표
if (key.includes('가격') || key.includes('플랜')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="text-align: center; margin-bottom: 12px;"><span style="font-size:11px;font-weight:700;color:#1e293b;">요금제 선택</span></div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
${[{name:'Basic',price:'₩49,000',color:'#64748b',pop:false},{name:'Pro',price:'₩99,000',color:'#3b82f6',pop:true},{name:'Enterprise',price:'문의',color:'#8b5cf6',pop:false}].map(p => `
<div style="border-radius:8px;border:${p.pop?'2px solid #3b82f6':'1px solid #e2e8f0'};padding:12px;text-align:center;position:relative;${p.pop?'box-shadow:0 4px 12px rgba(59,130,246,.15);':''}">
${p.pop?'<div style="position:absolute;top:-8px;left:50%;transform:translateX(-50%);background:#3b82f6;color:#fff;font-size:7px;padding:1px 8px;border-radius:8px;">인기</div>':''}
<div style="font-size:10px;font-weight:600;color:${p.color};margin-bottom:4px;">${p.name}</div>
<div style="font-size:16px;font-weight:800;color:#1e293b;margin-bottom:8px;">${p.price}</div>
${['기능 A','기능 B','기능 C'].map(f => `<div style="font-size:8px;color:#64748b;padding:2px 0;">✓ ${f}</div>`).join('')}
<div style="margin-top:8px;padding:4px;background:${p.pop?p.color:'#f1f5f9'};color:${p.pop?'#fff':'#64748b'};border-radius:4px;font-size:8px;">선택</div>
</div>
`).join('')}
</div>
</div>`;
// 채팅
if (key.includes('채팅') || key.includes('메시징')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 120px; background: #f8fafc; border-right: 1px solid #e2e8f0; padding: 8px 4px;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;padding:4px 6px;">채널</div>
${['# 일반','# 영업팀','# 개발팀'].map((c,i) => `<div style="padding:4px 6px;font-size:9px;border-radius:4px;${i===1?'background:#eff6ff;color:#3b82f6;font-weight:600;':'color:#64748b;'}">${c}</div>`).join('')}
<div style="font-size:8px;font-weight:600;color:#94a3b8;padding:4px 6px;margin-top:6px;">DM</div>
${['김영업','이관리'].map(c => `<div style="display:flex;align-items:center;gap:4px;padding:4px 6px;font-size:9px;color:#64748b;"><div class="wf-circle" style="width:12px;height:12px;"></div>${c}</div>`).join('')}
</div>
<div style="flex:1;display:flex;flex-direction:column;">
<div style="padding:6px 12px;border-bottom:1px solid #e2e8f0;font-size:10px;font-weight:600;">🔒 # 영업팀</div>
<div style="flex:1;padding:10px 12px;display:flex;flex-direction:column;gap:8px;overflow:hidden;">
<div style="display:flex;gap:6px;"><div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div><div><div style="font-size:8px;font-weight:600;color:#1e293b;">김영업 <span style="color:#94a3b8;font-weight:400;">10:30</span></div><div style="font-size:9px;color:#475569;background:#f1f5f9;padding:4px 8px;border-radius:0 8px 8px 8px;margin-top:2px;">수주건 확인 부탁드립니다</div></div></div>
<div style="display:flex;gap:6px;justify-content:flex-end;"><div><div style="font-size:9px;color:#fff;background:#3b82f6;padding:4px 8px;border-radius:8px 0 8px 8px;">확인했습니다 👍</div></div></div>
<div style="display:flex;gap:6px;"><div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div><div><div style="font-size:8px;font-weight:600;color:#1e293b;">이관리 <span style="color:#94a3b8;font-weight:400;">11:05</span></div><div style="font-size:9px;color:#475569;background:#f1f5f9;padding:4px 8px;border-radius:0 8px 8px 8px;margin-top:2px;">거래처 정보 업데이트 완료</div></div></div>
</div>
<div style="padding:8px;border-top:1px solid #e2e8f0;display:flex;gap:6px;align-items:center;">
<div style="flex:1;height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:0 8px;font-size:9px;line-height:28px;color:#94a3b8;">메시지 입력...</div>
<div style="font-size:10px;">📎</div><div style="font-size:10px;">😊</div>
</div>
</div>
</div>`;
// 파일 업로드
if (key.includes('파일') && key.includes('업로드')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="border: 2px dashed #cbd5e1; border-radius: 10px; padding: 20px; text-align: center; margin-bottom: 12px;">
<div style="font-size: 24px; margin-bottom: 4px;">📁</div>
<div style="font-size: 10px; color: #64748b;">파일을 드래그하거나 <span style="color: #3b82f6; text-decoration: underline;">클릭</span>하여 업로드</div>
<div style="font-size: 8px; color: #94a3b8; margin-top: 4px;">PNG, JPG, PDF (최대 10MB)</div>
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
${[{name:'견적서_v2.pdf',size:'2.4MB',pct:100,status:'완료'},{name:'도면_A-101.png',size:'5.1MB',pct:67,status:'업로드 중...'},{name:'스펙시트.xlsx',size:'1.8MB',pct:30,status:'대기'}].map(f => `
<div style="display:flex;align-items:center;gap:8px;padding:8px;border:1px solid #e2e8f0;border-radius:6px;">
<span style="font-size:16px;">${f.name.includes('.pdf')?'📄':f.name.includes('.png')?'🖼️':'📊'}</span>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;"><span style="font-size:9px;font-weight:600;color:#1e293b;">${f.name}</span><span style="font-size:8px;color:#94a3b8;">${f.size}</span></div>
<div style="height:4px;background:#f1f5f9;border-radius:2px;margin-top:4px;overflow:hidden;"><div style="height:100%;width:${f.pct}%;background:${f.pct===100?'#10b981':'#3b82f6'};border-radius:2px;"></div></div>
</div>
<span style="font-size:8px;color:${f.pct===100?'#10b981':'#64748b'};">${f.status}</span>
</div>
`).join('')}
</div>
</div>`;
// 브레드크럼
if (key.includes('브레드크럼') || key.includes('breadcrumb')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 16px;">
${['🏠 홈','/','📋 영업관리','/','📊 수주관리','/','<b>#1024</b>'].map(b => b==='/'?`<span style="color:#cbd5e1;font-size:10px;">/</span>`:`<span style="font-size:10px;color:${b.includes('<b>')?'#1e293b':'#3b82f6'};${b.includes('<b>')?'font-weight:700;':''}">${b.replace(/<\/?b>/g,'')}</span>`).join('')}
</div>
<div style="font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;">수주 #1024</div>
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;"></div>
<div class="wf-bar lg" style="height:8px;"></div>
</div>`;
// 온보딩 스테퍼
if (key.includes('온보딩') || key.includes('스테퍼') || key.includes('위자드')) return `
<div class="wf-wrap" style="padding: 20px;">
<div style="display: flex; align-items: center; gap: 0; margin-bottom: 20px; padding: 0 20px;">
${[{n:'1',t:'회사 정보',done:true},{n:'2',t:'팀 설정',active:true},{n:'3',t:'데이터 연동'},{n:'4',t:'완료'}].map((s,i) => `
<div style="display:flex;align-items:center;gap:4px;${i>0?'flex:1;':''}">
${i>0?`<div style="flex:1;height:2px;background:${s.done||s.active?'#3b82f6':'#e2e8f0'};"></div>`:''}
<div style="width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;
${s.done?'background:#3b82f6;color:#fff;':s.active?'background:#fff;border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${s.done?'✓':s.n}</div>
</div>
`).join('')}
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:16px;padding:0 10px;">
${['회사 정보','팀 설정','데이터 연동','완료'].map((t,i) => `<span style="font-size:8px;color:${i<=1?'#3b82f6':'#94a3b8'};font-weight:${i===1?'600':'400'};text-align:center;">${t}</span>`).join('')}
</div>
<div class="wf-box" style="padding: 16px; text-align: center;">
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:4px;">팀 구성원을 초대하세요</div>
<div style="font-size:9px;color:#64748b;margin-bottom:12px;">함께 사용할 팀원의 이메일을 입력하세요</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:8px;"></div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:12px;"></div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:9px;color:#3b82f6;"> 이전</span>
<div style="padding:4px 14px;background:#3b82f6;color:#fff;border-radius:4px;font-size:9px;">다음 </div>
</div>
</div>
</div>`;
// 토스트
if (key.includes('토스트') || key.includes('알림') && key.includes('시스템')) return `
<div style="width: 100%; max-width: 380px; position: relative; height: 240px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; overflow: hidden;">
<div style="padding: 12px; opacity: .3;">
<div class="wf-bar xl" style="height: 10px; margin-bottom: 8px;"></div>
<div class="wf-bar lg" style="height: 8px;"></div>
</div>
<div style="position: absolute; bottom: 12px; right: 12px; display: flex; flex-direction: column; gap: 6px; width: 220px;">
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #10b981;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;"></span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 완료</div><div style="font-size:8px;color:#64748b;">수주 정보가 저장되었습니다</div></div>
<span style="color:#94a3b8;font-size:10px;cursor:pointer;"></span>
</div>
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #ef4444;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;"></span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 실패</div><div style="font-size:8px;color:#64748b;">네트워크 오류가 발생했습니다</div></div>
<span style="color:#94a3b8;font-size:10px;cursor:pointer;"></span>
</div>
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #f59e0b;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;">⚠️</span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 변경</div><div style="font-size:8px;color:#64748b;">변경사항을 저장하세요</div></div>
<span style="font-size:8px;color:#3b82f6;cursor:pointer;">되돌리기</span>
</div>
</div>
</div>`;
// Empty State
if (key.includes('empty') || key.includes('빈') && key.includes('상태')) return `
<div class="wf-wrap" style="padding: 40px; text-align: center;">
<div style="font-size: 48px; margin-bottom: 12px; opacity: .6;">📭</div>
<div style="font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 6px;">아직 수주가 없습니다</div>
<div style="font-size: 10px; color: #94a3b8; margin-bottom: 16px; line-height: 1.6;"> 번째 수주를 등록하여<br>영업 관리를 시작하세요</div>
<div style="display:inline-flex;align-items:center;gap:4px;padding:6px 16px;background:#3b82f6;color:#fff;border-radius:6px;font-size:10px;font-weight:600;">+ 수주 등록</div>
<div style="margin-top:10px;font-size:9px;color:#3b82f6;">도움말 보기 </div>
</div>`;
// 검색 자동완성
if (key.includes('자동완성') || key.includes('서제스트')) return `
<div style="width: 100%; max-width: 400px;">
<div class="wf-wrap" style="padding: 12px; overflow: visible; position: relative;">
<div style="background:#f8fafc;border:2px solid #3b82f6;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;position:relative;z-index:2;">
<span style="color:#94a3b8;">🔍</span><span style="font-size:11px;color:#1e293b;">블라인</span><span style="animation:blink 1s infinite;color:#3b82f6;">|</span>
</div>
<div style="position:absolute;left:12px;right:12px;top:44px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:3;overflow:hidden;">
${['블라인드 50mm 수동|품목','블라인드 25mm 전동|품목','블라인드 롤스크린 조합|품목','블라인드코리아|거래처'].map((r,i) => {
const [name,cat] = r.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:7px 12px;${i===0?'background:#f8fafc;':''}cursor:pointer;">
<span style="font-size:9px;padding:1px 6px;background:#f1f5f9;border-radius:3px;color:#64748b;">${cat}</span>
<span style="font-size:10px;color:#1e293b;"><b style="color:#3b82f6;">블라인</b>${name.replace('블라인','')}</span>
${i===0?'<span style="margin-left:auto;font-size:8px;color:#94a3b8;">↵</span>':''}
</div>`;
}).join('')}
</div>
</div>
<div style="height: 80px;"></div>
</div>`;
// 탭 레이아웃
if (key.includes('탭') && key.includes('레이아웃')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="display: flex; border-bottom: 2px solid #e2e8f0; padding: 0 12px;">
${['📊 개요|true','📋 상세|false','📎 첨부|false','💬 댓글|false','📜 이력|false'].map(t => {
const [n,a] = t.split('|');
return `<div style="padding:8px 12px;font-size:9px;${a==='true'?'color:#3b82f6;border-bottom:2px solid #3b82f6;font-weight:600;margin-bottom:-2px;':'color:#94a3b8;'}">${n}${n.includes('댓글')?'<span style="background:#ef4444;color:#fff;font-size:7px;padding:0 4px;border-radius:8px;margin-left:3px;">3</span>':''}</div>`;
}).join('')}
</div>
<div style="padding: 14px;">
<div class="wf-bar xl dark" style="height: 10px; margin-bottom: 10px;"></div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 10px; height: 60px;"><div class="wf-text">요약 정보</div></div>
<div class="wf-box" style="padding: 10px; height: 60px;"><div class="wf-text">차트</div></div>
</div>
</div>
</div>`;
// 기본 와이어프레임 (매칭 안 됨)
return `
<div class="wf-wrap" style="padding: 24px; text-align: center;">
<div style="font-size: 32px; margin-bottom: 12px; opacity: .5;"><i class="${this.getTypeIcon(card.type)}" style="font-size:32px;color:#94a3b8;"></i></div>
<div style="font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 4px;">${card.title || ''}</div>
<div style="font-size: 10px; color: #94a3b8;">이미지를 추가하면 여기에 표시됩니다</div>
</div>`;
},
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);
},
// ===== Preset Templates =====
loadPresetTemplates() {
const existing = (this.currentProject.cards || []).length;
if (existing > 0 && !confirm('현재 프로젝트에 인기 UI 패턴 20종을 추가합니다. 계속하시겠습니까?')) return;
const now = new Date().toISOString();
const mkId = () => 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
const presets = [
{
type: 'pattern', title: 'KPI 대시보드', category: 'dashboard', rating: 5,
tags: ['대시보드', 'KPI', '통계', '차트'],
memo: 'Stripe, Shopify, Vercel 등 SaaS 서비스에서 사용하는 핵심 패턴. 로그인 직후 전체 현황을 3초 안에 파악할 수 있어야 한다.',
components: [
{ name: 'KPI 요약 카드 (4~6개, 상단 고정)', required: true },
{ name: '추이 차트 (라인/바 차트, 기간 선택)', required: true },
{ name: '최근 활동 피드 / 알림', required: false },
{ name: '빠른 액션 버튼', required: false },
{ name: '기간 필터 (오늘/주/월/커스텀)', required: true },
],
usedIn: ['Stripe Dashboard', 'Shopify Admin', 'Vercel Dashboard', 'SAM 메인 대시보드'],
guidelines: 'KPI 카드는 숫자 + 변화율(▲▼) + 미니 스파크라인 조합. 가장 중요한 지표를 좌상단에 배치. 색상은 긍정(초록)/부정(빨강)으로 즉시 인지 가능하게.',
},
{
type: 'pattern', title: '데이터 테이블 + 검색/필터', category: 'list', rating: 5,
tags: ['테이블', '검색', '필터', '정렬', '페이지네이션'],
memo: 'Airtable, Notion Database, GitHub Issues 등 데이터 중심 서비스의 핵심 패턴. CRUD 목록 화면의 표준.',
components: [
{ name: '검색바 (상단 고정, 플레이스홀더 힌트)', required: true },
{ name: '필터 칩/드롭다운 (상태, 날짜, 카테고리)', required: true },
{ name: '데이터 테이블 (컬럼 정렬, 행 선택)', required: true },
{ name: '페이지네이션 / 무한 스크롤', required: true },
{ name: '벌크 액션 바 (선택 시 나타남)', required: false },
{ name: '컬럼 커스터마이징 (표시/숨기기)', required: false },
],
usedIn: ['Airtable', 'Notion', 'GitHub Issues', 'SAM 수주목록, 거래처목록, 품목목록'],
guidelines: '검색은 debounce 300ms 적용. 필터 상태는 URL 파라미터로 유지 (뒤로가기 대응). 빈 상태 시 "결과 없음" + 필터 초기화 버튼 제공.',
},
{
type: 'pattern', title: '칸반 보드 (Kanban)', category: 'dashboard', rating: 5,
tags: ['칸반', '드래그앤드롭', '워크플로우', '상태관리'],
memo: 'Trello, Jira, Linear, Notion Board 등 프로젝트 관리 도구의 핵심. 작업 상태를 시각적으로 한눈에 파악.',
components: [
{ name: '컬럼 헤더 (상태명 + 카드 수)', required: true },
{ name: '드래그 가능 카드 (제목 + 라벨 + 담당자)', required: true },
{ name: '컬럼 간 드래그 앤 드롭', required: true },
{ name: '카드 추가 버튼 (각 컬럼 하단)', required: true },
{ name: '필터 (담당자, 라벨, 우선순위)', required: false },
{ name: 'WIP 제한 표시', required: false },
],
usedIn: ['Trello', 'Jira', 'Linear', 'Notion Board', 'GitHub Projects'],
guidelines: '컬럼은 3~6개 권장 (너무 많으면 가독성 저하). 카드에는 핵심 정보만 표시 (제목 + 라벨 + 아바타). 드래그 시 시각적 피드백 필수.',
},
{
type: 'pattern', title: 'Command Palette (Cmd+K)', category: 'navigation', rating: 5,
tags: ['커맨드팔레트', '검색', '네비게이션', '키보드'],
memo: 'Linear, Vercel, GitHub, VS Code, Figma 등 파워유저 대상 서비스의 필수 패턴. 키보드만으로 모든 기능에 접근.',
components: [
{ name: '오버레이 모달 (화면 중앙 상단)', required: true },
{ name: '검색 입력란 (자동 포커스)', required: true },
{ name: '결과 목록 (아이콘 + 이름 + 단축키)', required: true },
{ name: '카테고리 그룹핑 (페이지, 액션, 최근)', required: true },
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
{ name: '최근 사용 기록', required: false },
],
usedIn: ['Linear', 'Vercel', 'GitHub', 'VS Code', 'Figma', 'Raycast'],
guidelines: 'Cmd+K 또는 Ctrl+K로 열기. 타이핑 즉시 퍼지 검색. 결과는 최대 8~10개 표시. ESC로 닫기. 최근 사용 항목 상단 표시.',
},
{
type: 'pattern', title: '사이드바 네비게이션', category: 'navigation', rating: 5,
tags: ['사이드바', '메뉴', '네비게이션', '트리'],
memo: 'Slack, Discord, Notion, Linear, VS Code 등 거의 모든 SaaS 앱의 기본 네비게이션. 접기/펼치기 + 트리 구조.',
components: [
{ name: '로고/앱 이름 (상단)', required: true },
{ name: '메인 메뉴 그룹 (아이콘 + 라벨)', required: true },
{ name: '접기/펼치기 토글', required: true },
{ name: '현재 위치 하이라이트', required: true },
{ name: '트리 구조 (하위 메뉴 들여쓰기)', required: false },
{ name: '즐겨찾기/고정 섹션', required: false },
{ name: '사용자 프로필 (하단)', required: false },
],
usedIn: ['Slack', 'Discord', 'Notion', 'Linear', 'VS Code', 'SAM MNG 사이드바'],
guidelines: '너비 240~280px 권장. 접힌 상태에서는 아이콘만 표시 (56px). 메뉴 그룹 간 구분선. 활성 항목은 배경색 + 좌측 인디케이터.',
},
{
type: 'pattern', title: '모달 폼 (생성/편집)', category: 'modal', rating: 4,
tags: ['모달', '폼', 'CRUD', '입력'],
memo: '대부분의 SaaS에서 레코드 생성/편집에 사용. 페이지 이동 없이 빠르게 데이터 입력. Notion, Linear, Jira 등.',
components: [
{ name: '오버레이 배경 (클릭 시 닫기)', required: true },
{ name: '모달 헤더 (제목 + 닫기 버튼)', required: true },
{ name: '폼 필드 (라벨 + 입력 + 검증 메시지)', required: true },
{ name: '액션 버튼 (저장/취소, 우하단)', required: true },
{ name: '키보드 지원 (ESC 닫기, Enter 저장)', required: true },
{ name: '로딩 상태 표시', required: false },
],
usedIn: ['Linear', 'Notion', 'Jira', 'SAM 등록/수정 팝업'],
guidelines: '너비 480~640px 권장. 필드 5개 이하면 모달, 그 이상이면 전체 페이지 고려. 필수 필드 * 표시. Tab 순서 정확히 설정.',
},
{
type: 'pattern', title: '설정 페이지 (그룹 섹션)', category: 'form', rating: 4,
tags: ['설정', '프로필', '섹션', '그룹'],
memo: 'GitHub Settings, Vercel Settings, Notion Settings 등. 좌측 탭 메뉴 + 우측 섹션별 설정 카드.',
components: [
{ name: '좌측 탭 메뉴 (세로 목록)', required: true },
{ name: '섹션 카드 (제목 + 설명 + 입력 필드)', required: true },
{ name: '개별 저장 버튼 (섹션마다)', required: true },
{ name: '위험 영역 (빨간 테두리, 하단 배치)', required: false },
{ name: '변경사항 감지 (저장 안 된 변경 알림)', required: false },
],
usedIn: ['GitHub Settings', 'Vercel Settings', 'Notion Settings', 'SAM 시스템 설정'],
guidelines: '좌측 탭은 고정, 우측은 스크롤. 섹션 간 명확한 구분선. 위험 작업(삭제, 비활성화)은 페이지 최하단 빨간 영역에 배치.',
},
{
type: 'pattern', title: '타임라인/활동 피드', category: 'list', rating: 4,
tags: ['타임라인', '피드', '활동로그', '히스토리'],
memo: 'GitHub Activity, Twitter/X Feed, Slack Messages 등. 시간순 이벤트 흐름을 표시하는 패턴.',
components: [
{ name: '타임라인 세로선 (좌측)', required: true },
{ name: '이벤트 노드 (아이콘 + 시간)', required: true },
{ name: '이벤트 카드 (내용 + 작성자 + 시간)', required: true },
{ name: '날짜 구분선', required: false },
{ name: '더 보기 / 무한 스크롤', required: false },
{ name: '이벤트 타입별 아이콘/색상', required: true },
],
usedIn: ['GitHub', 'Twitter/X', 'Slack', 'Jira', 'SAM 변경이력'],
guidelines: '최신 이벤트 상단. 아이콘과 색상으로 이벤트 타입 즉시 구분. 시간 표시는 상대 시간(3분 전) + hover 시 절대 시간.',
},
{
type: 'pattern', title: '트리 + 상세 분할 뷰', category: 'form', rating: 4,
tags: ['트리', '분할뷰', '마스터-디테일', '패널'],
memo: 'VS Code 파일 탐색기, Figma 레이어 패널, macOS Finder 컬럼 뷰. 계층 구조를 탐색하며 상세를 확인.',
components: [
{ name: '좌측 트리 패널 (접기/펼치기 노드)', required: true },
{ name: '우측 상세 패널 (선택 항목 정보)', required: true },
{ name: '분할선 (드래그 리사이즈)', required: false },
{ name: '빈 상태 (선택 안 했을 때)', required: true },
{ name: '트리 검색/필터', required: false },
{ name: '드래그 앤 드롭 재정렬', required: false },
],
usedIn: ['VS Code', 'Figma', 'macOS Finder', 'SAM 메뉴관리, 조직도'],
guidelines: '좌측 패널 240~320px, 최소 너비 제한. 선택 항목 시각적 하이라이트. 트리 깊이 4단계 이하 권장. 키보드 화살표 네비게이션 지원.',
},
{
type: 'pattern', title: '온보딩 스테퍼/위자드', category: 'form', rating: 4,
tags: ['온보딩', '스테퍼', '위자드', '단계별'],
memo: 'Notion 초기 설정, Stripe 계정 생성, Linear 워크스페이스 설정 등. 복잡한 프로세스를 단계별로 안내.',
components: [
{ name: '진행 표시줄 (상단, 현재 단계 강조)', required: true },
{ name: '단계별 카드 (제목 + 설명 + 입력)', required: true },
{ name: '이전/다음 버튼', required: true },
{ name: '단계 건너뛰기 옵션', required: false },
{ name: '완료 축하 화면 (마지막 단계)', required: false },
{ name: '현재 진행률 (예: 3/5)', required: true },
],
usedIn: ['Notion', 'Stripe', 'Linear', 'Vercel', 'SAM 초기 설정'],
guidelines: '단계는 3~5개 권장. 각 단계는 하나의 주제에 집중. 이전 단계 데이터 유지. 진행률 시각적 표시 필수. 마지막 단계에서 전체 요약.',
},
{
type: 'pattern', title: '토스트 알림 시스템', category: 'etc', rating: 4,
tags: ['토스트', '알림', '피드백', '노티피케이션'],
memo: 'Vercel, Linear, Notion 등 거의 모든 SaaS 앱. 사용자 액션에 대한 즉각적인 피드백을 비침습적으로 제공.',
components: [
{ name: '토스트 컨테이너 (우하단 고정)', required: true },
{ name: '타입별 아이콘+색상 (성공/오류/경고/정보)', required: true },
{ name: '메시지 텍스트 + 선택적 액션 버튼', required: true },
{ name: '자동 사라짐 (3~5초)', required: true },
{ name: '수동 닫기 (X 버튼)', required: true },
{ name: '다중 토스트 스택', required: false },
],
usedIn: ['Vercel', 'Linear', 'Notion', 'Stripe', 'SAM 전역'],
guidelines: '성공=초록, 오류=빨강, 경고=노랑, 정보=파랑. 텍스트는 1줄 이내. 되돌리기(Undo) 액션 제공 시 사용자 신뢰도 향상. 오류는 자동 사라짐 비활성화.',
},
{
type: 'pattern', title: 'Empty State (빈 상태)', category: 'etc', rating: 4,
tags: ['빈상태', '온보딩', '가이드', 'CTA'],
memo: 'Dropbox, Mailchimp, Notion, Linear 등. 데이터가 없을 때 사용자를 안내하는 핵심 패턴. 이탈을 방지하고 첫 행동을 유도.',
components: [
{ name: '일러스트/아이콘 (시각적 안내)', required: true },
{ name: '제목 (상황 설명)', required: true },
{ name: '설명 텍스트 (다음 단계 안내)', required: true },
{ name: 'CTA 버튼 (첫 행동 유도)', required: true },
{ name: '대안 링크 (도움말, 템플릿 등)', required: false },
],
usedIn: ['Dropbox', 'Mailchimp', 'Notion', 'Linear', 'SAM 각 목록 화면'],
guidelines: '일러스트는 브랜드 톤 유지. CTA 버튼은 1개만 (선택 피로 방지). "데이터 없음" 대신 긍정적 표현 사용 ("첫 프로젝트를 시작하세요!").',
},
{
type: 'pattern', title: '검색 + 자동완성 드롭다운', category: 'navigation', rating: 4,
tags: ['검색', '자동완성', '드롭다운', '서제스트'],
memo: 'Google, Algolia, GitHub Search, Amazon 등. 타이핑과 동시에 결과를 보여주어 탐색 속도를 극대화.',
components: [
{ name: '검색 입력란 (돋보기 아이콘 + placeholder)', required: true },
{ name: '자동완성 드롭다운 (입력 시 표시)', required: true },
{ name: '결과 하이라이팅 (매칭 텍스트 볼드)', required: true },
{ name: '카테고리별 그룹핑', required: false },
{ name: '최근 검색 기록', required: false },
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
],
usedIn: ['Google', 'Algolia', 'GitHub', 'Amazon', 'SAM 품목 검색'],
guidelines: 'Debounce 200~300ms. 최소 2글자부터 검색. 결과 최대 8개. 키워드 하이라이팅 필수. ESC로 닫기, 외부 클릭 시 닫기.',
},
{
type: 'pattern', title: '탭 레이아웃', category: 'navigation', rating: 4,
tags: ['탭', '네비게이션', '섹션', '컨텐츠전환'],
memo: 'Google Analytics, Stripe Dashboard, GitHub Repo 등. 같은 페이지 내에서 컨텐츠 섹션을 전환.',
components: [
{ name: '탭 바 (가로 목록, 활성 탭 하이라이트)', required: true },
{ name: '탭 콘텐츠 영역', required: true },
{ name: '탭 카운트 배지', required: false },
{ name: '탭 아이콘 (선택적)', required: false },
{ name: '탭 스크롤 (많을 때 화살표)', required: false },
],
usedIn: ['Google Analytics', 'Stripe', 'GitHub', 'SAM 품목기준관리, 설정'],
guidelines: '탭 5개 이하 권장 (7개 초과 시 드롭다운 또는 더보기). 활성 탭은 하단 인디케이터 + 볼드. 탭 전환 시 URL 해시 변경 (북마크 가능).',
},
{
type: 'pattern', title: '카드 그리드 레이아웃', category: 'dashboard', rating: 4,
tags: ['카드', '그리드', '갤러리', '레이아웃'],
memo: 'Pinterest, Dribbble, Notion Gallery, YouTube 등. 시각적 콘텐츠를 격자로 배열. 정보 밀도와 시각적 매력 균형.',
components: [
{ name: '반응형 그리드 컨테이너', required: true },
{ name: '카드 (이미지 + 제목 + 메타정보)', required: true },
{ name: '호버 오버레이 (액션 버튼)', required: false },
{ name: '무한 스크롤 / 더 보기', required: false },
{ name: '필터/정렬 툴바', required: false },
],
usedIn: ['Pinterest', 'Dribbble', 'YouTube', 'Notion Gallery', 'SAM 대시보드 카드'],
guidelines: '카드 최소 너비 280px, 간격 16px. 이미지 비율 고정 (16:9 또는 4:3). 호버 시 그림자 + 미세 상승 효과. Skeleton 로딩 적용.',
},
{
type: 'pattern', title: '가격표/플랜 비교', category: 'etc', rating: 4,
tags: ['가격', '플랜', '비교', '테이블', 'CTA'],
memo: 'Stripe, Vercel, Notion, Slack 등 SaaS 가격 페이지. 플랜 간 차이를 한눈에 비교하여 구매 결정을 유도.',
components: [
{ name: '플랜 카드 (이름 + 가격 + 주요 기능)', required: true },
{ name: '추천 플랜 강조 (배지, 테두리)', required: true },
{ name: 'CTA 버튼 (각 플랜마다)', required: true },
{ name: '상세 기능 비교 테이블 (하단)', required: false },
{ name: '월간/연간 토글', required: false },
],
usedIn: ['Stripe', 'Vercel', 'Notion', 'Slack', 'SAM 요금 안내'],
guidelines: '3~4개 플랜 권장. 가장 인기 플랜을 시각적으로 강조. 가격은 크고 볼드하게. 무료 체험 CTA 제공. 기능 비교는 ✓/✗ 아이콘.',
},
{
type: 'pattern', title: '캘린더 뷰', category: 'dashboard', rating: 4,
tags: ['캘린더', '일정', '날짜', '이벤트'],
memo: 'Google Calendar, Calendly, Notion Calendar 등. 시간 기반 데이터를 날짜 격자에 시각화.',
components: [
{ name: '월/주/일 뷰 전환 탭', required: true },
{ name: '날짜 격자 (이벤트 표시)', required: true },
{ name: '이벤트 카드 (색상 코딩)', required: true },
{ name: '이전/다음 네비게이션', required: true },
{ name: '오늘 버튼 (빠른 이동)', required: true },
{ name: '이벤트 생성 (날짜 클릭)', required: false },
],
usedIn: ['Google Calendar', 'Calendly', 'Notion Calendar', 'SAM 일정관리, 근태'],
guidelines: '오늘 날짜 강조 (원형 배경). 이벤트 색상은 카테고리별 구분. 주말 배경색 다르게. 이벤트 3개 초과 시 "+N more" 표시.',
},
{
type: 'pattern', title: '채팅/메시징 인터페이스', category: 'etc', rating: 4,
tags: ['채팅', '메시징', '실시간', '대화'],
memo: 'Slack, Discord, Intercom, WhatsApp Web 등. 실시간 대화를 위한 인터페이스. 고객 지원 위젯에도 활용.',
components: [
{ name: '채널/대화 목록 (좌측)', required: true },
{ name: '메시지 영역 (시간순 스크롤)', required: true },
{ name: '메시지 입력란 (하단 고정)', required: true },
{ name: '메시지 버블 (내 메시지 우측, 상대 좌측)', required: true },
{ name: '첨부파일/이모지 버튼', required: false },
{ name: '읽음 표시 / 타이핑 인디케이터', required: false },
],
usedIn: ['Slack', 'Discord', 'Intercom', 'WhatsApp Web'],
guidelines: '새 메시지 자동 스크롤. 날짜 구분선. 연속 메시지는 아바타 한 번만 표시. 링크 미리보기. 이미지 인라인 표시.',
},
{
type: 'pattern', title: '파일 업로드 + 진행률', category: 'modal', rating: 3,
tags: ['업로드', '파일', '진행률', '드래그앤드롭'],
memo: 'Dropbox, Google Drive, WeTransfer, Figma 등. 파일 업로드 과정을 시각적으로 안내하여 사용자 불안 해소.',
components: [
{ name: '드롭존 (점선 테두리, 드래그 안내)', required: true },
{ name: '파일 선택 버튼', required: true },
{ name: '업로드 진행률 바', required: true },
{ name: '파일 목록 (이름 + 크기 + 상태)', required: true },
{ name: '개별 취소/삭제 버튼', required: true },
{ name: '미리보기 (이미지 썸네일)', required: false },
],
usedIn: ['Dropbox', 'Google Drive', 'WeTransfer', 'SAM 파일 첨부'],
guidelines: '드래그 시 드롭존 하이라이트. 파일 크기/형식 제한 사전 안내. 실패 시 재시도 버튼. 다중 파일 동시 업로드 지원.',
},
{
type: 'pattern', title: '브레드크럼 네비게이션', category: 'navigation', rating: 3,
tags: ['브레드크럼', '경로', '네비게이션', '계층'],
memo: 'AWS Console, Shopify Admin, Jira, SAM 등. 현재 위치를 계층적으로 표시하여 깊은 네비게이션에서 길을 잃지 않게.',
components: [
{ name: '경로 항목 (클릭 가능 링크)', required: true },
{ name: '구분자 (/ 또는 > 아이콘)', required: true },
{ name: '현재 페이지 (비링크, 볼드)', required: true },
{ name: '긴 경로 말줄임 (중간 생략)', required: false },
{ name: '드롭다운 (형제 페이지 선택)', required: false },
],
usedIn: ['AWS Console', 'Shopify', 'Jira', 'SAM 상세 페이지'],
guidelines: '페이지 상단 좌측 배치. 글꼴 크기 12~13px. 현재 페이지는 클릭 불가 (시각적 구분). 3단계 이상일 때 가장 유용.',
},
];
const cards = presets.map(p => ({
id: mkId(),
type: p.type,
title: p.title,
image: '',
memo: p.memo,
source: '',
tags: p.tags,
category: p.category,
rating: p.rating,
pinned: false,
archived: false,
components: p.components || [],
usedIn: p.usedIn || [],
guidelines: p.guidelines || '',
frequency: 0,
principles: {},
suggestion: '',
severity: 'info',
beforeImage: '',
afterImage: '',
changes: [],
effect: '',
createdAt: now,
updatedAt: now,
}));
if (!this.currentProject.cards) this.currentProject.cards = [];
this.currentProject.cards = [...cards, ...this.currentProject.cards];
this.saveProject();
this.categoryFilter = 'pattern';
this.toast('인기 UI 패턴 20종이 추가되었습니다');
},
};
}
</script>
@endpush