Files
sam-manage/resources/views/rd/design-insight/index.blade.php
김보곤 1543db684d feat: [design-insight] UI 패턴 50종 → 100종 확장
- 프리셋 템플릿 50개 추가 (51~100번)
- CSS 와이어프레임 50개 추가
- 버튼/토스트/다이얼로그 텍스트 100종으로 수정
2026-03-08 11:02:03 +09:00

6222 lines
440 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 패턴 100
</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 패턴 100 불러오기
</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>`;
// ===== 로그인/인증 =====
// 로그인 폼 (클래식)
if (key.includes('로그인') && (key.includes('폼') || key.includes('클래식'))) return `
<div style="width:100%;max-width:380px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:12px;padding:40px 30px;display:flex;justify-content:center;">
<div style="width:100%;max-width:300px;background:#fff;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.2);padding:24px;">
<div style="text-align:center;margin-bottom:20px;">
<div style="width:36px;height:36px;background:#3b82f6;border-radius:8px;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:14px;">S</div>
<div style="font-size:13px;font-weight:700;color:#1e293b;">SAM에 로그인</div>
</div>
<div style="margin-bottom:10px;">
<div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:3px;">이메일</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:0 8px;font-size:9px;line-height:28px;color:#94a3b8;">user@company.com</div>
</div>
<div style="margin-bottom:6px;">
<div style="display:flex;justify-content:space-between;margin-bottom:3px;">
<span style="font-size:9px;font-weight:600;color:#475569;">비밀번호</span>
<span style="font-size:8px;color:#3b82f6;">비밀번호 찾기</span>
</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;position:relative;"><span style="position:absolute;right:8px;top:6px;font-size:10px;color:#94a3b8;">👁</span></div>
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:12px;">
<div style="width:12px;height:12px;border:1px solid #cbd5e1;border-radius:3px;"></div>
<span style="font-size:8px;color:#64748b;">자동 로그인</span>
</div>
<div style="background:#3b82f6;color:#fff;text-align:center;padding:7px;border-radius:6px;font-size:10px;font-weight:600;margin-bottom:10px;">로그인</div>
<div style="text-align:center;font-size:8px;color:#94a3b8;">계정이 없으신가요? <span style="color:#3b82f6;">회원가입</span></div>
</div>
</div>`;
// 소셜 로그인 / SSO
if (key.includes('소셜') || key.includes('sso') || key.includes('oauth')) return `
<div style="width:100%;max-width:380px;background:#f8fafc;border-radius:12px;padding:40px 30px;display:flex;justify-content:center;">
<div style="width:100%;max-width:300px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:24px;">
<div style="text-align:center;margin-bottom:20px;">
<div style="font-size:13px;font-weight:700;color:#1e293b;margin-bottom:4px;">Welcome back</div>
<div style="font-size:9px;color:#94a3b8;">계속하려면 로그인하세요</div>
</div>
${[{icon:'🔵',name:'Google로 계속',bg:'#fff',border:'#e2e8f0',color:'#1e293b'},{icon:'⚫',name:'GitHub로 계속',bg:'#24292e',border:'#24292e',color:'#fff'},{icon:'🟣',name:'Slack으로 계속',bg:'#fff',border:'#e2e8f0',color:'#1e293b'}].map(s =>
`<div style="display:flex;align-items:center;justify-content:center;gap:8px;padding:7px;border:1px solid ${s.border};background:${s.bg};border-radius:6px;font-size:10px;color:${s.color};margin-bottom:6px;font-weight:500;">${s.icon} ${s.name}</div>`
).join('')}
<div style="display:flex;align-items:center;gap:8px;margin:12px 0;">
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
<span style="font-size:8px;color:#94a3b8;">또는</span>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:6px;padding:0 8px;font-size:9px;line-height:28px;color:#94a3b8;">이메일 주소</div>
<div style="background:#1e293b;color:#fff;text-align:center;padding:7px;border-radius:6px;font-size:10px;font-weight:500;">이메일로 계속</div>
<div style="text-align:center;font-size:7px;color:#94a3b8;margin-top:10px;">계속하면 <span style="color:#3b82f6;">이용약관</span> <span style="color:#3b82f6;">개인정보처리방침</span> 동의합니다.</div>
</div>
</div>`;
// 2단계 인증 (2FA)
if (key.includes('2fa') || key.includes('otp') || key.includes('인증코드') || (key.includes('2단계') && key.includes('인증'))) return `
<div style="width:100%;max-width:380px;background:#f8fafc;border-radius:12px;padding:40px 30px;display:flex;justify-content:center;">
<div style="width:100%;max-width:300px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:24px;text-align:center;">
<div style="width:48px;height:48px;background:#eff6ff;border-radius:50%;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:20px;">🔐</div>
<div style="font-size:13px;font-weight:700;color:#1e293b;margin-bottom:4px;">2단계 인증</div>
<div style="font-size:9px;color:#64748b;margin-bottom:16px;">인증 앱에서 6자리 코드를 입력하세요</div>
<div style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;">
${[3,8,2,7,4,1].map((n,i) => `<div style="width:32px;height:40px;background:#f8fafc;border:${i<4?'2px solid #3b82f6':'1px solid #e2e8f0'};border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#1e293b;">${i<4?n:''}</div>`).join('')}
</div>
<div style="background:#3b82f6;color:#fff;padding:7px;border-radius:6px;font-size:10px;font-weight:600;margin-bottom:10px;">확인</div>
<div style="font-size:8px;color:#94a3b8;">코드를 받지 못하셨나요? <span style="color:#3b82f6;">재전송 (48)</span></div>
<div style="font-size:8px;color:#3b82f6;margin-top:6px;">백업 코드로 인증 </div>
</div>
</div>`;
// 비밀번호 재설정
if (key.includes('비밀번호') && (key.includes('재설정') || key.includes('복구'))) return `
<div style="width:100%;max-width:380px;background:#f8fafc;border-radius:12px;padding:40px 30px;display:flex;justify-content:center;">
<div style="width:100%;max-width:300px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:24px;text-align:center;">
<div style="width:48px;height:48px;background:#fef3c7;border-radius:50%;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:20px;">✉️</div>
<div style="font-size:13px;font-weight:700;color:#1e293b;margin-bottom:4px;">비밀번호 재설정</div>
<div style="font-size:9px;color:#64748b;margin-bottom:16px;">가입한 이메일을 입력하시면<br>재설정 링크를 보내드립니다</div>
<div style="text-align:left;margin-bottom:10px;">
<div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:3px;">이메일 주소</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div>
</div>
<div style="background:#3b82f6;color:#fff;padding:7px;border-radius:6px;font-size:10px;font-weight:600;margin-bottom:10px;">재설정 링크 발송</div>
<div style="font-size:8px;color:#3b82f6;"> 로그인으로 돌아가기</div>
</div>
</div>`;
// 회원가입 폼
if (key.includes('회원가입') || key.includes('가입') && key.includes('폼')) return `
<div style="width:100%;max-width:380px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:12px;padding:40px 30px;display:flex;justify-content:center;">
<div style="width:100%;max-width:300px;background:#fff;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.2);padding:24px;">
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:13px;font-weight:700;color:#1e293b;">회원가입</div>
<div style="font-size:9px;color:#94a3b8;">무료로 시작하세요</div>
</div>
${['이름','이메일','비밀번호'].map(f => `<div style="margin-bottom:8px;"><div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:3px;">${f}</div><div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div></div>`).join('')}
<div style="margin-bottom:4px;">
<div style="display:flex;gap:3px;margin-top:2px;">
${['#ef4444','#f59e0b','#10b981','#e2e8f0'].map(c => `<div style="flex:1;height:3px;background:${c};border-radius:2px;"></div>`).join('')}
</div>
<div style="font-size:7px;color:#f59e0b;margin-top:2px;">보통 강도</div>
</div>
<div style="display:flex;align-items:flex-start;gap:4px;margin:8px 0 12px;">
<div style="width:12px;height:12px;border:1px solid #3b82f6;border-radius:3px;background:#eff6ff;flex-shrink:0;margin-top:1px;display:flex;align-items:center;justify-content:center;font-size:8px;color:#3b82f6;"></div>
<span style="font-size:7px;color:#64748b;"><span style="color:#3b82f6;">이용약관</span> <span style="color:#3b82f6;">개인정보처리방침</span> 동의합니다</span>
</div>
<div style="background:#3b82f6;color:#fff;text-align:center;padding:7px;border-radius:6px;font-size:10px;font-weight:600;margin-bottom:10px;">가입하기</div>
<div style="text-align:center;font-size:8px;color:#94a3b8;">이미 계정이 있으신가요? <span style="color:#3b82f6;">로그인</span></div>
</div>
</div>`;
// ===== 보고서/인쇄 =====
// 인쇄용 보고서
if (key.includes('인쇄') && key.includes('보고서')) return `
<div class="wf-wrap" style="padding:0;background:#fff;border:1px solid #cbd5e1;">
<div style="padding:16px 20px;border-bottom:2px solid #1e293b;display:flex;justify-content:space-between;align-items:flex-start;">
<div>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<div style="width:24px;height:24px;background:#3b82f6;border-radius:4px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:800;">S</div>
<span style="font-size:12px;font-weight:800;color:#1e293b;">()코드브릿지엑스</span>
</div>
<div style="font-size:8px;color:#94a3b8;">서울시 강남구 테헤란로 123</div>
</div>
<div style="text-align:right;">
<div style="font-size:14px;font-weight:800;color:#1e293b;margin-bottom:4px;">보고서</div>
<div style="font-size:8px;color:#64748b;">문서번호: RPT-2026-0308</div>
<div style="font-size:8px;color:#64748b;">작성일: 2026.03.08</div>
</div>
</div>
<div style="padding:12px 20px;">
<table style="width:100%;border-collapse:collapse;font-size:8px;">
<thead><tr style="background:#f1f5f9;">
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:left;color:#475569;">항목</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:center;color:#475569;">수량</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;color:#475569;">단가</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;color:#475569;">금액</th>
</tr></thead>
<tbody>${[['블라인드 50mm','120','15,000','1,800,000'],['롤스크린 전동','45','28,000','1,260,000'],['커튼레일 2m','80','8,500','680,000']].map((r,i) =>
`<tr style="background:${i%2?'#fafbfc':'#fff'};"><td style="padding:5px 8px;border:1px solid #e2e8f0;">${r[0]}</td><td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:center;">${r[1]}</td><td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">${r[2]}</td><td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">${r[3]}</td></tr>`
).join('')}</tbody>
</table>
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
<div style="width:150px;font-size:8px;">
<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid #f1f5f9;"><span style="color:#94a3b8;">소계</span><span>₩3,740,000</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid #f1f5f9;"><span style="color:#94a3b8;">부가세(10%)</span><span>₩374,000</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0;font-weight:800;font-size:10px;"><span>합계</span><span style="color:#3b82f6;">₩4,114,000</span></div>
</div>
</div>
</div>
<div style="padding:8px 20px;border-top:1px solid #e2e8f0;display:flex;justify-content:space-between;font-size:7px;color:#94a3b8;">
<span>인쇄일: 2026.03.08</span><span>Page 1 / 1</span>
</div>
</div>`;
// 인보이스/견적서
if (key.includes('인보이스') || key.includes('견적서')) return `
<div class="wf-wrap" style="padding:0;background:#fff;">
<div style="padding:16px 20px;display:flex;justify-content:space-between;">
<div>
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:2px;">발행</div>
<div style="font-size:10px;font-weight:700;color:#1e293b;">()코드브릿지엑스</div>
<div style="font-size:8px;color:#64748b;">서울시 강남구 테헤란로</div>
</div>
<div style="text-align:right;">
<div style="font-size:16px;font-weight:800;color:#3b82f6;margin-bottom:4px;"> </div>
<div style="font-size:8px;color:#64748b;">No. QT-2026-0042</div>
</div>
</div>
<div style="padding:0 20px;">
<div style="padding:10px 12px;background:#f8fafc;border-radius:6px;margin-bottom:12px;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:2px;">수신</div>
<div style="font-size:10px;font-weight:600;color:#1e293b;">ABC 산업()</div>
<div style="font-size:8px;color:#64748b;">경기도 화성시 동탄대로 45</div>
</div>
</div>
<div style="padding:0 20px 12px;">
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:0;font-size:8px;">
<div style="padding:5px 8px;background:#1e293b;color:#fff;font-weight:600;">품목</div>
<div style="padding:5px 8px;background:#1e293b;color:#fff;font-weight:600;text-align:center;">수량</div>
<div style="padding:5px 8px;background:#1e293b;color:#fff;font-weight:600;text-align:right;">단가</div>
<div style="padding:5px 8px;background:#1e293b;color:#fff;font-weight:600;text-align:right;">금액</div>
${[['블라인드 50mm','100','12,000','1,200,000'],['롤스크린 200','50','25,000','1,250,000']].map((r,i) =>
`<div style="padding:5px 8px;border-bottom:1px solid #f1f5f9;background:${i%2?'#fafbfc':'#fff'};">${r[0]}</div><div style="padding:5px 8px;border-bottom:1px solid #f1f5f9;text-align:center;background:${i%2?'#fafbfc':'#fff'};">${r[1]}</div><div style="padding:5px 8px;border-bottom:1px solid #f1f5f9;text-align:right;background:${i%2?'#fafbfc':'#fff'};">${r[2]}</div><div style="padding:5px 8px;border-bottom:1px solid #f1f5f9;text-align:right;background:${i%2?'#fafbfc':'#fff'};">${r[3]}</div>`
).join('')}
</div>
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
<div style="padding:8px 12px;background:#eff6ff;border-radius:6px;text-align:right;">
<div style="font-size:8px;color:#64748b;margin-bottom:2px;">합계 금액</div>
<div style="font-size:16px;font-weight:800;color:#3b82f6;">₩2,695,000</div>
</div>
</div>
</div>
</div>`;
// 데이터 분석 리포트
if (key.includes('분석') && key.includes('리포트')) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:11px;font-weight:700;color:#1e293b;">📊 분석 리포트</span>
<div style="display:flex;gap:4px;">
${['7일','30일','90일'].map((p,i) => `<span style="padding:3px 8px;border-radius:4px;font-size:8px;${i===1?'background:#3b82f6;color:#fff;':'background:#f1f5f9;color:#64748b;'}">${p}</span>`).join('')}
<span style="padding:3px 8px;border-radius:4px;font-size:8px;background:#f1f5f9;color:#64748b;">📥 PDF</span>
</div>
</div>
<div style="padding:12px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
${[{l:'방문자',v:'12,847',d:'+15.3%',c:'#10b981'},{l:'전환율',v:'3.24%',d:'+0.8%',c:'#10b981'},{l:'이탈률',v:'42.1%',d:'-2.1%',c:'#ef4444'}].map(k =>
`<div class="wf-box" style="padding:8px;text-align:center;"><div style="font-size:8px;color:#94a3b8;">${k.l}</div><div style="font-size:14px;font-weight:800;color:#1e293b;margin:2px 0;">${k.v}</div><div style="font-size:7px;color:${k.c};">${k.d}</div></div>`
).join('')}
</div>
<div style="padding:0 12px 12px;">
<div class="wf-box" style="padding:10px;height:80px;">
<div style="font-size:8px;font-weight:600;color:#475569;margin-bottom:6px;">일별 방문자 추이</div>
<svg width="100%" height="45" viewBox="0 0 300 45"><polyline points="10,35 40,28 70,32 100,15 130,20 160,12 190,18 220,8 250,14 280,5" fill="none" stroke="#3b82f6" stroke-width="2"/><polyline points="10,38 40,35 70,30 100,28 130,32 160,25 190,30 220,22 250,26 280,20" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="3,3"/></svg>
</div>
</div>
</div>`;
// 일일/주간 업무 보고서
if (key.includes('일일') || key.includes('주간') || key.includes('업무보고')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div><div style="font-size:12px;font-weight:700;color:#1e293b;">📋 주간 업무 보고서</div><div style="font-size:8px;color:#94a3b8;">2026.03.02 ~ 03.08 | 작성자: 홍길동</div></div>
</div>
<div style="margin-bottom:10px;">
<div style="font-size:9px;font-weight:600;color:#10b981;margin-bottom:6px;"> 완료 업무</div>
${['수주관리 API 개발 완료','거래처 목록 검색 기능 개선','견적서 PDF 출력 오류 수정'].map(t =>
`<div style="display:flex;align-items:center;gap:6px;padding:3px 0;font-size:9px;color:#64748b;"><span style="color:#10b981;">☑</span><span style="text-decoration:line-through;">${t}</span></div>`
).join('')}
</div>
<div style="margin-bottom:10px;">
<div style="font-size:9px;font-weight:600;color:#3b82f6;margin-bottom:6px;">🔄 진행 </div>
${[{t:'품목 마스터 마이그레이션',p:65},{t:'재고 관리 화면 설계',p:30}].map(i =>
`<div style="padding:3px 0;"><div style="display:flex;justify-content:space-between;font-size:9px;color:#475569;margin-bottom:2px;"><span>${i.t}</span><span style="color:#3b82f6;">${i.p}%</span></div><div style="height:4px;background:#f1f5f9;border-radius:2px;overflow:hidden;"><div style="height:100%;width:${i.p}%;background:#3b82f6;border-radius:2px;"></div></div></div>`
).join('')}
</div>
<div>
<div style="font-size:9px;font-weight:600;color:#f59e0b;margin-bottom:6px;">📌 예정 업무</div>
${['BOM 테이블 인라인 편집 구현','모바일 반응형 레이아웃 작업'].map(t =>
`<div style="display:flex;align-items:center;gap:6px;padding:3px 0;font-size:9px;color:#64748b;"><span style="color:#f59e0b;">☐</span>${t}</div>`
).join('')}
</div>
</div>`;
// 대시보드 PDF 리포트
if (key.includes('pdf') && (key.includes('대시보드') || key.includes('리포트'))) return `
<div class="wf-wrap" style="padding:0;background:#fff;">
<div style="background:#1e293b;padding:20px;text-align:center;">
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">MONTHLY REPORT</div>
<div style="font-size:14px;font-weight:800;color:#fff;margin-bottom:4px;">2026 3 월간 보고서</div>
<div style="font-size:9px;color:#94a3b8;">()코드브릿지엑스 | SAM</div>
</div>
<div style="padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<div class="wf-box" style="padding:10px;text-align:center;">
<div style="font-size:8px;color:#94a3b8;"> 매출</div>
<div style="font-size:14px;font-weight:800;color:#1e293b;">₩48.5M</div>
</div>
<div class="wf-box" style="padding:10px;text-align:center;">
<div style="font-size:8px;color:#94a3b8;">신규 수주</div>
<div style="font-size:14px;font-weight:800;color:#1e293b;">247</div>
</div>
</div>
<div style="padding:0 12px 12px;">
<div class="wf-box" style="padding:10px;height:60px;">
<div style="font-size:8px;font-weight:600;color:#475569;">월별 매출 차트</div>
<div style="display:flex;align-items:flex-end;gap:4px;height:35px;margin-top:4px;">
${[20,35,28,42,38,50,45].map(h => `<div style="flex:1;background:#bfdbfe;border-radius:2px 2px 0 0;height:${h}%;"></div>`).join('')}
</div>
</div>
</div>
<div style="padding:6px 12px;border-top:1px solid #e2e8f0;font-size:7px;color:#94a3b8;display:flex;justify-content:space-between;"><span>Confidential</span><span>Page 1 / 4</span></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:11px;font-weight:700;color:#1e293b;">🎛️ 대시보드</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:3px 8px;background:#eff6ff;color:#3b82f6;border-radius:4px;">+ 위젯 추가</span>
<span style="font-size:8px;color:#94a3b8;">⚙️</span>
</div>
<div style="padding:10px;display:grid;grid-template-columns:1fr 1fr 1fr;grid-template-rows:auto auto;gap:8px;">
<div class="wf-box" style="padding:8px;grid-column:span 2;position:relative;">
<div style="position:absolute;top:4px;right:4px;opacity:.4;font-size:8px;"> </div>
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">📈 매출 추이</div>
<svg width="100%" height="40" viewBox="0 0 200 40"><polyline points="10,30 50,20 90,25 130,10 170,15 190,5" fill="none" stroke="#3b82f6" stroke-width="2"/></svg>
</div>
<div class="wf-box" style="padding:8px;text-align:center;position:relative;">
<div style="position:absolute;top:4px;right:4px;opacity:.4;font-size:8px;"> </div>
<div style="font-size:8px;color:#94a3b8;">수주</div>
<div style="font-size:16px;font-weight:800;color:#3b82f6;">148</div>
</div>
<div class="wf-box" style="padding:8px;position:relative;">
<div style="position:absolute;top:4px;right:4px;opacity:.4;font-size:8px;"> </div>
<div style="font-size:8px;color:#94a3b8;"> </div>
${['미팅 준비','보고서 작성'].map(t => `<div style="font-size:8px;color:#475569;padding:2px 0;">☐ ${t}</div>`).join('')}
</div>
<div class="wf-box" style="padding:8px;grid-column:span 2;position:relative;">
<div style="position:absolute;top:4px;right:4px;opacity:.4;font-size:8px;"> </div>
<div style="font-size:8px;color:#94a3b8;">최근 활동</div>
${['수주 #1024 등록','거래처 정보 수정'].map(t => `<div style="font-size:8px;color:#475569;padding:2px 0;border-bottom:1px solid #f1f5f9;">${t}</div>`).join('')}
</div>
</div>
</div>`;
// 실시간 모니터링 대시보드
if (key.includes('실시간') && key.includes('모니터링')) return `
<div class="wf-wrap" style="padding:0;background:#0f172a;">
<div style="padding:8px 12px;border-bottom:1px solid #1e293b;display:flex;align-items:center;gap:8px;">
<span style="font-size:11px;font-weight:700;color:#e2e8f0;">🔴 실시간 모니터링</span>
<div style="flex:1;"></div>
<div style="width:6px;height:6px;background:#10b981;border-radius:50%;animation:pulse 2s infinite;"></div>
<span style="font-size:8px;color:#10b981;">LIVE</span>
<span style="font-size:8px;color:#64748b;">30s</span>
</div>
<div style="padding:10px;display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:6px;">
${[{n:'API',s:'정상',c:'#10b981',up:'99.9%'},{n:'DB',s:'정상',c:'#10b981',up:'99.8%'},{n:'Queue',s:'경고',c:'#f59e0b',up:'97.2%'},{n:'Storage',s:'정상',c:'#10b981',up:'99.5%'}].map(v =>
`<div style="background:#1e293b;border-radius:6px;padding:8px;text-align:center;border-left:2px solid ${v.c};"><div style="font-size:8px;color:#94a3b8;">${v.n}</div><div style="font-size:7px;color:${v.c};margin:2px 0;">${v.s}</div><div style="font-size:10px;font-weight:700;color:#e2e8f0;">${v.up}</div></div>`
).join('')}
</div>
<div style="padding:0 10px 10px;">
<div style="background:#1e293b;border-radius:6px;padding:8px;height:60px;">
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">CPU / Memory</div>
<svg width="100%" height="30" viewBox="0 0 200 30"><polyline points="10,20 30,18 50,15 70,22 90,12 110,8 130,14 150,10 170,16 190,12" fill="none" stroke="#10b981" stroke-width="1.5"/><polyline points="10,25 30,22 50,20 70,24 90,18 110,20 130,22 150,19 170,21 190,17" fill="none" stroke="#3b82f6" stroke-width="1.5"/></svg>
</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:11px;font-weight:700;color:#1e293b;">📊 분석 대시보드</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;border-radius:4px;color:#64748b;">이번 </span>
</div>
<div style="padding:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<div class="wf-box" style="padding:10px;height:90px;">
<div style="font-size:8px;font-weight:600;color:#475569;margin-bottom:4px;">매출 추이 (Line)</div>
<svg width="100%" height="55" viewBox="0 0 200 55"><polyline points="10,45 40,35 70,40 100,25 130,30 160,15 190,20" fill="none" stroke="#3b82f6" stroke-width="2"/><polyline points="10,48 40,42 70,38 100,35 130,32 160,28 190,22" fill="none" stroke="#10b981" stroke-width="1.5" stroke-dasharray="4,2"/></svg>
</div>
<div class="wf-box" style="padding:10px;height:90px;">
<div style="font-size:8px;font-weight:600;color:#475569;margin-bottom:4px;">카테고리별 (Bar)</div>
<div style="display:flex;align-items:flex-end;gap:6px;height:50px;">
${[{h:80,c:'#3b82f6'},{h:60,c:'#10b981'},{h:90,c:'#f59e0b'},{h:45,c:'#8b5cf6'},{h:70,c:'#ec4899'}].map(b =>
`<div style="flex:1;height:${b.h}%;background:${b.c};border-radius:2px 2px 0 0;opacity:.8;"></div>`
).join('')}
</div>
</div>
<div class="wf-box" style="padding:10px;height:90px;">
<div style="font-size:8px;font-weight:600;color:#475569;margin-bottom:4px;">비율 (Donut)</div>
<div style="display:flex;align-items:center;justify-content:center;height:55px;">
<svg width="60" height="60" viewBox="0 0 60 60"><circle cx="30" cy="30" r="20" fill="none" stroke="#e2e8f0" stroke-width="8"/><circle cx="30" cy="30" r="20" fill="none" stroke="#3b82f6" stroke-width="8" stroke-dasharray="75 125" stroke-dashoffset="0"/><circle cx="30" cy="30" r="20" fill="none" stroke="#10b981" stroke-width="8" stroke-dasharray="35 125" stroke-dashoffset="-75"/><text x="30" y="33" text-anchor="middle" font-size="8" font-weight="700" fill="#1e293b">60%</text></svg>
</div>
</div>
<div class="wf-box" style="padding:10px;height:90px;">
<div style="font-size:8px;font-weight:600;color:#475569;margin-bottom:4px;">Top 5</div>
${['블라인드 50mm|42%','롤스크린 전동|28%','커튼레일|15%','기타|15%'].map(r => {
const [n,p] = r.split('|');
return `<div style="display:flex;align-items:center;gap:4px;padding:2px 0;"><span style="font-size:7px;color:#475569;width:60px;">${n}</span><div style="flex:1;height:4px;background:#f1f5f9;border-radius:2px;"><div style="height:100%;width:${p};background:#3b82f6;border-radius:2px;"></div></div><span style="font-size:7px;color:#94a3b8;">${p}</span></div>`;
}).join('')}
</div>
</div>
</div>`;
// ===== 목록 추가 =====
// 무한 스크롤 피드
if (key.includes('무한스크롤') || key.includes('피드') && key.includes('소셜')) return `
<div class="wf-wrap" style="padding:12px;max-width:340px;margin:0 auto;">
${[{user:'김영업',time:'10분 전',text:'이번 분기 수주 현황 보고서를 공유합니다.',likes:5,comments:3},{user:'이관리',time:'2시간 전',text:'새로운 거래처 ABC산업과 계약 체결되었습니다! 🎉',likes:12,comments:8}].map(p =>
`<div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:10px;margin-bottom:8px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<div class="wf-circle" style="width:24px;height:24px;"></div>
<div><div style="font-size:9px;font-weight:600;color:#1e293b;">${p.user}</div><div style="font-size:7px;color:#94a3b8;">${p.time}</div></div>
</div>
<div style="font-size:9px;color:#475569;margin-bottom:8px;">${p.text}</div>
<div style="display:flex;gap:12px;font-size:8px;color:#94a3b8;border-top:1px solid #f1f5f9;padding-top:6px;">
<span>❤️ ${p.likes}</span><span>💬 ${p.comments}</span><span>🔗 공유</span>
</div>
</div>`
).join('')}
<div style="text-align:center;padding:12px;">
<div style="width:20px;height:20px;border:2px solid #e2e8f0;border-top-color:#3b82f6;border-radius:50%;margin:0 auto;"></div>
<div style="font-size:8px;color:#94a3b8;margin-top:4px;">불러오는 ...</div>
</div>
</div>`;
// 그룹/섹션 목록
if (key.includes('그룹') && (key.includes('섹션') || key.includes('목록'))) return `
<div class="wf-wrap" style="padding:0;">
${[{name:'영업관리',count:4,open:true,items:['수주관리','거래처관리','견적서','매출현황']},{name:'생산관리',count:3,open:true,items:['작업지시','BOM관리','재고현황']},{name:'회계관리',count:2,open:false,items:[]}].map(g =>
`<div>
<div style="padding:8px 12px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:6px;position:sticky;top:0;">
<span style="font-size:10px;color:#64748b;">${g.open?'▾':'▸'}</span>
<span style="font-size:10px;font-weight:600;color:#1e293b;">${g.name}</span>
<span style="font-size:8px;background:#e2e8f0;color:#64748b;padding:1px 6px;border-radius:8px;">${g.count}</span>
</div>
${g.open ? g.items.map(i => `<div style="padding:8px 12px 8px 28px;border-bottom:1px solid #f1f5f9;font-size:9px;color:#475569;display:flex;align-items:center;gap:6px;">
<span style="color:#94a3b8;">📄</span>${i}
</div>`).join('') : ''}
</div>`
).join('')}
</div>`;
// 벌크 액션 바
if (key.includes('벌크') || key.includes('일괄처리') || key.includes('다중선택')) return `
<div class="wf-wrap" style="padding:0;position:relative;">
<div style="padding:0 12px;">
${[{name:'수주 #1024',checked:true},{name:'수주 #1025',checked:true},{name:'수주 #1026',checked:false},{name:'수주 #1027',checked:true},{name:'수주 #1028',checked:false}].map(r =>
`<div style="display:flex;align-items:center;gap:8px;padding:7px 0;border-bottom:1px solid #f1f5f9;font-size:9px;color:#475569;">
<div style="width:14px;height:14px;border:1px solid ${r.checked?'#3b82f6':'#cbd5e1'};border-radius:3px;background:${r.checked?'#3b82f6':'#fff'};display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;">${r.checked?'✓':''}</div>
<span style="${r.checked?'background:#eff6ff;padding:0 4px;border-radius:2px;':''}">${r.name}</span>
<div style="flex:1;"></div>
<span style="font-size:8px;color:#94a3b8;">2026-03-08</span>
</div>`
).join('')}
</div>
<div style="position:absolute;bottom:0;left:0;right:0;background:#1e293b;border-radius:0 0 8px 8px;padding:8px 12px;display:flex;align-items:center;gap:8px;">
<span style="font-size:9px;color:#e2e8f0;font-weight:600;">3 선택</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">상태 변경</span>
<span style="font-size:8px;padding:3px 8px;background:#64748b;color:#fff;border-radius:4px;">이동</span>
<span style="font-size:8px;padding:3px 8px;background:#ef4444;color:#fff;border-radius:4px;">삭제</span>
<span style="font-size:9px;color:#94a3b8;cursor:pointer;"></span>
</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;font-weight:700;color:#1e293b;">BOM 품목 목록</span>
<div style="flex:1;"></div>
<span style="font-size:8px;color:#94a3b8;"> 클릭하여 편집</span>
</div>
<div style="padding:0 12px;font-size:8px;">
<div style="display:grid;grid-template-columns:30px 2fr 1fr 1fr 1fr;gap:0;padding:6px 0;border-bottom:1px solid #e2e8f0;font-weight:600;color:#94a3b8;">
<div>#</div><div>품명</div><div>수량</div><div>단위</div><div>비고</div>
</div>
<div style="display:grid;grid-template-columns:30px 2fr 1fr 1fr 1fr;gap:0;padding:6px 0;border-bottom:1px solid #f1f5f9;color:#475569;align-items:center;">
<div>1</div><div>블라인드 50mm</div><div>120</div><div>EA</div><div style="color:#94a3b8;">-</div>
</div>
<div style="display:grid;grid-template-columns:30px 2fr 1fr 1fr 1fr;gap:0;padding:4px 0;border-bottom:1px solid #f1f5f9;color:#475569;align-items:center;">
<div>2</div>
<div style="border:2px solid #3b82f6;border-radius:4px;padding:2px 6px;background:#eff6ff;">롤스크린 전동<span style="animation:blink 1s infinite;color:#3b82f6;">|</span></div>
<div>45</div><div>EA</div><div style="color:#94a3b8;">-</div>
</div>
<div style="display:grid;grid-template-columns:30px 2fr 1fr 1fr 1fr;gap:0;padding:6px 0;border-bottom:1px solid #f1f5f9;color:#475569;align-items:center;">
<div>3</div><div>커튼레일 2m</div><div>80</div><div>EA</div><div style="color:#94a3b8;">재고 확인 필요</div>
</div>
<div style="padding:6px 0;text-align:center;color:#94a3b8;border:1px dashed #e2e8f0;border-radius:4px;margin:6px 0;">+ 추가</div>
</div>
</div>`;
// 리치 텍스트 에디터
if (key.includes('리치') || key.includes('에디터') || key.includes('wysiwyg')) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:6px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:2px;flex-wrap:wrap;">
${['B','I','U','S','|','H1','H2','H3','|','≡','•','1.','|','🔗','📷','</>','|','↩','↪'].map(b =>
b === '|' ? `<div style="width:1px;height:16px;background:#e2e8f0;margin:0 4px;"></div>` :
`<div style="width:22px;height:22px;display:flex;align-items:center;justify-content:center;border-radius:3px;font-size:9px;color:#64748b;cursor:pointer;${b==='B'?'font-weight:800;background:#f1f5f9;color:#1e293b;':''}">${b}</div>`
).join('')}
</div>
<div style="padding:12px;min-height:140px;">
<div style="font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;">프로젝트 개요</div>
<div style="font-size:9px;color:#475569;line-height:1.6;margin-bottom:8px;">SAM 프로젝트는 <b style="background:#fef3c7;">블라인드/스크린 제조업체</b> 위한 차세대 ERP/MES 통합 시스템입니다.</div>
<div style="font-size:11px;font-weight:600;color:#1e293b;margin-bottom:6px;">핵심 기능</div>
<div style="font-size:9px;color:#475569;padding-left:12px;">
<div style="margin-bottom:2px;"> 수주/견적 관리</div>
<div style="margin-bottom:2px;"> 생산/품질 관리</div>
<div> 재무/회계 관리</div>
</div>
</div>
</div>`;
// 상세 정보 카드 (프로필)
if (key.includes('프로필') || key.includes('정보카드') && key.includes('상세')) return `
<div class="wf-wrap" style="padding:0;">
<div style="height:40px;background:linear-gradient(135deg,#3b82f6,#6366f1);"></div>
<div style="padding:0 16px 16px;margin-top:-20px;">
<div style="display:flex;align-items:flex-end;gap:10px;margin-bottom:10px;">
<div style="width:48px;height:48px;background:#fff;border-radius:50%;border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.1);display:flex;align-items:center;justify-content:center;font-size:18px;">👤</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:700;color:#1e293b;">홍길동</div>
<div style="font-size:9px;color:#64748b;">영업팀 팀장</div>
</div>
<div style="display:flex;gap:4px;">
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">메시지</span>
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">편집</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px;">
${[{l:'이메일',v:'hong@sam.co.kr'},{l:'전화',v:'010-1234-5678'},{l:'부서',v:'영업1팀'},{l:'상태',v:'근무 중',badge:true}].map(i =>
`<div class="wf-box" style="padding:6px 8px;"><div style="font-size:7px;color:#94a3b8;">${i.l}</div><div style="font-size:9px;color:#1e293b;${i.badge?'':''}margin-top:1px;">${i.badge?`<span style="display:inline-block;width:5px;height:5px;background:#10b981;border-radius:50%;margin-right:3px;"></span>`:''}${i.v}</div></div>`
).join('')}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;">
${[{l:'거래건수',v:'247'},{l:'수주액',v:'₩48.5M'},{l:'달성률',v:'112%'}].map(s =>
`<div style="text-align:center;padding:6px;"><div style="font-size:14px;font-weight:800;color:#1e293b;">${s.v}</div><div style="font-size:7px;color:#94a3b8;">${s.l}</div></div>`
).join('')}
</div>
</div>
</div>`;
// ===== 모달/팝업 추가 =====
// 확인/경고 다이얼로그
if (key.includes('확인') && (key.includes('경고') || key.includes('다이얼로그') || 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%;max-width:320px;background:#fff;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.3);overflow:hidden;">
<div style="padding:20px;text-align:center;">
<div style="width:48px;height:48px;background:#fef2f2;border-radius:50%;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:22px;">⚠️</div>
<div style="font-size:13px;font-weight:700;color:#1e293b;margin-bottom:6px;">정말 삭제하시겠습니까?</div>
<div style="font-size:9px;color:#64748b;line-height:1.6;"> 작업은 되돌릴 없습니다.<br>관련된 모든 데이터가 영구적으로 삭제됩니다.</div>
<div style="margin-top:12px;text-align:left;">
<div style="font-size:8px;color:#ef4444;font-weight:600;margin-bottom:4px;">확인하려면 "수주 #1024" 입력하세요:</div>
<div style="height:28px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:0 8px;font-size:9px;line-height:28px;color:#94a3b8;">수주 #10...</div>
</div>
</div>
<div style="padding:12px 20px;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:#ef4444;opacity:.5;">삭제</div>
</div>
</div>
</div>`;
// 이미지 라이트박스
if (key.includes('라이트박스') || key.includes('갤러리') && key.includes('이미지')) return `
<div style="width:100%;background:#000;border-radius:12px;padding:20px;position:relative;min-height:240px;display:flex;align-items:center;justify-content:center;">
<div style="position:absolute;top:12px;right:12px;display:flex;gap:6px;">
<span style="font-size:10px;color:#fff;opacity:.8;">🔍+</span>
<span style="font-size:10px;color:#fff;opacity:.8;">🔍-</span>
<span style="font-size:14px;color:#fff;opacity:.8;cursor:pointer;"></span>
</div>
<div style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:28px;height:28px;background:rgba(255,255,255,.15);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;cursor:pointer;"></div>
<div style="position:absolute;right:12px;top:50%;transform:translateY(-50%);width:28px;height:28px;background:rgba(255,255,255,.15);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;cursor:pointer;"></div>
<div style="width:180px;height:130px;background:#1e293b;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:10px;">📸 이미지</div>
<div style="position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:8px;">
<span style="font-size:9px;color:#94a3b8;">3 / 12</span>
</div>
<div style="position:absolute;bottom:12px;left:50%;transform:translateX(-50%);margin-top:20px;display:flex;gap:4px;">
${Array.from({length:5},(_, i) => `<div style="width:28px;height:20px;background:${i===2?'#fff':'#333'};border-radius:3px;border:${i===2?'1px solid #3b82f6':'1px solid #444'};opacity:${i===2?1:.6};"></div>`).join('')}
</div>
</div>`;
// 알림 센터 패널
if (key.includes('알림') && (key.includes('센터') || key.includes('패널') || key.includes('노티'))) return `
<div style="width:100%;max-width:360px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.1);overflow:hidden;border:1px solid #e2e8f0;">
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:11px;font-weight:700;color:#1e293b;">🔔 알림</span>
<span style="font-size:8px;color:#3b82f6;">모두 읽음</span>
</div>
<div style="font-size:7px;color:#94a3b8;padding:6px 14px;font-weight:600;background:#f8fafc;">오늘</div>
${[{icon:'📋',title:'수주 #1024 승인됨',desc:'김영업님이 수주를 승인했습니다.',time:'10분 전',unread:true},{icon:'💬',title:'새 댓글',desc:'이관리: "확인했습니다"',time:'1시간 전',unread:true},{icon:'⚠️',title:'재고 부족 알림',desc:'블라인드 50mm 재고가 10개 미만입니다.',time:'3시간 전',unread:false}].map(n =>
`<div style="padding:8px 14px;border-bottom:1px solid #f1f5f9;display:flex;gap:8px;${n.unread?'background:#f8fbff;':''}">
<span style="font-size:14px;flex-shrink:0;">${n.icon}</span>
<div style="flex:1;min-width:0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px;">
<span style="font-size:9px;font-weight:${n.unread?'700':'500'};color:#1e293b;">${n.title}</span>
${n.unread?'<div style="width:6px;height:6px;background:#3b82f6;border-radius:50%;flex-shrink:0;"></div>':''}
</div>
<div style="font-size:8px;color:#64748b;margin-bottom:2px;">${n.desc}</div>
<div style="font-size:7px;color:#94a3b8;">${n.time}</div>
</div>
</div>`
).join('')}
<div style="font-size:7px;color:#94a3b8;padding:6px 14px;font-weight:600;background:#f8fafc;">어제</div>
<div style="padding:8px 14px;display:flex;gap:8px;">
<span style="font-size:14px;"></span>
<div><div style="font-size:9px;color:#475569;">견적서 #502 발송 완료</div><div style="font-size:7px;color:#94a3b8;">어제 오후 3:24</div></div>
</div>
</div>`;
// 날짜/기간 선택기
if (key.includes('날짜') && (key.includes('선택') || key.includes('피커'))) return `
<div style="width:100%;max-width:320px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.1);overflow:hidden;border:1px solid #e2e8f0;">
<div style="padding:10px 14px;display:flex;align-items:center;gap:8px;">
<div style="display:flex;gap:4px;">
${['오늘','이번 주','이번 달','직접 선택'].map((p,i) => `<span style="padding:3px 6px;border-radius:4px;font-size:7px;${i===3?'background:#3b82f6;color:#fff;':'background:#f1f5f9;color:#64748b;'}">${p}</span>`).join('')}
</div>
</div>
<div style="padding:0 14px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom: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>
<div style="display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-size:8px;">
${['일','월','화','수','목','금','토'].map(d => `<div style="padding:3px;color:#94a3b8;font-weight:600;">${d}</div>`).join('')}
${Array.from({length:35},(_, i) => {
const d = i - 6;
const inRange = d >= 5 && d <= 12;
const isStart = d === 5;
const isEnd = d === 12;
return `<div style="padding:3px;${d<1||d>31?'color:#cbd5e1;':'color:#475569;'}${inRange?`background:${isStart||isEnd?'#3b82f6':'#eff6ff'};color:${isStart||isEnd?'#fff':'#3b82f6'};${isStart?'border-radius:50% 0 0 50%;':''}${isEnd?'border-radius:0 50% 50% 0;':''}`:''}">${d<1?'':d>31?'':d}</div>`;
}).join('')}
</div>
</div>
<div style="padding:10px 14px;border-top:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:8px;color:#64748b;">3 5 ~ 3 12</span>
<div style="display:flex;gap:4px;">
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;border-radius:4px;color:#64748b;">취소</span>
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;border-radius:4px;color:#fff;">적용</span>
</div>
</div>
</div>`;
// ===== 네비게이션 추가 =====
// 메가 메뉴
if (key.includes('메가') && key.includes('메뉴')) return `
<div class="wf-wrap" style="padding:0;">
<div style="background:#1e293b;padding:8px 16px;display:flex;align-items:center;gap:16px;">
<span style="font-size:12px;font-weight:800;color:#fff;">SAM</span>
${['제품 ▾','솔루션 ▾','고객지원','문서'].map((m,i) => `<span style="font-size:9px;color:${i===0?'#fff':'#94a3b8'};font-weight:${i===0?'600':'400'};padding:4px 0;${i===0?'border-bottom:2px solid #3b82f6;':''}">${m}</span>`).join('')}
<div style="flex:1;"></div>
<span style="font-size:9px;color:#94a3b8;">로그인</span>
</div>
<div style="background:#fff;border-bottom:2px solid #e2e8f0;padding:16px;display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:16px;">
${[{cat:'ERP',items:['수주관리','견적서','거래처']},{cat:'MES',items:['작업지시','품질관리','재고']},{cat:'회계',items:['매출분석','세금관리','결산']},{cat:'인사',items:['근태관리','급여','조직도']}].map(col =>
`<div>
<div style="font-size:9px;font-weight:700;color:#3b82f6;margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid #eff6ff;">${col.cat}</div>
${col.items.map(i => `<div style="font-size:9px;color:#475569;padding:3px 0;">${i}</div>`).join('')}
</div>`
).join('')}
</div>
</div>`;
// 모바일 하단 네비게이션
if (key.includes('모바일') && (key.includes('하단') || key.includes('탭바'))) return `
<div style="width:100%;max-width:320px;background:#f8fafc;border-radius:20px;overflow:hidden;border:6px solid #1e293b;margin:0 auto;">
<div style="padding:12px;height:200px;display:flex;flex-direction:column;">
<div class="wf-bar xl dark" style="height:10px;margin-bottom:8px;"></div>
<div class="wf-bar lg" style="height:8px;margin-bottom:16px;"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;flex:1;">
<div class="wf-box" style="padding:8px;"><div class="wf-text">콘텐츠</div></div>
<div class="wf-box" style="padding:8px;"><div class="wf-text">콘텐츠</div></div>
</div>
</div>
<div style="background:#fff;border-top:1px solid #e2e8f0;padding:6px 0 12px;display:flex;justify-content:space-around;align-items:center;">
${[{icon:'🏠',label:'홈',active:true},{icon:'📋',label:'수주',active:false},{icon:'',label:'',fab:true},{icon:'📊',label:'분석',active:false,badge:3},{icon:'👤',label:'MY',active:false}].map(i =>
i.fab ? `<div style="width:40px;height:40px;background:#3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;margin-top:-16px;box-shadow:0 4px 12px rgba(59,130,246,.3);">+</div>` :
`<div style="text-align:center;position:relative;">
<div style="font-size:16px;${i.active?'':'opacity:.5;'}">${i.icon}</div>
<div style="font-size:7px;color:${i.active?'#3b82f6':'#94a3b8'};font-weight:${i.active?'600':'400'};">${i.label}</div>
${i.badge?`<div style="position:absolute;top:-2px;right:-4px;width:14px;height:14px;background:#ef4444;border-radius:50%;font-size:7px;color:#fff;display:flex;align-items:center;justify-content:center;">${i.badge}</div>`:''}
</div>`
).join('')}
</div>
</div>`;
// 다단계 드롭다운
if (key.includes('드롭다운') && (key.includes('다단계') || key.includes('계층'))) return `
<div style="width:100%;max-width:400px;padding:20px;">
<div style="display:flex;gap:4px;">
<div style="width:160px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.1);overflow:hidden;">
${[{t:'새로 만들기',icon:'',sub:false},{t:'파일 열기',icon:'📂',sub:false,key:'⌘O'},{t:'최근 파일',icon:'🕐',sub:true},{t:'',sep:true},{t:'설정',icon:'⚙️',sub:true,active:true},{t:'',sep:true},{t:'로그아웃',icon:'🚪',sub:false,danger:true}].map(i =>
i.sep ? `<div style="height:1px;background:#e2e8f0;margin:2px 0;"></div>` :
`<div style="display:flex;align-items:center;gap:6px;padding:5px 10px;font-size:9px;color:${i.danger?'#ef4444':'#475569'};${i.active?'background:#eff6ff;color:#3b82f6;':''}">
<span style="width:14px;text-align:center;">${i.icon}</span>
<span style="flex:1;${i.active?'font-weight:600;':''}">${i.t}</span>
${i.key?`<span style="font-size:7px;color:#94a3b8;">${i.key}</span>`:''}
${i.sub?`<span style="font-size:8px;color:#94a3b8;"></span>`:''}
</div>`
).join('')}
</div>
<div style="width:140px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.1);overflow:hidden;margin-top:68px;">
${['일반','알림','보안','테마'].map((s,i) =>
`<div style="display:flex;align-items:center;gap:6px;padding:5px 10px;font-size:9px;color:${i===0?'#3b82f6':'#475569'};${i===0?'background:#eff6ff;':''}">
${i===0?'<span style="font-size:8px;">✓</span>':'<span style="width:10px;"></span>'}<span>${s}</span>
</div>`
).join('')}
</div>
</div>
</div>`;
// ===== 기타 추가 =====
// 드래그 앤 드롭 정렬
if (key.includes('드래그') && (key.includes('정렬') || key.includes('순서'))) return `
<div class="wf-wrap" style="padding:12px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">📋 메뉴 순서 관리</div>
${[{n:'대시보드',dragging:false},{n:'수주관리',dragging:true},{n:'',drop:true},{n:'거래처관리',dragging:false},{n:'품목관리',dragging:false},{n:'견적서관리',dragging:false}].map(i =>
i.drop ? `<div style="height:2px;background:#3b82f6;border-radius:1px;margin:2px 0;box-shadow:0 0 4px rgba(59,130,246,.4);"></div>` :
`<div style="display:flex;align-items:center;gap:8px;padding:7px 10px;background:${i.dragging?'#eff6ff':'#fff'};border:1px solid ${i.dragging?'#3b82f6':'#e2e8f0'};border-radius:6px;margin-bottom:4px;${i.dragging?'opacity:.7;box-shadow:0 4px 12px rgba(59,130,246,.2);transform:rotate(1deg);':''}">
<span style="color:#94a3b8;font-size:12px;cursor:grab;">⠿</span>
<span style="font-size:9px;color:#475569;">${i.n}</span>
<div style="flex:1;"></div>
<span style="font-size:7px;color:#94a3b8;">↕</span>
</div>`
).join('')}
</div>`;
// 스켈레톤 로딩
if (key.includes('스켈레톤') || key.includes('플레이스홀더') && key.includes('로딩')) return `
<div class="wf-wrap" style="padding:12px;">
<style>@keyframes skeletonWave{0%{background-position:-200px 0}100%{background-position:calc(200px + 100%) 0}}</style>
${Array.from({length:3},(_, i) =>
`<div style="display:flex;gap:10px;margin-bottom:12px;${i>0?'opacity:'+(1-i*0.2)+';':''}">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;flex-shrink:0;"></div>
<div style="flex:1;">
<div style="height:10px;border-radius:4px;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;margin-bottom:6px;width:${60+i*15}%;"></div>
<div style="height:8px;border-radius:4px;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;width:${80+i*5}%;margin-bottom:4px;"></div>
<div style="height:8px;border-radius:4px;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;width:${40+i*10}%;"></div>
</div>
</div>`
).join('')}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
${Array.from({length:2},(_, i) =>
`<div style="border-radius:6px;overflow:hidden;border:1px solid #f1f5f9;">
<div style="height:60px;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;"></div>
<div style="padding:8px;"><div style="height:8px;border-radius:4px;background:linear-gradient(90deg,#f1f5f9 25%,#e2e8f0 50%,#f1f5f9 75%);background-size:200px 100%;animation:skeletonWave 1.5s infinite;width:70%;"></div></div>
</div>`
).join('')}
</div>
</div>`;
// 알림 배지 시스템
if (key.includes('배지') && (key.includes('카운터') || key.includes('인디케이터'))) return `
<div class="wf-wrap" style="padding:20px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:16px;">배지 유형</div>
<div style="display:flex;flex-wrap:wrap;gap:20px;margin-bottom:20px;">
${[{icon:'🔔',badge:'3',type:'숫자 배지'},{icon:'💬',badge:'99+',type:'큰 숫자'},{icon:'📧',badge:'',dot:true,type:'점 배지'},{icon:'📋',badge:'NEW',text:true,type:'텍스트 배지'}].map(b =>
`<div style="text-align:center;">
<div style="position:relative;display:inline-block;font-size:24px;padding:4px;">
${b.icon}
${b.dot?`<div style="position:absolute;top:2px;right:2px;width:8px;height:8px;background:#ef4444;border-radius:50%;border:2px solid #fff;"></div>`:
b.text?`<div style="position:absolute;top:-2px;right:-12px;background:#10b981;color:#fff;font-size:6px;padding:1px 4px;border-radius:4px;font-weight:600;">${b.badge}</div>`:
b.badge?`<div style="position:absolute;top:-2px;right:-4px;min-width:16px;height:16px;background:#ef4444;color:#fff;font-size:8px;border-radius:8px;display:flex;align-items:center;justify-content:center;border:2px solid #fff;padding:0 3px;">${b.badge}</div>`:''}
</div>
<div style="font-size:7px;color:#94a3b8;margin-top:4px;">${b.type}</div>
</div>`
).join('')}
</div>
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">사이드바 메뉴 적용 예시</div>
<div style="background:#1e293b;border-radius:6px;padding:8px;width:160px;">
${[{m:'📊 대시보드',badge:''},{m:'📋 수주관리',badge:'5'},{m:'💬 메시지',badge:'12'},{m:'🔔 알림',badge:'',dot:true}].map(i =>
`<div style="display:flex;align-items:center;gap:6px;padding:4px 6px;font-size:9px;color:#94a3b8;">
<span>${i.m}</span><div style="flex:1;"></div>
${i.dot?`<div style="width:6px;height:6px;background:#ef4444;border-radius:50%;"></div>`:
i.badge?`<span style="background:#ef4444;color:#fff;font-size:7px;padding:1px 5px;border-radius:8px;">${i.badge}</span>`:''}
</div>`
).join('')}
</div>
</div>`;
// 데이터 시각화 차트 컴포넌트
if (key.includes('시각화') && key.includes('차트')) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<div><span style="font-size:11px;font-weight:700;color:#1e293b;">월별 매출 현황</span><span style="font-size:8px;color:#94a3b8;margin-left:6px;">2026</span></div>
<div style="display:flex;gap:4px;">
${['라인','바','파이'].map((t,i) => `<span style="font-size:8px;padding:2px 6px;border-radius:3px;${i===0?'background:#3b82f6;color:#fff;':'background:#f1f5f9;color:#64748b;'}">${t}</span>`).join('')}
</div>
</div>
<div style="padding:12px;">
<div style="display:flex;gap:8px;margin-bottom:8px;">
<span style="font-size:7px;color:#3b82f6;"> 매출</span>
<span style="font-size:7px;color:#10b981;"> 이익</span>
</div>
<div style="position:relative;height:100px;">
<div style="position:absolute;left:0;top:0;bottom:20px;width:30px;display:flex;flex-direction:column;justify-content:space-between;font-size:7px;color:#94a3b8;text-align:right;">
<span>50M</span><span>25M</span><span>0</span>
</div>
<div style="margin-left:35px;height:80px;position:relative;">
<svg width="100%" height="80" viewBox="0 0 280 80" preserveAspectRatio="none">
<line x1="0" y1="20" x2="280" y2="20" stroke="#f1f5f9" stroke-width="1"/>
<line x1="0" y1="40" x2="280" y2="40" stroke="#f1f5f9" stroke-width="1"/>
<line x1="0" y1="60" x2="280" y2="60" stroke="#f1f5f9" stroke-width="1"/>
<polyline points="20,50 60,40 100,45 140,25 180,30 220,15 260,20" fill="none" stroke="#3b82f6" stroke-width="2"/>
<polyline points="20,60 60,52 100,55 140,40 180,45 220,30 260,35" fill="none" stroke="#10b981" stroke-width="2"/>
<circle cx="220" cy="15" r="4" fill="#3b82f6"/>
<rect x="195" y="-2" width="50" height="14" rx="3" fill="#1e293b"/>
<text x="220" y="8" text-anchor="middle" font-size="7" fill="#fff">₩48.5M</text>
</svg>
</div>
<div style="margin-left:35px;display:flex;justify-content:space-between;font-size:7px;color:#94a3b8;margin-top:2px;">
${['1월','2월','3월','4월','5월','6월','7월'].map(m => `<span>${m}</span>`).join('')}
</div>
</div>
</div>
</div>`;
// ===== 추가 50종 와이어프레임 (51~100) =====
// 게이지/미터
if (key.includes('게이지') || key.includes('미터') && key.includes('대시보드')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
${[{l:'CPU 사용률',v:'73%',c:'#f59e0b',pct:73},{l:'메모리',v:'45%',c:'#10b981',pct:45},{l:'달성률',v:'92%',c:'#ef4444',pct:92}].map(g => `
<div style="text-align:center;">
<svg width="80" height="50" viewBox="0 0 80 50">
<path d="M10,45 A30,30 0 0,1 70,45" fill="none" stroke="#f1f5f9" stroke-width="6" stroke-linecap="round"/>
<path d="M10,45 A30,30 0 0,1 70,45" fill="none" stroke="${g.c}" stroke-width="6" stroke-linecap="round" stroke-dasharray="${g.pct*0.94} 100"/>
</svg>
<div style="font-size:14px;font-weight:800;color:#1e293b;margin-top:-8px;">${g.v}</div>
<div style="font-size:8px;color:#94a3b8;margin-top:2px;">${g.l}</div>
</div>
`).join('')}
</div>
</div>`;
// 히트맵 캘린더
if (key.includes('히트맵') || key.includes('잔디') && key.includes('캘린더')) return `
<div class="wf-wrap" style="padding:12px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">활동 기록</div>
<div style="display:flex;gap:2px;">
<div style="display:flex;flex-direction:column;gap:2px;font-size:6px;color:#94a3b8;padding-right:4px;justify-content:space-between;">
<span></span><span></span><span></span>
</div>
<div style="display:flex;gap:2px;flex-wrap:nowrap;overflow:hidden;flex:1;">
${Array.from({length:26},(_, w) => `<div style="display:flex;flex-direction:column;gap:2px;">${Array.from({length:7},(_, d) => {
const r = Math.random();
const colors = ['#f1f5f9','#dcfce7','#86efac','#22c55e','#15803d'];
const ci = r > 0.7 ? (r > 0.9 ? 4 : 3) : (r > 0.4 ? (r > 0.55 ? 2 : 1) : 0);
return `<div style="width:8px;height:8px;background:${colors[ci]};border-radius:1px;"></div>`;
}).join('')}</div>`).join('')}
</div>
</div>
<div style="display:flex;align-items:center;gap:4px;justify-content:flex-end;margin-top:6px;">
<span style="font-size:7px;color:#94a3b8;">적음</span>
${['#f1f5f9','#dcfce7','#86efac','#22c55e','#15803d'].map(c => `<div style="width:8px;height:8px;background:${c};border-radius:1px;"></div>`).join('')}
<span style="font-size:7px;color:#94a3b8;">많음</span>
</div>
</div>`;
// 퍼널/전환율
if (key.includes('퍼널') || key.includes('전환율')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">수주 파이프라인</div>
${[{s:'리드',v:'1,240',w:100,c:'#bfdbfe'},{s:'미팅',v:'680',w:82,c:'#93c5fd',r:'54.8%'},{s:'제안',v:'340',w:64,c:'#60a5fa',r:'50.0%'},{s:'협상',v:'180',w:46,c:'#3b82f6',r:'52.9%'},{s:'수주',v:'95',w:28,c:'#1d4ed8',r:'52.8%'}].map(f => `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
<span style="font-size:8px;color:#64748b;width:28px;">${f.s}</span>
<div style="width:${f.w}%;height:22px;background:${f.c};border-radius:4px;display:flex;align-items:center;padding:0 8px;">
<span style="font-size:9px;font-weight:600;color:#1e293b;">${f.v}</span>
</div>
${f.r?`<span style="font-size:7px;color:#10b981;"> ${f.r}</span>`:''}
</div>
`).join('')}
</div>`;
// 비교 대시보드
if (key.includes('비교') && (key.includes('전월') || 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;font-weight:700;color:#1e293b;">기간 비교</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:2px 6px;background:#3b82f6;color:#fff;border-radius:3px;">3</span>
<span style="font-size:8px;color:#94a3b8;">vs</span>
<span style="font-size:8px;padding:2px 6px;background:#f1f5f9;color:#64748b;border-radius:3px;">2</span>
</div>
<div style="padding:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">
${[{l:'매출',a:'₩48.5M',b:'₩42.1M',d:'+15.2%',up:true},{l:'수주',a:'247건',b:'198건',d:'+24.7%',up:true},{l:'미수금',a:'₩3.2M',b:'₩2.8M',d:'+14.3%',up:false},{l:'거래처',a:'52개',b:'49개',d:'+6.1%',up:true}].map(k =>
`<div class="wf-box" style="padding:8px;">
<div style="font-size:7px;color:#94a3b8;margin-bottom:4px;">${k.l}</div>
<div style="display:flex;justify-content:space-between;align-items:baseline;">
<span style="font-size:12px;font-weight:800;color:#1e293b;">${k.a}</span>
<span style="font-size:8px;color:#94a3b8;">${k.b}</span>
</div>
<div style="font-size:7px;color:${k.up?'#10b981':'#ef4444'};margin-top:2px;">${k.up?'▲':'▲'} ${k.d}</div>
</div>`
).join('')}
</div>
</div>`;
// 지도 기반
if (key.includes('지도') && (key.includes('데이터') || key.includes('시각화') || key.includes('분포'))) return `
<div class="wf-wrap" style="padding:0;background:#e8f4f8;min-height:200px;position:relative;">
<div style="position:absolute;top:8px;left:8px;background:#fff;border-radius:6px;padding:6px 8px;box-shadow:0 2px 8px rgba(0,0,0,.1);z-index:2;">
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">거래처 분포</div>
${['서울 28','경기 15','부산 8'].map(r => `<div style="font-size:7px;color:#64748b;padding:1px 0;">${r}개</div>`).join('')}
</div>
<div style="position:absolute;top:8px;right:8px;display:flex;flex-direction:column;gap:2px;z-index:2;">
<div style="width:24px;height:24px;background:#fff;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:0 1px 4px rgba(0,0,0,.1);">+</div>
<div style="width:24px;height:24px;background:#fff;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
</div>
${[{x:35,y:30,s:20,c:'#3b82f6'},{x:40,y:35,s:14,c:'#3b82f6'},{x:55,y:70,s:10,c:'#3b82f6'},{x:25,y:55,s:8,c:'#3b82f6'},{x:60,y:45,s:6,c:'#3b82f6'}].map(m =>
`<div style="position:absolute;left:${m.x}%;top:${m.y}%;width:${m.s}px;height:${m.s}px;background:${m.c};opacity:.4;border-radius:50%;border:2px solid ${m.c};"></div>`
).join('')}
<div style="position:absolute;left:35%;top:28%;font-size:8px;color:#1e293b;font-weight:600;background:rgba(255,255,255,.8);padding:1px 4px;border-radius:2px;">서울</div>
</div>`;
// 마스터-디테일
if (key.includes('마스터') && key.includes('디테일')) return `
<div class="wf-wrap" style="display:flex;padding:0;min-height:220px;">
<div style="width:160px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;">
<div style="padding:8px;border-bottom:1px solid #e2e8f0;">
<div style="height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 8px;font-size:8px;line-height:24px;color:#94a3b8;">🔍 검색...</div>
</div>
${['수주 #1024|ABC산업|true','수주 #1023|XYZ전자|false','수주 #1022|한국물산|false','수주 #1021|글로벌|false'].map(r => {
const [t,c,a] = r.split('|');
return `<div style="padding:6px 8px;border-bottom:1px solid #f1f5f9;${a==='true'?'background:#eff6ff;border-left:2px solid #3b82f6;':''}">
<div style="font-size:9px;font-weight:${a==='true'?'600':'400'};color:#1e293b;">${t}</div>
<div style="font-size:7px;color:#94a3b8;">${c}</div>
</div>`;
}).join('')}
</div>
<div style="flex:1;padding:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:700;color:#1e293b;">수주 #1024</div>
<div style="display:flex;gap:4px;">
<span style="font-size:8px;padding:2px 8px;background:#3b82f6;color:#fff;border-radius:4px;">편집</span>
<span style="font-size:8px;padding:2px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">인쇄</span>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:8px;">
${['거래처|ABC산업(주)','금액|₩12,500,000','납기일|2026-04-15','상태|진행 중'].map(f => {
const [k,v] = f.split('|');
return `<div class="wf-box" style="padding:6px 8px;"><span style="color:#94a3b8;">${k}</span><div style="color:#1e293b;font-weight:500;margin-top:2px;">${v}</div></div>`;
}).join('')}
</div>
</div>
</div>`;
// 타일/썸네일 뷰
if (key.includes('타일') || 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;font-weight:700;color:#1e293b;">이미지 라이브러리</span>
<div style="flex:1;"></div>
<span style="font-size:10px;color:#3b82f6;"></span>
<span style="font-size:10px;color:#94a3b8;"></span>
<span style="font-size:8px;color:#94a3b8;">정렬 </span>
</div>
<div style="padding:10px;display:grid;grid-template-columns:repeat(3,1fr);gap:6px;">
${[{h:70,c:'#bfdbfe',t:'도면 A-101'},{h:90,c:'#bbf7d0',t:'제품 사진'},{h:60,c:'#fde68a',t:'견적서'},{h:80,c:'#ddd6fe',t:'시방서'},{h:65,c:'#fecaca',t:'카탈로그'},{h:75,c:'#e0e7ff',t:'설치 매뉴얼'}].map(i => `
<div style="border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;">
<div style="height:${i.h}px;background:${i.c};display:flex;align-items:center;justify-content:center;font-size:16px;opacity:.5;">📸</div>
<div style="padding:4px 6px;">
<div style="font-size:8px;font-weight:500;color:#1e293b;">${i.t}</div>
<div style="font-size:7px;color:#94a3b8;">2026-03-08</div>
</div>
</div>
`).join('')}
</div>
</div>`;
// 피벗 테이블
if (key.includes('피벗') || key.includes('크로스탭')) return `
<div class="wf-wrap" style="padding:0;font-size:8px;">
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">매출 분석 (피벗)</span>
</div>
<div style="overflow:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead><tr style="background:#f8fafc;">
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:left;">거래처 \\ </th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">1</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">2</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">3</th>
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;font-weight:800;">합계</th>
</tr></thead>
<tbody>
${[['ABC산업','12.5','15.2','18.3','46.0'],['XYZ전자','8.1','9.4','11.2','28.7'],['한국물산','5.3','6.8','7.5','19.6']].map(r =>
`<tr><td style="padding:5px 8px;border:1px solid #e2e8f0;font-weight:500;">${r[0]}</td>${r.slice(1).map((v,i) => `<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;${i===3?'font-weight:800;background:#f8fafc;':''}">${v}M</td>`).join('')}</tr>`
).join('')}
<tr style="background:#f8fafc;font-weight:800;">
<td style="padding:5px 8px;border:1px solid #e2e8f0;">합계</td>
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">25.9M</td>
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">31.4M</td>
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">37.0M</td>
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;color:#3b82f6;">94.3M</td>
</tr>
</tbody>
</table>
</div>
</div>`;
// 수평 타임라인
if (key.includes('수평') && key.includes('타임라인')) return `
<div class="wf-wrap" style="padding:16px;overflow-x:auto;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:16px;">프로젝트 로드맵</div>
<div style="position:relative;min-width:400px;height:100px;">
<div style="position:absolute;top:40px;left:0;right:0;height:2px;background:#e2e8f0;"></div>
${[{x:5,t:'기획',d:'1월',done:true},{x:25,t:'설계',d:'2월',done:true},{x:45,t:'개발',d:'3월',active:true},{x:65,t:'테스트',d:'4월'},{x:85,t:'배포',d:'5월'}].map(e => `
<div style="position:absolute;left:${e.x}%;top:${e.done||e.active?28:28}px;transform:translateX(-50%);text-align:center;">
<div style="width:18px;height:18px;border-radius:50%;margin:0 auto;display:flex;align-items:center;justify-content:center;font-size:8px;
${e.done?'background:#10b981;color:#fff;':e.active?'background:#3b82f6;color:#fff;box-shadow:0 0 0 4px rgba(59,130,246,.2);':'background:#f1f5f9;color:#94a3b8;border:1px solid #e2e8f0;'}">${e.done?'✓':e.active?'●':'○'}</div>
<div style="font-size:9px;font-weight:600;color:${e.done||e.active?'#1e293b':'#94a3b8'};margin-top:6px;">${e.t}</div>
<div style="font-size:7px;color:#94a3b8;">${e.d}</div>
</div>
`).join('')}
</div>
</div>`;
// 트리 테이블
if (key.includes('트리') && key.includes('테이블')) return `
<div class="wf-wrap" style="padding:0;font-size:8px;">
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:6px;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">BOM 구조</span>
<div style="flex:1;"></div>
<span style="font-size:8px;color:#3b82f6;">전체 펼치기</span>
</div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:0;padding:4px 12px;border-bottom:1px solid #e2e8f0;font-weight:600;color:#94a3b8;">
<div>품명</div><div style="text-align:center;">수량</div><div style="text-align:right;">단가</div><div style="text-align:right;">금액</div>
</div>
${[{n:'▾ 블라인드 50mm',q:'1',p:'',a:'15,000',bold:true,lv:0},{n:'알루미늄 슬랫',q:'25',p:'200',a:'5,000',lv:1},{n:'헤드레일',q:'1',p:'3,000',a:'3,000',lv:1},{n:'▸ 부속품 세트',q:'1',p:'',a:'4,500',lv:1,collapsed:true},{n:'조작 코드',q:'1',p:'2,500',a:'2,500',lv:1}].map(r => `
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:0;padding:5px 12px;border-bottom:1px solid #f1f5f9;${r.bold?'font-weight:600;background:#f8fafc;':''}color:#475569;align-items:center;">
<div style="padding-left:${r.lv*16}px;${r.collapsed?'color:#94a3b8;':''}">${r.n}</div>
<div style="text-align:center;">${r.q}</div>
<div style="text-align:right;">${r.p}</div>
<div style="text-align:right;">${r.a}</div>
</div>
`).join('')}
</div>`;
// 멀티 스텝 폼
if (key.includes('멀티') && key.includes('스텝') || key.includes('멀티스텝')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="display:flex;align-items:center;margin-bottom:16px;">
${[{n:'1',t:'기본 정보',done:true},{n:'2',t:'상세 설정',active:true},{n:'3',t:'확인'}].map((s,i) => `
<div style="display:flex;align-items:center;${i>0?'flex:1;':''}">
${i>0?`<div style="flex:1;height:2px;background:${s.done||s.active?'#3b82f6':'#e2e8f0'};margin:0 8px;"></div>`:''}
<div style="width:28px;height:28px;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?'border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${s.done?'✓':s.n}</div>
</div>
`).join('')}
</div>
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:10px;">상세 설정</div>
${['납기일','배송 방법','결제 조건'].map(f => `<div style="margin-bottom:8px;"><div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:3px;">${f}</div><div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div></div>`).join('')}
<div style="display:flex;justify-content:space-between;margin-top:12px;">
<span style="font-size:9px;color:#3b82f6;padding:5px 12px;"> 이전</span>
<div style="padding:5px 14px;background:#3b82f6;color:#fff;border-radius:6px;font-size:9px;">다음 </div>
</div>
</div>`;
// 태그/칩 입력
if (key.includes('태그') && (key.includes('칩') || key.includes('입력'))) return `
<div class="wf-wrap" style="padding:16px;">
<div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:4px;">태그</div>
<div style="min-height:36px;background:#f8fafc;border:2px solid #3b82f6;border-radius:6px;padding:4px 6px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
${['블라인드','50mm','수동','알루미늄'].map(t => `<span style="display:flex;align-items:center;gap:3px;background:#eff6ff;color:#3b82f6;padding:2px 6px;border-radius:4px;font-size:8px;font-weight:500;">${t}<span style="color:#93c5fd;cursor:pointer;font-size:10px;">×</span></span>`).join('')}
<span style="font-size:9px;color:#94a3b8;">태그 입력...</span>
</div>
<div style="margin-top:4px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.08);overflow:hidden;">
${['전동','롤스크린','커튼'].map((s,i) => `<div style="padding:5px 10px;font-size:9px;color:#475569;${i===0?'background:#f8fafc;':''}cursor:pointer;">${s}</div>`).join('')}
</div>
</div>`;
// 슬라이더/레인지
if (key.includes('슬라이더') && (key.includes('레인지') || key.includes('조절'))) return `
<div class="wf-wrap" style="padding:20px;">
<div style="margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span style="font-size:9px;font-weight:600;color:#475569;">가격 범위</span>
<span style="font-size:9px;color:#3b82f6;font-weight:600;">₩10,000 ~ ₩50,000</span>
</div>
<div style="position:relative;height:6px;background:#e2e8f0;border-radius:3px;">
<div style="position:absolute;left:20%;right:35%;height:100%;background:#3b82f6;border-radius:3px;"></div>
<div style="position:absolute;left:20%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #3b82f6;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
<div style="position:absolute;left:65%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #3b82f6;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:4px;font-size:7px;color:#94a3b8;"><span>₩0</span><span>₩100,000</span></div>
</div>
<div>
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span style="font-size:9px;font-weight:600;color:#475569;">수량</span>
<span style="font-size:9px;color:#1e293b;font-weight:600;">65</span>
</div>
<div style="position:relative;height:6px;background:#e2e8f0;border-radius:3px;">
<div style="position:absolute;left:0;width:65%;height:100%;background:#10b981;border-radius:3px;"></div>
<div style="position:absolute;left:65%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #10b981;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
</div>
</div>
</div>`;
// 토글 설정
if (key.includes('토글') && (key.includes('설정') || key.includes('스위치'))) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:11px;font-weight:700;color:#1e293b;">알림 설정</div>
${[{t:'이메일 알림',d:'수주/견적 상태 변경 시 이메일 발송',on:true},{t:'푸시 알림',d:'실시간 푸시 알림 수신',on:true},{t:'슬랙 연동',d:'슬랙 채널에 알림 전달',on:false},{t:'주간 리포트',d:'매주 월요일 주간 요약 발송',on:true},{t:'마케팅 메일',d:'신규 기능 안내 및 팁',on:false}].map(s => `
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px;">
<div style="flex:1;">
<div style="font-size:9px;font-weight:600;color:#1e293b;">${s.t}</div>
<div style="font-size:8px;color:#94a3b8;">${s.d}</div>
</div>
<div style="width:36px;height:20px;border-radius:10px;background:${s.on?'#3b82f6':'#e2e8f0'};position:relative;cursor:pointer;">
<div style="position:absolute;top:2px;${s.on?'right:2px;':'left:2px;'}width:16px;height:16px;background:#fff;border-radius:50%;box-shadow:0 1px 2px rgba(0,0,0,.15);"></div>
</div>
</div>
`).join('')}
</div>`;
// 서명 패드
if (key.includes('서명') && (key.includes('패드') || key.includes('전자'))) return `
<div class="wf-wrap" style="padding:16px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">전자 서명</div>
<div style="border:2px dashed #cbd5e1;border-radius:8px;padding:20px;background:repeating-linear-gradient(0deg,transparent,transparent 19px,#f1f5f9 19px,#f1f5f9 20px);min-height:80px;position:relative;display:flex;align-items:center;justify-content:center;">
<svg width="120" height="40" viewBox="0 0 120 40"><path d="M10,30 Q20,10 40,25 T70,15 T100,28" fill="none" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/></svg>
<div style="position:absolute;bottom:4px;right:8px;font-size:7px;color:#94a3b8;">마우스/터치로 서명</div>
</div>
<div style="display:flex;gap:6px;margin-top:8px;">
<span style="font-size:8px;padding:4px 10px;background:#f1f5f9;color:#64748b;border-radius:4px;">지우기</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;">서명 완료</span>
</div>
</div>`;
// 컬러 피커
if (key.includes('컬러') && key.includes('피커')) return `
<div style="width:100%;max-width:240px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.1);overflow:hidden;border:1px solid #e2e8f0;padding:10px;">
<div style="width:100%;height:100px;border-radius:6px;background:linear-gradient(to right,#fff,#3b82f6);position:relative;">
<div style="position:absolute;inset:0;background:linear-gradient(to bottom,transparent,#000);border-radius:6px;"></div>
<div style="position:absolute;left:65%;top:35%;width:12px;height:12px;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.3);"></div>
</div>
<div style="margin-top:8px;height:10px;border-radius:5px;background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00);position:relative;">
<div style="position:absolute;left:60%;top:-2px;width:14px;height:14px;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.3);background:#3b82f6;"></div>
</div>
<div style="display:flex;gap:6px;margin-top:8px;align-items:center;">
<div style="width:28px;height:28px;background:#3b82f6;border-radius:6px;border:1px solid #e2e8f0;"></div>
<div style="flex:1;height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 6px;font-size:9px;line-height:24px;color:#475569;">#3B82F6</div>
</div>
<div style="display:flex;gap:3px;margin-top:6px;">
${['#ef4444','#f59e0b','#10b981','#3b82f6','#8b5cf6','#ec4899','#64748b','#1e293b'].map(c => `<div style="width:18px;height:18px;background:${c};border-radius:4px;cursor:pointer;"></div>`).join('')}
</div>
</div>`;
// 평점/별점
if (key.includes('별점') || key.includes('평점') && key.includes('리뷰')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:28px;font-weight:800;color:#1e293b;">4.5</div>
<div style="font-size:16px;color:#f59e0b;margin:4px 0;">★★★★☆</div>
<div style="font-size:8px;color:#94a3b8;">128 리뷰</div>
</div>
<div style="display:flex;flex-direction:column;gap:3px;margin-bottom:12px;">
${[{s:5,w:65},{s:4,w:20},{s:3,w:8},{s:2,w:4},{s:1,w:3}].map(r => `
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:8px;color:#64748b;width:12px;">${r.s}★</span>
<div style="flex:1;height:6px;background:#f1f5f9;border-radius:3px;overflow:hidden;"><div style="height:100%;width:${r.w}%;background:#f59e0b;border-radius:3px;"></div></div>
<span style="font-size:7px;color:#94a3b8;width:20px;">${r.w}%</span>
</div>
`).join('')}
</div>
<div style="border-top:1px solid #e2e8f0;padding-top:10px;">
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">리뷰 작성</div>
<div style="font-size:18px;color:#e2e8f0;margin-bottom:4px;">☆☆☆☆☆</div>
<div style="height:40px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div>
</div>
</div>`;
// 바텀 시트
if (key.includes('바텀') && key.includes('시트')) return `
<div style="width:100%;max-width:280px;background:#f8fafc;border-radius:20px;overflow:hidden;border:6px solid #1e293b;margin:0 auto;height:280px;position:relative;">
<div style="padding:10px;opacity:.3;">
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;"></div>
<div class="wf-bar lg" style="height:6px;"></div>
</div>
<div style="position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;box-shadow:0 -4px 20px rgba(0,0,0,.1);padding:8px 16px 16px;">
<div style="width:32px;height:4px;background:#e2e8f0;border-radius:2px;margin:0 auto 10px;"></div>
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:8px;">필터</div>
${['상태','날짜 범위','거래처'].map(f => `<div style="padding:8px 0;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center;font-size:9px;"><span style="color:#475569;">${f}</span><span style="color:#94a3b8;">선택 ▸</span></div>`).join('')}
<div style="display:flex;gap:6px;margin-top:10px;">
<div style="flex:1;padding:6px;text-align:center;background:#f1f5f9;border-radius:6px;font-size:9px;color:#64748b;">초기화</div>
<div style="flex:1;padding:6px;text-align:center;background:#3b82f6;border-radius:6px;font-size:9px;color:#fff;">적용</div>
</div>
</div>
</div>`;
// 컨텍스트 메뉴
if (key.includes('컨텍스트') && key.includes('우클릭')) return `
<div style="width:100%;max-width:400px;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:200px;padding:12px;">
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;opacity:.3;"></div>
<div class="wf-bar lg" style="height:6px;opacity:.3;"></div>
<div style="position:absolute;top:60px;left:120px;width:160px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.12);overflow:hidden;z-index:2;">
${[{t:'복사',k:'⌘C'},{t:'붙여넣기',k:'⌘V'},{t:'잘라내기',k:'⌘X'},{sep:true},{t:'편집',icon:'✏️'},{t:'복제',icon:'📋'},{t:'이동',icon:'📁'},{sep:true},{t:'삭제',danger:true,icon:'🗑️'}].map(i =>
i.sep?`<div style="height:1px;background:#e2e8f0;margin:2px 0;"></div>`:
`<div style="display:flex;align-items:center;gap:6px;padding:5px 10px;font-size:9px;color:${i.danger?'#ef4444':'#475569'};cursor:pointer;">
${i.icon?`<span>${i.icon}</span>`:''}
<span style="flex:1;">${i.t}</span>
${i.k?`<span style="font-size:7px;color:#94a3b8;">${i.k}</span>`:''}
</div>`
).join('')}
</div>
</div>`;
// 슬라이드오버 패널
if (key.includes('슬라이드오버') || key.includes('사이드') && key.includes('패널') && key.includes('상세')) return `
<div style="width:100%;display:flex;min-height:220px;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;position:relative;overflow:hidden;">
<div style="flex:1;padding:12px;opacity:.4;">
<div class="wf-bar xl dark" style="height:10px;margin-bottom:8px;"></div>
<div class="wf-bar lg" style="height:8px;margin-bottom:16px;"></div>
<div class="wf-box" style="height:80px;"></div>
</div>
<div style="position:absolute;top:0;right:0;bottom:0;width:200px;background:#fff;box-shadow:-4px 0 20px rgba(0,0,0,.1);border-left:1px solid #e2e8f0;">
<div style="padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">수주 상세</span>
<span style="color:#94a3b8;cursor:pointer;font-size:12px;"></span>
</div>
<div style="padding:10px 12px;">
${['거래처|ABC산업','금액|₩12.5M','상태|진행 중','담당자|홍길동'].map(f => {
const [k,v] = f.split('|');
return `<div style="margin-bottom:8px;"><div style="font-size:7px;color:#94a3b8;">${k}</div><div style="font-size:9px;color:#1e293b;font-weight:500;">${v}</div></div>`;
}).join('')}
</div>
<div style="padding:8px 12px;border-top:1px solid #e2e8f0;display:flex;gap:4px;">
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;flex:1;text-align:center;">편집</span>
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">삭제</span>
</div>
</div>
</div>`;
// 쿠키 동의 배너
if (key.includes('쿠키') || key.includes('gdpr') || key.includes('동의') && key.includes('배너')) return `
<div style="width:100%;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:220px;padding:12px;">
<div style="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:0;left:0;right:0;background:#1e293b;border-radius:0 0 8px 8px;padding:12px 16px;display:flex;align-items:center;gap:12px;">
<span style="font-size:16px;">🍪</span>
<div style="flex:1;">
<div style="font-size:9px;color:#e2e8f0;margin-bottom:2px;"> 웹사이트는 쿠키를 사용합니다</div>
<div style="font-size:7px;color:#94a3b8;"> 나은 경험을 위해 쿠키를 사용합니다. <span style="color:#3b82f6;">자세히 보기</span></div>
</div>
<div style="display:flex;gap:4px;flex-shrink:0;">
<span style="font-size:8px;padding:4px 10px;background:#475569;color:#e2e8f0;border-radius:4px;">설정</span>
<span style="font-size:8px;padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;">모두 동의</span>
</div>
</div>
</div>`;
// 탑 네비게이션
if (key.includes('탑바') || key.includes('gnb') || key.includes('탑') && key.includes('네비게이션') && key.includes('검색')) return `
<div class="wf-wrap" style="padding:0;">
<div style="background:#fff;border-bottom:1px solid #e2e8f0;padding:0 16px;height:48px;display:flex;align-items:center;gap:12px;">
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;">
<div style="width:24px;height:24px;background:#3b82f6;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:800;">S</div>
<span style="font-size:12px;font-weight:700;color:#1e293b;">SAM</span>
</div>
${['대시보드','수주','거래처','품목'].map((m,i) => `<span style="font-size:9px;color:${i===0?'#3b82f6':'#64748b'};font-weight:${i===0?'600':'400'};padding:14px 0;${i===0?'border-bottom:2px solid #3b82f6;':''}">${m}</span>`).join('')}
<div style="flex:1;max-width:200px;margin:0 auto;">
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:0 10px;display:flex;align-items:center;gap:4px;">
<span style="font-size:10px;color:#94a3b8;">🔍</span>
<span style="font-size:8px;color:#94a3b8;">검색... ⌘K</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<div style="position:relative;"><span style="font-size:14px;">🔔</span><div style="position:absolute;top:-2px;right:-4px;width:10px;height:10px;background:#ef4444;border-radius:50%;font-size:6px;color:#fff;display:flex;align-items:center;justify-content:center;">3</div></div>
<div class="wf-circle" style="width:24px;height:24px;"></div>
</div>
</div>
<div style="padding:16px;">
<div class="wf-bar xl dark" style="height:10px;margin-bottom:8px;"></div>
<div class="wf-box" style="height:60px;padding:10px;"><div class="wf-text">콘텐츠</div></div>
</div>
</div>`;
// 앵커/스크롤 스파이
if (key.includes('앵커') || key.includes('스크롤스파이') || key.includes('목차') && key.includes('사이드')) return `
<div class="wf-wrap" style="display:flex;padding:0;min-height:220px;">
<div style="flex:1;padding:14px;overflow:hidden;">
<div style="font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;">가이드 문서</div>
${['1. 소개','내용이 여기에 표시됩니다...','2. 설치 방법','npm install sam-sdk','3. 기본 사용법'].map((t,i) =>
i%2===0?`<div style="font-size:11px;font-weight:600;color:#1e293b;margin:12px 0 4px;${i===2?'color:#3b82f6;':''}">${t}</div>`:
`<div style="font-size:9px;color:#64748b;line-height:1.6;">${t}</div>`
).join('')}
</div>
<div style="width:120px;border-left:1px solid #e2e8f0;padding:14px 8px;position:sticky;top:0;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:6px;">목차</div>
${['소개','설치 방법','기본 사용법','API 레퍼런스','FAQ'].map((t,i) => `
<div style="font-size:8px;padding:3px 8px;color:${i===1?'#3b82f6':'#64748b'};font-weight:${i===1?'600':'400'};border-left:2px solid ${i===1?'#3b82f6':'transparent'};margin-bottom:2px;">${t}</div>
`).join('')}
</div>
</div>`;
// 페이지네이션
if (key.includes('페이지네이션') && key.includes('패턴')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">페이지네이션 유형</div>
<div style="margin-bottom:12px;">
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">기본형</div>
<div style="display:flex;align-items:center;gap:3px;">
${['◀','1','2','3','...','28','29','30','▶'].map(p => `<span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;font-size:8px;cursor:pointer;${p==='3'?'background:#3b82f6;color:#fff;':'background:#f8fafc;color:#64748b;border:1px solid #e2e8f0;'}">${p}</span>`).join('')}
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">미니형</div>
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:9px;color:#3b82f6;"> 이전</span>
<span style="font-size:9px;color:#475569;">3 / 30</span>
<span style="font-size:9px;color:#3b82f6;">다음 </span>
</div>
</div>
<div>
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">상세형</div>
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:8px;color:#64748b;"> 842</span>
<span style="font-size:8px;color:#64748b;">|</span>
<span style="font-size:8px;color:#64748b;">페이지당 <span style="padding:1px 4px;background:#f1f5f9;border-radius:2px;">20 </span></span>
<div style="flex:1;"></div>
<div style="display:flex;gap:2px;">
${['◀','1','2','3','▶'].map(p => `<span style="padding:2px 6px;border-radius:3px;font-size:8px;${p==='2'?'background:#3b82f6;color:#fff;':'background:#f1f5f9;color:#64748b;'}">${p}</span>`).join('')}
</div>
</div>
</div>
</div>`;
// FAB
if (key.includes('fab') || key.includes('플로팅') && key.includes('액션')) return `
<div style="width:100%;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:240px;padding:12px;">
<div style="opacity:.3;"><div class="wf-bar xl" style="height:10px;margin-bottom:8px;"></div><div class="wf-bar lg" style="height:8px;margin-bottom:12px;"></div><div class="wf-box" style="height:80px;"></div></div>
<div style="position:absolute;bottom:16px;right:16px;display:flex;flex-direction:column;align-items:center;gap:6px;">
${[{icon:'📋',label:'수주 등록'},{icon:'🏢',label:'거래처 등록'},{icon:'📦',label:'품목 등록'}].map(a => `
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:8px;color:#475569;background:#fff;padding:2px 6px;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.1);">${a.label}</span>
<div style="width:36px;height:36px;background:#fff;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.1);display:flex;align-items:center;justify-content:center;font-size:14px;">${a.icon}</div>
</div>
`).join('')}
<div style="width:48px;height:48px;background:#3b82f6;border-radius:50%;box-shadow:0 4px 12px rgba(59,130,246,.3);display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;transform:rotate(45deg);">+</div>
</div>
</div>`;
// 사이드 탭
if (key.includes('세로') && key.includes('탭') || key.includes('사이드') && key.includes('탭')) return `
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
<div style="width:48px;background:#f8fafc;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:2px;">
${[{icon:'📊',active:true},{icon:'📋',active:false},{icon:'🏢',active:false},{icon:'⚙️',active:false}].map(t => `
<div style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:6px;font-size:14px;${t.active?'background:#eff6ff;border-left:2px solid #3b82f6;':''}">${t.icon}</div>
`).join('')}
</div>
<div style="flex:1;padding:12px;">
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:8px;">📊 대시보드</div>
<div class="wf-bar lg" style="height:8px;margin-bottom:6px;"></div>
<div class="wf-bar md" style="height:8px;margin-bottom:12px;"></div>
<div class="wf-box" style="height:80px;padding:8px;"><div class="wf-text">콘텐츠 영역</div></div>
</div>
</div>`;
// RBAC 권한 관리
if (key.includes('권한') && (key.includes('rbac') || key.includes('역할') || key.includes('접근'))) return `
<div class="wf-wrap" style="padding:0;font-size:8px;">
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:10px;font-weight:700;color:#1e293b;">역할/권한 관리</div>
<div style="overflow:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead><tr style="background:#f8fafc;">
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:left;">기능</th>
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#ef4444;">관리자</th>
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#3b82f6;">매니저</th>
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#10b981;">일반</th>
</tr></thead>
<tbody>
${[['수주 조회','✅','✅','✅'],['수주 등록','✅','✅','❌'],['수주 삭제','✅','❌','❌'],['설정 변경','✅','❌','❌'],['사용자 관리','✅','❌','❌']].map(r =>
`<tr><td style="padding:4px 10px;border:1px solid #e2e8f0;">${r[0]}</td>${r.slice(1).map(v => `<td style="padding:4px 10px;border:1px solid #e2e8f0;text-align:center;">${v}</td>`).join('')}</tr>`
).join('')}
</tbody>
</table>
</div>
</div>`;
// 세션/디바이스 관리
if (key.includes('세션') && (key.includes('디바이스') || key.includes('관리'))) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">활성 세션</span>
<span style="font-size:8px;color:#ef4444;">모두 로그아웃</span>
</div>
${[{d:'Chrome · Windows',ip:'192.168.1.100',time:'현재 활동 중',current:true},{d:'Safari · macOS',ip:'10.0.0.55',time:'2시간 전'},{d:'Samsung Internet · Android',ip:'172.16.0.12',time:'어제'}].map(s => `
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px;">
<span style="font-size:18px;">${s.d.includes('Windows')?'💻':s.d.includes('mac')?'🖥️':'📱'}</span>
<div style="flex:1;">
<div style="font-size:9px;font-weight:600;color:#1e293b;">${s.d} ${s.current?'<span style="font-size:7px;background:#dcfce7;color:#16a34a;padding:1px 4px;border-radius:3px;">이 기기</span>':''}</div>
<div style="font-size:7px;color:#94a3b8;">${s.ip} · ${s.time}</div>
</div>
${!s.current?'<span style="font-size:8px;color:#ef4444;cursor:pointer;">로그아웃</span>':''}
</div>
`).join('')}
</div>`;
// API 키 관리
if (key.includes('api') && key.includes('키') || key.includes('토큰') && key.includes('관리')) return `
<div class="wf-wrap" style="padding:0;">
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">🔑 API </span>
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">+ 생성</span>
</div>
${[{name:'Production Key',key:'sk-prod...x8f2',created:'2026-01-15',last:'2분 전'},{name:'Development Key',key:'sk-dev...m4k9',created:'2026-02-20',last:'1시간 전'},{name:'Test Key',key:'sk-test...p2j7',created:'2026-03-01',last:'3일 전'}].map(k => `
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:8px;">
<div style="flex:1;">
<div style="font-size:9px;font-weight:600;color:#1e293b;">${k.name}</div>
<div style="display:flex;align-items:center;gap:4px;margin-top:2px;">
<code style="font-size:8px;background:#f8fafc;padding:1px 4px;border-radius:2px;color:#64748b;font-family:monospace;">${k.key}</code>
<span style="font-size:7px;color:#3b82f6;cursor:pointer;">📋</span>
</div>
<div style="font-size:7px;color:#94a3b8;margin-top:1px;">생성: ${k.created} · 마지막 사용: ${k.last}</div>
</div>
<span style="font-size:8px;color:#ef4444;">폐기</span>
</div>
`).join('')}
</div>`;
// 간트 차트
if (key.includes('간트') || key.includes('gantt')) return `
<div class="wf-wrap" style="padding:0;font-size:8px;">
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;">
<span style="font-size:10px;font-weight:700;color:#1e293b;">프로젝트 일정</span>
<div style="flex:1;"></div>
<span style="color:#94a3b8;"></span><span style="font-weight:600;color:#1e293b;">2026 3</span><span style="color:#94a3b8;"></span>
</div>
<div style="display:flex;">
<div style="width:100px;flex-shrink:0;border-right:1px solid #e2e8f0;">
<div style="padding:4px 8px;border-bottom:1px solid #e2e8f0;font-weight:600;color:#94a3b8;height:24px;">작업</div>
${['기획','UI 설계','개발','테스트','배포'].map(t => `<div style="padding:4px 8px;border-bottom:1px solid #f1f5f9;color:#475569;height:20px;">${t}</div>`).join('')}
</div>
<div style="flex:1;overflow:hidden;">
<div style="display:flex;border-bottom:1px solid #e2e8f0;height:24px;">
${Array.from({length:10},(_, i) => `<div style="flex:1;padding:4px;text-align:center;color:#94a3b8;border-right:1px solid #f1f5f9;">${i+1}</div>`).join('')}
</div>
${[{s:0,w:3,c:'#10b981',p:100},{s:2,w:3,c:'#3b82f6',p:80},{s:3,w:5,c:'#f59e0b',p:40},{s:6,w:3,c:'#8b5cf6',p:0},{s:8,w:2,c:'#ec4899',p:0}].map(b => `
<div style="height:20px;position:relative;border-bottom:1px solid #f1f5f9;">
<div style="position:absolute;left:${b.s*10}%;width:${b.w*10}%;top:3px;height:14px;background:${b.c};opacity:.2;border-radius:3px;"></div>
<div style="position:absolute;left:${b.s*10}%;width:${b.w*10*b.p/100}%;top:3px;height:14px;background:${b.c};border-radius:3px;"></div>
</div>
`).join('')}
<div style="position:absolute;left:calc(100px + 50%);top:24px;bottom:0;width:2px;background:#ef4444;opacity:.5;"></div>
</div>
</div>
</div>`;
// 조직도
if (key.includes('조직도') && key.includes('트리')) return `
<div class="wf-wrap" style="padding:16px;text-align:center;">
<div style="display:inline-block;padding:8px 16px;background:#3b82f6;color:#fff;border-radius:8px;font-size:10px;font-weight:700;">대표이사<div style="font-size:8px;font-weight:400;opacity:.8;">홍길동</div></div>
<div style="width:2px;height:12px;background:#e2e8f0;margin:0 auto;"></div>
<div style="display:flex;justify-content:center;gap:0;">
<div style="flex:1;border-top:2px solid #e2e8f0;"></div>
</div>
<div style="display:flex;justify-content:center;gap:20px;margin-top:0;">
${['영업팀|김영업','개발팀|이개발','생산팀|박생산'].map(d => {
const [dept,lead] = d.split('|');
return `<div style="text-align:center;">
<div style="width:2px;height:12px;background:#e2e8f0;margin:0 auto;"></div>
<div style="display:inline-block;padding:6px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;font-size:9px;font-weight:600;color:#1e293b;">${dept}<div style="font-size:7px;color:#64748b;font-weight:400;">${lead}</div></div>
</div>`;
}).join('')}
</div>
</div>`;
// 워터폴 차트
if (key.includes('워터폴') && key.includes('재무')) return `
<div class="wf-wrap" style="padding:12px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">손익 분석</div>
<div style="display:flex;align-items:flex-end;gap:4px;height:120px;padding-bottom:20px;position:relative;">
${[{l:'매출',v:100,h:100,c:'#3b82f6',base:0},{l:'원가',v:-40,h:40,c:'#ef4444',base:60},{l:'판관비',v:-25,h:25,c:'#ef4444',base:35},{l:'기타수익',v:10,h:10,c:'#10b981',base:35},{l:'영업이익',v:45,h:45,c:'#8b5cf6',base:0}].map(b => `
<div style="flex:1;display:flex;flex-direction:column;align-items:center;">
<div style="font-size:7px;color:#475569;font-weight:600;margin-bottom:2px;">${b.v>0?'+':''}${b.v}M</div>
<div style="width:100%;height:${b.h}%;margin-bottom:${b.base}%;background:${b.c};border-radius:3px 3px 0 0;opacity:.8;min-height:2px;"></div>
<div style="font-size:6px;color:#94a3b8;margin-top:2px;white-space:nowrap;">${b.l}</div>
</div>
`).join('')}
</div>
</div>`;
// 리포트 빌더
if (key.includes('리포트') && key.includes('빌더') || key.includes('커스텀') && key.includes('보고서')) return `
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
<div style="width:120px;border-right:1px solid #e2e8f0;padding:8px;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:4px;">필드 선택</div>
${['📋 거래처명','💰 수주금액','📅 수주일','👤 담당자','📊 상태'].map((f,i) => `<div style="padding:3px 6px;font-size:8px;color:${i<3?'#3b82f6':'#64748b'};background:${i<3?'#eff6ff':'transparent'};border-radius:3px;margin-bottom:2px;cursor:grab;">${f} ${i<3?'✓':''}</div>`).join('')}
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin:6px 0 4px;">차트</div>
${['📊 바','📈 라인','🍩 도넛'].map((c,i) => `<div style="padding:3px 6px;font-size:8px;color:${i===0?'#3b82f6':'#64748b'};background:${i===0?'#eff6ff':'transparent'};border-radius:3px;margin-bottom:2px;">${c}</div>`).join('')}
</div>
<div style="flex:1;padding:10px;">
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:6px;">미리보기</div>
<div class="wf-box" style="padding:8px;height:60px;">
<div style="display:flex;align-items:flex-end;gap:4px;height:40px;">
${[60,40,80,50,70,90,45].map(h => `<div style="flex:1;height:${h}%;background:#3b82f6;border-radius:2px 2px 0 0;opacity:.6;"></div>`).join('')}
</div>
</div>
<div style="display:flex;gap:4px;margin-top:6px;">
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">실행</span>
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">저장</span>
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">📥 PDF</span>
</div>
</div>
</div>`;
// 댓글/스레드
if (key.includes('댓글') || key.includes('스레드') && key.includes('답글')) return `
<div class="wf-wrap" style="padding:12px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:10px;">💬 댓글 3</div>
${[{u:'김영업',t:'견적 금액이 확정되면 알려주세요.',time:'2시간 전',replies:[{u:'이관리',t:'네, 오늘 중으로 확인하겠습니다.',time:'1시간 전'}]},{u:'박생산',t:'납기일 조정이 필요합니다.',time:'30분 전',replies:[]}].map(c => `
<div style="margin-bottom:10px;">
<div style="display:flex;gap:6px;">
<div class="wf-circle" style="width:24px;height:24px;flex-shrink:0;"></div>
<div style="flex:1;">
<div style="font-size:9px;"><span style="font-weight:600;color:#1e293b;">${c.u}</span> <span style="color:#94a3b8;font-size:7px;">${c.time}</span></div>
<div style="font-size:9px;color:#475569;margin:3px 0;">${c.t}</div>
<div style="display:flex;gap:8px;font-size:7px;color:#94a3b8;"><span>👍 2</span><span>답글</span></div>
${c.replies.map(r => `
<div style="display:flex;gap:6px;margin-top:6px;padding-left:8px;border-left:2px solid #e2e8f0;">
<div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div>
<div><div style="font-size:8px;"><span style="font-weight:600;color:#1e293b;">${r.u}</span> <span style="color:#94a3b8;font-size:7px;">${r.time}</span></div><div style="font-size:8px;color:#475569;margin-top:2px;">${r.t}</div></div>
</div>
`).join('')}
</div>
</div>
</div>
`).join('')}
</div>`;
// 에러 페이지
if (key.includes('에러') && (key.includes('404') || key.includes('500') || key.includes('오류'))) return `
<div class="wf-wrap" style="padding:30px;text-align:center;">
<div style="font-size:48px;font-weight:900;color:#e2e8f0;line-height:1;">404</div>
<div style="font-size:13px;font-weight:700;color:#1e293b;margin:8px 0 4px;">페이지를 찾을 없습니다</div>
<div style="font-size:9px;color:#94a3b8;margin-bottom:16px;line-height:1.6;">요청하신 페이지가 존재하지 않거나<br>이동되었을 있습니다</div>
<div style="display:flex;justify-content:center;gap:6px;">
<span style="font-size:9px;padding:5px 14px;background:#3b82f6;color:#fff;border-radius:6px;">홈으로 가기</span>
<span style="font-size:9px;padding:5px 14px;background:#f1f5f9;color:#64748b;border-radius:6px;"> 이전 페이지</span>
</div>
</div>`;
// 비교 테이블
if (key.includes('비교') && (key.includes('기능') || key.includes('제품') || key.includes('체크'))) return `
<div class="wf-wrap" style="padding:0;font-size:8px;">
<div style="display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:0;">
<div style="padding:8px;border:1px solid #e2e8f0;"></div>
${['Basic','Pro','Enterprise'].map((p,i) => `<div style="padding:8px;border:1px solid ${i===1?'#3b82f6':'#e2e8f0'};text-align:center;${i===1?'background:#eff6ff;':''}"><div style="font-weight:700;color:${i===1?'#3b82f6':'#1e293b'};">${p}</div>${i===1?'<div style="font-size:6px;background:#3b82f6;color:#fff;padding:1px 4px;border-radius:6px;display:inline-block;margin-top:2px;">추천</div>':''}</div>`).join('')}
${[['사용자 수','5명','50명','무제한'],['저장 공간','1GB','10GB','무제한'],['API 호출','1,000/일','10,000/일','무제한'],['우선 지원','❌','✅','✅'],['커스텀 도메인','❌','❌','✅']].map(r =>
`<div style="padding:5px 8px;border:1px solid #e2e8f0;color:#475569;">${r[0]}</div>${r.slice(1).map((v,i) => `<div style="padding:5px 8px;border:1px solid ${i===1?'#3b82f6':'#e2e8f0'};text-align:center;${i===1?'background:#f8fbff;':''}">${v}</div>`).join('')}`
).join('')}
</div>
</div>`;
// 캐러셀/슬라이더
if (key.includes('캐러셀') || key.includes('슬라이더') && key.includes('스와이프')) return `
<div class="wf-wrap" style="padding:0;position:relative;overflow:hidden;">
<div style="display:flex;">
<div style="width:100%;flex-shrink:0;height:140px;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;color:#fff;">
<div style="text-align:center;"><div style="font-size:12px;font-weight:700;">슬라이드 2 / 5</div><div style="font-size:9px;opacity:.8;">콘텐츠가 여기에 표시됩니다</div></div>
</div>
</div>
<div style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;background:rgba(255,255,255,.8);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 2px 4px rgba(0,0,0,.1);"></div>
<div style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;background:rgba(255,255,255,.8);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 2px 4px rgba(0,0,0,.1);"></div>
<div style="position:absolute;bottom:8px;left:50%;transform:translateX(-50%);display:flex;gap:4px;">
${[false,true,false,false,false].map(a => `<div style="width:${a?'16px':'6px'};height:6px;background:${a?'#fff':'rgba(255,255,255,.5)'};border-radius:3px;"></div>`).join('')}
</div>
</div>`;
// 아코디언/FAQ
if (key.includes('아코디언') || key.includes('faq')) return `
<div class="wf-wrap" style="padding:12px;">
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:10px;">자주 묻는 질문</div>
${[{q:'SAM은 어떤 서비스인가요?',a:'SAM은 블라인드/스크린 제조업체를 위한 ERP/MES 통합 솔루션입니다.',open:true},{q:'무료 체험이 가능한가요?',a:'',open:false},{q:'데이터 마이그레이션을 지원하나요?',a:'',open:false},{q:'커스터마이징이 가능한가요?',a:'',open:false}].map(f => `
<div style="border:1px solid ${f.open?'#3b82f6':'#e2e8f0'};border-radius:6px;margin-bottom:4px;overflow:hidden;">
<div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;${f.open?'background:#eff6ff;':''}">
<span style="font-size:9px;font-weight:600;color:${f.open?'#3b82f6':'#1e293b'};">${f.q}</span>
<span style="font-size:10px;color:${f.open?'#3b82f6':'#94a3b8'};transition:transform .2s;${f.open?'transform:rotate(180deg);':''}">▾</span>
</div>
${f.open?`<div style="padding:0 12px 8px;font-size:9px;color:#64748b;line-height:1.6;">${f.a}</div>`:''}
</div>
`).join('')}
</div>`;
// 툴팁/팝오버
if (key.includes('툴팁') || key.includes('팝오버')) return `
<div class="wf-wrap" style="padding:24px;display:flex;flex-direction:column;align-items:center;gap:24px;">
<div style="position:relative;display:inline-block;">
<div style="padding:4px 10px;background:#f1f5f9;border-radius:4px;font-size:9px;color:#64748b;">hover 요소</div>
<div style="position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;padding:4px 8px;border-radius:4px;font-size:8px;white-space:nowrap;">
이것은 툴팁입니다
<div style="position:absolute;top:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:#1e293b;"></div>
</div>
</div>
<div style="position:relative;display:inline-block;">
<div style="padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;font-size:9px;">클릭 요소</div>
<div style="position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:10px;box-shadow:0 4px 12px rgba(0,0,0,.1);width:160px;">
<div style="position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#fff;"></div>
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">팝오버 제목</div>
<div style="font-size:8px;color:#64748b;">추가 정보를 여기에 표시합니다.</div>
</div>
</div>
</div>`;
// 프로그레스 트래커
if (key.includes('프로그레스') && (key.includes('배송') || key.includes('처리') || key.includes('추적'))) return `
<div class="wf-wrap" style="padding:16px;">
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">수주 진행 상태</div>
<div style="display:flex;flex-direction:column;gap:0;padding-left:12px;">
${[{s:'수주 접수',t:'2026-03-01 09:00',done:true},{s:'설계 검토',t:'2026-03-03 14:00',done:true},{s:'생산 중',t:'2026-03-05 ~',active:true},{s:'품질 검사',t:'',wait:true},{s:'출하/배송',t:'',wait:true}].map(e => `
<div style="display:flex;gap:10px;padding-bottom:${e.wait&&!e.active?'0':'12'}px;position:relative;">
<div style="display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:16px;">
<div style="width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:8px;
${e.done?'background:#10b981;color:#fff;':e.active?'background:#3b82f6;color:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.2);':'background:#f1f5f9;color:#94a3b8;border:1px solid #e2e8f0;'}">${e.done?'✓':e.active?'●':'○'}</div>
${!e.wait?`<div style="width:2px;flex:1;background:${e.done?'#10b981':'#e2e8f0'};margin-top:2px;"></div>`:''}
</div>
<div style="padding-bottom:4px;">
<div style="font-size:9px;font-weight:600;color:${e.done||e.active?'#1e293b':'#94a3b8'};">${e.s}</div>
${e.t?`<div style="font-size:7px;color:#94a3b8;">${e.t}</div>`:''}
</div>
</div>
`).join('')}
</div>
</div>`;
// 데이터 임포트
if (key.includes('임포트') || key.includes('가져오기') && key.includes('csv')) return `
<div class="wf-wrap" style="padding:16px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
${['1 업로드','2 매핑','3 확인'].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:${i<=1?'#3b82f6':'#e2e8f0'};"></div>`:''}
<div style="width:20px;height:20px;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center;
${i===0?'background:#3b82f6;color:#fff;':i===1?'border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${i+1}</div>
<span style="font-size:8px;color:${i<=1?'#1e293b':'#94a3b8'};">${s.slice(2)}</span>
</div>
`).join('')}
</div>
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">필드 매핑</div>
${['거래처명 → company_name','연락처 → phone','주소 → address'].map(m => `
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="font-size:8px;color:#64748b;width:60px;text-align:right;">${m.split(' → ')[0]}</span>
<span style="font-size:10px;color:#94a3b8;">→</span>
<div style="flex:1;height:24px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;padding:0 6px;font-size:8px;line-height:24px;color:#3b82f6;">${m.split(' → ')[1]}</div>
</div>
`).join('')}
</div>`;
// 랜딩 히어로
if (key.includes('랜딩') || key.includes('히어로') && key.includes('cta')) return `
<div class="wf-wrap" style="padding:0;background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);min-height:200px;display:flex;align-items:center;justify-content:center;">
<div style="text-align:center;padding:24px;">
<div style="font-size:16px;font-weight:800;color:#fff;line-height:1.3;margin-bottom:8px;">스마트한 제조 관리,<br>SAM으로 시작하세요</div>
<div style="font-size:9px;color:#94a3b8;margin-bottom:16px;line-height:1.6;">수주부터 출하까지, 하나의 플랫폼에서<br>제조 공정을 관리하세요</div>
<div style="display:flex;gap:6px;justify-content:center;">
<span style="font-size:9px;padding:6px 16px;background:#3b82f6;color:#fff;border-radius:6px;font-weight:600;">무료로 시작하기</span>
<span style="font-size:9px;padding:6px 16px;background:transparent;color:#fff;border-radius:6px;border:1px solid rgba(255,255,255,.3);">데모 보기</span>
</div>
<div style="display:flex;justify-content:center;gap:12px;margin-top:12px;">
${['제조사 120+','도입 6개월','만족도 98%'].map(s => `<span style="font-size:7px;color:#64748b;">${s}</span>`).join('')}
</div>
</div>
</div>`;
// 코드 에디터
if (key.includes('코드') && (key.includes('에디터') || key.includes('구문'))) return `
<div class="wf-wrap" style="padding:0;background:#1e1e1e;border-radius:8px;overflow:hidden;">
<div style="padding:6px 12px;background:#2d2d2d;display:flex;align-items:center;gap:6px;">
<div style="display:flex;gap:4px;"><div style="width:8px;height:8px;background:#ff5f56;border-radius:50%;"></div><div style="width:8px;height:8px;background:#ffbd2e;border-radius:50%;"></div><div style="width:8px;height:8px;background:#27ca40;border-radius:50%;"></div></div>
<span style="font-size:8px;color:#808080;flex:1;text-align:center;">api.php</span>
<span style="font-size:8px;color:#808080;cursor:pointer;">📋</span>
</div>
<div style="padding:10px 12px;font-family:monospace;font-size:9px;line-height:1.8;">
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">1</span><span style="color:#c586c0;">use</span> <span style="color:#4ec9b0;">App\\Http\\Controllers</span>;</div>
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">2</span></div>
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">3</span><span style="color:#569cd6;">Route</span>::<span style="color:#dcdcaa;">get</span>(<span style="color:#ce9178;">'/orders'</span>, <span style="color:#569cd6;">function</span> () {</div>
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">4</span> <span style="color:#c586c0;">return</span> <span style="color:#4ec9b0;">Order</span>::<span style="color:#dcdcaa;">all</span>();</div>
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">5</span>});</div>
</div>
</div>`;
// 키보드 단축키
if (key.includes('단축키') && (key.includes('키보드') || key.includes('도움말'))) return `
<div style="width:100%;max-width:360px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);overflow:hidden;border:1px solid #e2e8f0;">
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:11px;font-weight:700;color:#1e293b;">⌨️ 키보드 단축키</span>
<span style="color:#94a3b8;font-size:12px;"></span>
</div>
<div style="padding:8px 14px;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:4px;">일반</div>
${[['⌘ K','검색 열기'],['⌘ N','새로 만들기'],['⌘ S','저장']].map(([k,d]) => `<div style="display:flex;justify-content:space-between;padding:3px 0;"><span style="font-size:9px;color:#475569;">${d}</span><kbd style="font-size:8px;background:#f8fafc;border:1px solid #e2e8f0;padding:1px 6px;border-radius:3px;color:#64748b;font-family:monospace;">${k}</kbd></div>`).join('')}
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin:6px 0 4px;">네비게이션</div>
${[['G → D','대시보드'],['G → O','수주 목록'],['?','단축키 도움말']].map(([k,d]) => `<div style="display:flex;justify-content:space-between;padding:3px 0;"><span style="font-size:9px;color:#475569;">${d}</span><kbd style="font-size:8px;background:#f8fafc;border:1px solid #e2e8f0;padding:1px 6px;border-radius:3px;color:#64748b;font-family:monospace;">${k}</kbd></div>`).join('')}
</div>
</div>`;
// 다크 모드
if (key.includes('다크') && key.includes('모드') || key.includes('다크모드')) return `
<div class="wf-wrap" style="padding:0;display:grid;grid-template-columns:1fr 1fr;min-height:160px;">
<div style="padding:12px;background:#fff;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
<span style="font-size:14px;">☀️</span>
<span style="font-size:9px;font-weight:600;color:#1e293b;">라이트</span>
</div>
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px;">
<div class="wf-bar md dark" style="height:8px;margin-bottom:4px;"></div>
<div class="wf-bar sm" style="height:6px;margin-bottom:6px;"></div>
<div style="height:30px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;"></div>
</div>
</div>
<div style="padding:12px;background:#0f172a;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
<span style="font-size:14px;">🌙</span>
<span style="font-size:9px;font-weight:600;color:#e2e8f0;">다크</span>
</div>
<div style="background:#1e293b;border:1px solid #334155;border-radius:6px;padding:8px;">
<div style="height:8px;width:60%;background:#475569;border-radius:4px;margin-bottom:4px;"></div>
<div style="height:6px;width:40%;background:#334155;border-radius:4px;margin-bottom:6px;"></div>
<div style="height:30px;background:#0f172a;border:1px solid #334155;border-radius:4px;"></div>
</div>
</div>
</div>`;
// 파일 매니저
if (key.includes('파일') && (key.includes('매니저') || key.includes('탐색기'))) return `
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
<div style="width:110px;border-right:1px solid #e2e8f0;padding:8px 4px;font-size:8px;">
<div style="padding:3px 6px;font-weight:600;color:#1e293b;">📁 전체 파일</div>
${['📂 견적서','📂 도면','📂 계약서','📂 사진'].map((f,i) => `<div style="padding:3px 6px 3px 14px;color:${i===1?'#3b82f6':'#64748b'};${i===1?'background:#eff6ff;border-radius:3px;font-weight:600;':''}">${f}</div>`).join('')}
</div>
<div style="flex:1;padding:8px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<span style="font-size:8px;color:#3b82f6;">전체 파일</span><span style="font-size:8px;color:#94a3b8;">/</span><span style="font-size:8px;font-weight:600;color:#1e293b;">도면</span>
<div style="flex:1;"></div>
<span style="font-size:8px;padding:2px 6px;background:#3b82f6;color:#fff;border-radius:3px;">업로드</span>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;">
${['A-101.dwg|📐','A-102.dwg|📐','제품사진.png|🖼️','시방서.pdf|📄','매뉴얼.pdf|📄','+ 새 폴더|📁'].map(f => {
const [n,i] = f.split('|');
return `<div style="border:1px solid ${n.includes('+')?'#cbd5e1':'#e2e8f0'};border-radius:6px;padding:8px;text-align:center;${n.includes('+')?'border-style:dashed;':''}">
<div style="font-size:18px;margin-bottom:2px;${n.includes('+')?'opacity:.4;':''}">${i}</div>
<div style="font-size:7px;color:${n.includes('+')?'#94a3b8':'#475569'};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${n}</div>
</div>`;
}).join('')}
</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 패턴 100종을 추가합니다. 계속하시겠습니까?')) 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단계 이상일 때 가장 유용.',
},
// ===== 로그인/인증 (auth) =====
{
type: 'pattern', title: '로그인 폼 (클래식)', category: 'auth', rating: 5,
tags: ['로그인', '인증', '폼', '보안'],
memo: 'GitHub, Google, Notion 등 거의 모든 서비스의 진입점. 심플하고 명확한 로그인 경험이 핵심.',
components: [
{ name: '로고 + 서비스명 (상단 중앙)', required: true },
{ name: '이메일/아이디 입력란', required: true },
{ name: '비밀번호 입력란 (보기 토글)', required: true },
{ name: '로그인 버튼 (풀 너비)', required: true },
{ name: '비밀번호 찾기 링크', required: true },
{ name: '자동 로그인 체크박스', required: false },
{ name: '회원가입 링크', required: false },
],
usedIn: ['GitHub', 'Google', 'Notion', 'SAM 로그인'],
guidelines: '폼은 화면 중앙 정렬, 너비 360~400px. 비밀번호 표시/숨기기 아이콘. Enter 키로 로그인 가능. 에러 메시지는 필드 하단에 빨간색.',
},
{
type: 'pattern', title: '소셜 로그인 / SSO', category: 'auth', rating: 4,
tags: ['소셜로그인', 'SSO', 'OAuth', '간편로그인'],
memo: 'Vercel, Figma, Notion 등 최신 SaaS. 소셜 계정으로 원클릭 로그인. 가입 허들을 극적으로 낮춤.',
components: [
{ name: '소셜 로그인 버튼 (Google, GitHub 등)', required: true },
{ name: '구분선 ("또는")', required: true },
{ name: '이메일 로그인 폼 (하단)', required: false },
{ name: '서비스 로고 + 환영 메시지', required: true },
{ name: '개인정보 동의 링크', required: true },
],
usedIn: ['Vercel', 'Figma', 'Notion', 'Linear'],
guidelines: 'Google 로그인 최상단 배치 (가장 많이 사용). 버튼에 각 서비스 로고 + 이름 표시. "Continue with Google" 패턴이 표준.',
},
{
type: 'pattern', title: '2단계 인증 (2FA)', category: 'auth', rating: 4,
tags: ['2FA', 'OTP', '보안', '인증코드'],
memo: 'GitHub, AWS, Stripe 등 보안이 중요한 서비스. 6자리 코드 입력으로 추가 인증.',
components: [
{ name: '안내 텍스트 (인증 방법 설명)', required: true },
{ name: '6자리 코드 입력란 (각 칸 분리)', required: true },
{ name: '확인 버튼', required: true },
{ name: '재전송 버튼 (카운트다운)', required: true },
{ name: '백업 코드로 인증 링크', required: false },
{ name: '자동 포커스 이동 (입력 시)', required: true },
],
usedIn: ['GitHub', 'AWS', 'Stripe', 'Google'],
guidelines: '각 숫자 칸은 40x48px, 중앙 정렬. 입력 시 자동으로 다음 칸 포커스. 붙여넣기(Ctrl+V) 시 자동 분배. 타이머 60초 후 재전송 가능.',
},
{
type: 'pattern', title: '비밀번호 재설정 플로우', category: 'auth', rating: 3,
tags: ['비밀번호', '재설정', '이메일', '복구'],
memo: 'GitHub, Google, Slack 등. 3단계 플로우: 이메일 입력 → 인증 메일 확인 → 새 비밀번호 설정.',
components: [
{ name: '이메일 입력 화면', required: true },
{ name: '이메일 발송 완료 안내 화면', required: true },
{ name: '새 비밀번호 설정 화면', required: true },
{ name: '비밀번호 강도 표시', required: false },
{ name: '완료 + 로그인 유도 화면', required: true },
],
usedIn: ['GitHub', 'Google', 'Slack', 'SAM 비밀번호 찾기'],
guidelines: '각 단계 명확한 안내 텍스트. 이메일 발송 후 "메일함 열기" 버튼 제공. 비밀번호 규칙 사전 안내. 토큰 만료 시 재시도 안내.',
},
{
type: 'pattern', title: '회원가입 폼', category: 'auth', rating: 4,
tags: ['회원가입', '가입', '온보딩', '폼'],
memo: 'Notion, Linear, Vercel 등. 최소 필드로 빠른 가입 후 프로필 완성은 나중에. 전환율이 핵심.',
components: [
{ name: '이름 입력란', required: true },
{ name: '이메일 입력란', required: true },
{ name: '비밀번호 입력란 (강도 표시)', required: true },
{ name: '가입 버튼 (풀 너비)', required: true },
{ name: '약관 동의 체크박스', required: true },
{ name: '소셜 가입 옵션', required: false },
{ name: '이미 계정이 있나요? 로그인 링크', required: true },
],
usedIn: ['Notion', 'Linear', 'Vercel', 'SAM 회원가입'],
guidelines: '필드 3~4개 이하 (이름, 이메일, 비밀번호). 비밀번호 강도 실시간 표시. 가입 버튼은 눈에 띄는 색상. 소셜 가입을 상단에 배치하면 전환율 향상.',
},
// ===== 보고서/인쇄 (report) =====
{
type: 'pattern', title: '인쇄용 보고서 레이아웃', category: 'report', rating: 4,
tags: ['인쇄', '보고서', 'A4', '프린트'],
memo: 'SAM, 전자세금계산서, 관공서 서식 등. A4 기준 인쇄 최적화. @media print 스타일 핵심.',
components: [
{ name: '회사 로고 + 문서 제목 (상단)', required: true },
{ name: '문서 번호 + 날짜 (우상단)', required: true },
{ name: '데이터 테이블 (테두리 인쇄)', required: true },
{ name: '합계/요약 영역 (하단)', required: true },
{ name: '서명란/도장 영역', required: false },
{ name: '페이지 번호 (푸터)', required: true },
{ name: '인쇄 버튼 (화면에서만 표시)', required: true },
],
usedIn: ['전자세금계산서', '관공서 서식', 'SAM 견적서/거래명세서'],
guidelines: 'A4 기준 210×297mm. @media print로 불필요 요소 숨기기. 테이블 테두리는 인쇄 시 선명하게. 페이지 나눔은 page-break-before/after.',
},
{
type: 'pattern', title: '인보이스/견적서 문서', category: 'report', rating: 4,
tags: ['인보이스', '견적서', '문서', '금액'],
memo: 'Stripe Invoice, FreshBooks, SAM 견적서 등. 발행자/수신자 정보 + 품목 테이블 + 합계의 정형화된 구조.',
components: [
{ name: '발행자 정보 (좌상단: 로고+주소)', required: true },
{ name: '수신자 정보 (우상단: 업체명+주소)', required: true },
{ name: '문서 번호/날짜/유효기간', required: true },
{ name: '품목 테이블 (품명, 수량, 단가, 금액)', required: true },
{ name: '소계/세금/합계 (우하단)', required: true },
{ name: '비고/조건 (하단)', required: false },
{ name: '인감/서명란', required: false },
],
usedIn: ['Stripe Invoice', 'FreshBooks', 'Wave', 'SAM 견적서/거래명세서'],
guidelines: '금액은 우측 정렬 + 천단위 콤마. 합계는 볼드 + 크게. 품목 테이블은 줄무늬 배경(zebra striping). PDF 다운로드 버튼 제공.',
},
{
type: 'pattern', title: '데이터 분석 리포트', category: 'report', rating: 4,
tags: ['분석', '리포트', '차트', 'KPI'],
memo: 'Google Analytics, Mixpanel, Amplitude 등. 다양한 차트와 데이터를 조합한 종합 분석 보고서.',
components: [
{ name: '기간 선택기 (상단)', required: true },
{ name: 'KPI 요약 카드 (핵심 지표 4~6개)', required: true },
{ name: '라인/바/파이 차트 (메인)', required: true },
{ name: '데이터 테이블 (상세)', required: true },
{ name: 'PDF/Excel 내보내기 버튼', required: true },
{ name: '비교 기간 토글 (전월/전년)', required: false },
],
usedIn: ['Google Analytics', 'Mixpanel', 'Amplitude', 'SAM 매출 분석'],
guidelines: '차트는 목적에 맞는 유형 선택 (추이→라인, 비교→바, 비율→파이). KPI 카드에 전기 대비 증감률 표시. 데이터 로딩 시 Skeleton UI.',
},
{
type: 'pattern', title: '일일/주간 업무 보고서', category: 'report', rating: 3,
tags: ['일일보고', '주간보고', '업무보고', '보고서'],
memo: '사내 업무 보고서 양식. 완료/진행/예정 업무를 구조화하여 상사에게 보고하는 형식.',
components: [
{ name: '보고 기간/작성일/작성자', required: true },
{ name: '완료 업무 섹션 (체크리스트)', required: true },
{ name: '진행 중 업무 섹션 (진행률)', required: true },
{ name: '예정 업무 섹션', required: true },
{ name: '이슈/건의 사항', required: false },
{ name: '첨부파일', required: false },
],
usedIn: ['사내 보고 시스템', 'Notion 일일보고', 'SAM 일일 스크럼'],
guidelines: '완료 항목은 취소선 또는 체크 표시. 진행 중은 퍼센트 바. 중요 이슈는 빨간 강조. 인쇄 시 A4 1~2페이지 이내.',
},
{
type: 'pattern', title: '대시보드 PDF 리포트', category: 'report', rating: 3,
tags: ['PDF', '대시보드', '리포트', '내보내기'],
memo: 'Stripe Report, HubSpot Report 등. 대시보드 데이터를 PDF로 내보내기 위한 인쇄 최적화 레이아웃.',
components: [
{ name: '리포트 표지 (로고+제목+기간)', required: true },
{ name: '요약 페이지 (핵심 KPI)', required: true },
{ name: '차트 페이지 (차트+설명)', required: true },
{ name: '상세 데이터 테이블', required: true },
{ name: '페이지 번호/날짜 (푸터)', required: true },
{ name: '목차 (3페이지 이상 시)', required: false },
],
usedIn: ['Stripe Report', 'HubSpot', 'Google Data Studio', 'SAM 월간 보고서'],
guidelines: 'A4 세로 또는 가로. 차트는 벡터(SVG) 또는 고해상도. 페이지 당 정보량 제한 (여백 충분히). 컬러 인쇄/흑백 인쇄 모두 고려.',
},
// ===== 대시보드 추가 =====
{
type: 'pattern', title: '위젯 대시보드 (커스터마이징)', category: 'dashboard', rating: 5,
tags: ['위젯', '대시보드', '드래그', '커스텀'],
memo: 'Notion Dashboard, Grafana, Datadog 등. 사용자가 위젯을 자유롭게 배치하고 크기를 조절하는 맞춤형 대시보드.',
components: [
{ name: '위젯 그리드 (드래그 이동)', required: true },
{ name: '위젯 리사이즈 (코너 드래그)', required: true },
{ name: '위젯 추가 버튼/패널', required: true },
{ name: '위젯 삭제/설정 (hover 메뉴)', required: true },
{ name: '레이아웃 저장/불러오기', required: true },
{ name: '위젯 유형: 차트, 숫자, 테이블, 목록', required: true },
],
usedIn: ['Grafana', 'Datadog', 'Notion', 'macOS 위젯'],
guidelines: '그리드 기반 배치 (12컬럼). 위젯 최소 크기 제한. 저장 시 레이아웃 JSON 직렬화. 기본 레이아웃 프리셋 제공.',
},
{
type: 'pattern', title: '실시간 모니터링 대시보드', category: 'dashboard', rating: 4,
tags: ['실시간', '모니터링', '라이브', '상태'],
memo: 'Datadog, New Relic, AWS CloudWatch 등. 서버/시스템 상태를 실시간으로 모니터링하는 대시보드.',
components: [
{ name: '실시간 차트 (자동 업데이트)', required: true },
{ name: '상태 인디케이터 (정상/경고/장애)', required: true },
{ name: '알림 피드 (최근 이벤트)', required: true },
{ name: '서비스별 상태 카드', required: true },
{ name: '업타임 퍼센트 표시', required: false },
{ name: '자동 새로고침 간격 설정', required: false },
],
usedIn: ['Datadog', 'New Relic', 'AWS CloudWatch', 'SAM 서버 모니터링'],
guidelines: '정상=초록, 경고=노랑, 장애=빨강 (신호등 패턴). 차트 30초~1분 자동 갱신. 장애 시 화면 상단 빨간 배너. 소리 알림 선택적.',
},
{
type: 'pattern', title: '멀티 차트 분석 대시보드', category: 'dashboard', rating: 4,
tags: ['차트', '분석', '시각화', '그래프'],
memo: 'Google Analytics, Tableau, Power BI 등. 다양한 차트 유형을 조합한 데이터 시각화 중심 대시보드.',
components: [
{ name: '라인 차트 (추이 분석)', required: true },
{ name: '바 차트 (비교 분석)', required: true },
{ name: '파이/도넛 차트 (비율 분석)', required: true },
{ name: '히트맵 (밀도 분석)', required: false },
{ name: '필터 바 (기간, 세그먼트)', required: true },
{ name: '차트 전환 옵션', required: false },
],
usedIn: ['Google Analytics', 'Tableau', 'Power BI', 'SAM 분석 대시보드'],
guidelines: '차트 간 일관된 색상 팔레트. 각 차트에 제목+범례. 데이터 포인트 hover 시 툴팁. 빈 데이터 시 안내 메시지.',
},
// ===== 목록 추가 =====
{
type: 'pattern', title: '무한 스크롤 피드', category: 'list', rating: 4,
tags: ['무한스크롤', '피드', '소셜', '스크롤'],
memo: 'Twitter/X, Instagram, LinkedIn 등 소셜 미디어. 스크롤하면 자동으로 다음 데이터를 로드하는 패턴.',
components: [
{ name: '피드 카드 (콘텐츠 + 메타정보)', required: true },
{ name: '자동 로딩 트리거 (스크롤 감지)', required: true },
{ name: '로딩 스피너/스켈레톤', required: true },
{ name: '새 글 알림 배너 ("새 글 N개")', required: false },
{ name: '맨 위로 가기 버튼', required: true },
{ name: '끝 도달 표시', required: true },
],
usedIn: ['Twitter/X', 'Instagram', 'LinkedIn', 'SAM 활동 로그'],
guidelines: 'IntersectionObserver로 스크롤 감지. 한 번에 20~30개 로드. 로딩 중 스켈레톤 3~4개 표시. 에러 시 재시도 버튼.',
},
{
type: 'pattern', title: '그룹/섹션 목록', category: 'list', rating: 3,
tags: ['그룹', '섹션', '목록', '분류'],
memo: 'iOS 설정, macOS Finder, Notion 등. 항목을 논리적 그룹으로 묶어 표시하는 목록 패턴.',
components: [
{ name: '그룹 헤더 (섹션 제목 + 카운트)', required: true },
{ name: '그룹 내 항목 목록', required: true },
{ name: '접기/펼치기 토글', required: true },
{ name: 'Sticky 그룹 헤더 (스크롤 시 고정)', required: false },
{ name: '전체 접기/펼치기 버튼', required: false },
],
usedIn: ['iOS 설정', 'macOS Finder', 'Notion', 'SAM 품목 분류 목록'],
guidelines: '그룹 헤더는 배경색으로 구분. 접힌 상태에서도 항목 수 표시. 스크롤 시 현재 그룹 헤더 sticky. 초기 상태: 첫 그룹만 펼침.',
},
{
type: 'pattern', title: '벌크 액션 바', category: 'list', rating: 4,
tags: ['벌크', '다중선택', '일괄처리', '액션바'],
memo: 'Gmail, GitHub, Jira 등. 여러 항목을 선택한 후 일괄 작업(삭제, 이동, 상태 변경)을 수행하는 패턴.',
components: [
{ name: '전체 선택 체크박스', required: true },
{ name: '선택 수 표시', required: true },
{ name: '벌크 액션 버튼 (삭제, 이동, 상태변경)', required: true },
{ name: '선택 해제 버튼', required: true },
{ name: '플로팅 액션 바 (하단 고정)', required: false },
{ name: '확인 다이얼로그 (위험 작업)', required: true },
],
usedIn: ['Gmail', 'GitHub', 'Jira', 'SAM 목록 화면'],
guidelines: '액션 바는 선택 시에만 표시 (애니메이션 슬라이드). 삭제 등 위험 작업은 확인 다이얼로그. 최대 선택 수 제한 안내.',
},
// ===== 상세/폼 추가 =====
{
type: 'pattern', title: '인라인 편집 테이블', category: 'form', rating: 4,
tags: ['인라인편집', '테이블', '스프레드시트', 'CRUD'],
memo: 'Airtable, Notion Table, Google Sheets 등. 행 클릭/더블클릭 시 셀을 직접 편집하는 스프레드시트형 패턴.',
components: [
{ name: '셀 클릭 → 편집 모드 전환', required: true },
{ name: '셀 유형별 에디터 (텍스트, 선택, 날짜)', required: true },
{ name: 'ESC 취소 / Enter 저장', required: true },
{ name: '행 추가 (하단 + 버튼)', required: true },
{ name: '행 삭제 (hover 메뉴)', required: true },
{ name: 'Tab 키로 다음 셀 이동', required: false },
],
usedIn: ['Airtable', 'Notion', 'Google Sheets', 'SAM BOM 테이블'],
guidelines: '편집 중인 셀은 파란 테두리 하이라이트. 변경 즉시 자동저장 (debounce 500ms). 셀 유형에 맞는 입력 UI (드롭다운, 날짜 피커).',
},
{
type: 'pattern', title: '리치 텍스트 에디터', category: 'form', rating: 4,
tags: ['에디터', '리치텍스트', 'WYSIWYG', '마크다운'],
memo: 'Notion, Google Docs, Medium 등. 서식 있는 텍스트를 편집하는 WYSIWYG 에디터.',
components: [
{ name: '서식 도구 모음 (볼드, 기울임, 밑줄)', required: true },
{ name: '제목 레벨 (H1~H3)', required: true },
{ name: '목록 (순서/비순서)', required: true },
{ name: '이미지/파일 삽입', required: false },
{ name: '코드 블록', required: false },
{ name: '링크 삽입', required: true },
{ name: '플로팅 툴바 (선택 시 표시)', required: false },
],
usedIn: ['Notion', 'Google Docs', 'Medium', 'SAM 게시판 에디터'],
guidelines: '플로팅 툴바 (텍스트 선택 시 나타남) 또는 상단 고정 툴바. Markdown 단축키 지원 (**볼드**, # 제목). 이미지 드래그 앤 드롭.',
},
{
type: 'pattern', title: '상세 정보 카드 (프로필)', category: 'form', rating: 3,
tags: ['상세', '프로필', '정보카드', '요약'],
memo: 'LinkedIn 프로필, GitHub 프로필, Salesforce Contact 등. 엔티티의 핵심 정보를 카드형으로 요약 표시.',
components: [
{ name: '프로필 이미지/아바타', required: true },
{ name: '이름 + 직함/역할', required: true },
{ name: '연락처 정보 (이메일, 전화)', required: true },
{ name: '액션 버튼 (편집, 메시지, 삭제)', required: true },
{ name: '태그/배지 (부서, 상태)', required: false },
{ name: '통계 (거래건수, 매출액 등)', required: false },
],
usedIn: ['LinkedIn', 'GitHub', 'Salesforce', 'SAM 거래처 상세'],
guidelines: '이미지 좌측 또는 상단 배치. 핵심 정보는 한눈에 파악 가능하게. 액션 버튼은 우상단. 통계는 숫자 + 라벨 조합.',
},
// ===== 모달/팝업 추가 =====
{
type: 'pattern', title: '확인/경고 다이얼로그', category: 'modal', rating: 4,
tags: ['확인', '경고', '다이얼로그', '삭제확인'],
memo: 'GitHub "Delete repository", Slack "Leave channel" 등. 위험한 작업 전 사용자에게 재확인을 요청하는 패턴.',
components: [
{ name: '경고 아이콘 (⚠️ 또는 빨간 방패)', required: true },
{ name: '제목 (명확한 액션 설명)', required: true },
{ name: '설명 텍스트 (영향 범위)', required: true },
{ name: '확인 입력 (리소스명 타이핑)', required: false },
{ name: '취소/확인 버튼 (확인은 빨간색)', required: true },
],
usedIn: ['GitHub', 'Slack', 'AWS', 'SAM 삭제 확인'],
guidelines: '삭제 버튼은 빨간색 (위험 인지). GitHub 패턴: 리소스명을 직접 타이핑해야 확인 가능. 취소가 기본 포커스 (실수 방지).',
},
{
type: 'pattern', title: '이미지 라이트박스/갤러리', category: 'modal', rating: 3,
tags: ['라이트박스', '갤러리', '이미지', '확대'],
memo: 'Medium, Dribbble, Instagram 등. 이미지 클릭 시 전체 화면으로 확대하여 상세히 볼 수 있는 패턴.',
components: [
{ name: '오버레이 배경 (어둡게)', required: true },
{ name: '이미지 (최대 크기, 중앙)', required: true },
{ name: '좌우 네비게이션 화살표', required: true },
{ name: '닫기 버튼 (우상단 또는 ESC)', required: true },
{ name: '이미지 카운터 (3/12)', required: false },
{ name: '확대/축소 컨트롤', required: false },
{ name: '썸네일 스트립 (하단)', required: false },
],
usedIn: ['Medium', 'Dribbble', 'Instagram', 'SAM 첨부 이미지 뷰어'],
guidelines: '배경 클릭 또는 ESC로 닫기. 좌우 화살표 키 네비게이션. 이미지 로딩 시 placeholder. 모바일: 스와이프 네비게이션.',
},
{
type: 'pattern', title: '알림 센터 (슬라이드 패널)', category: 'modal', rating: 4,
tags: ['알림', '패널', '슬라이드', '노티피케이션'],
memo: 'GitHub Notifications, Slack Activity, Linear Inbox 등. 화면 우측에서 슬라이드하여 알림 목록을 보여주는 패턴.',
components: [
{ name: '알림 아이콘 + 배지 (트리거)', required: true },
{ name: '슬라이드 패널 (우측)', required: true },
{ name: '알림 목록 (시간순)', required: true },
{ name: '읽음/안읽음 구분', required: true },
{ name: '모두 읽음 처리 버튼', required: true },
{ name: '알림 유형별 아이콘/색상', required: false },
{ name: '알림 클릭 → 해당 페이지 이동', required: true },
],
usedIn: ['GitHub', 'Slack', 'Linear', 'SAM 알림'],
guidelines: '패널 너비 360~400px. 안읽은 알림은 배경색 구분. 알림 그룹핑 (오늘/어제/이전). 빈 상태: "새 알림이 없습니다".',
},
// ===== 네비게이션 추가 =====
{
type: 'pattern', title: '메가 메뉴', category: 'navigation', rating: 3,
tags: ['메가메뉴', '드롭다운', '대형메뉴', 'GNB'],
memo: 'Amazon, Microsoft, Shopify 등 대규모 서비스. 메뉴 hover 시 전체 너비 드롭다운으로 하위 메뉴를 한눈에 표시.',
components: [
{ name: '상단 메뉴 바 (1차 메뉴)', required: true },
{ name: '메가 드롭다운 (전체 너비)', required: true },
{ name: '카테고리 컬럼 (2~4열)', required: true },
{ name: '아이콘/이미지 포함 항목', required: false },
{ name: '추천/하이라이트 영역', required: false },
],
usedIn: ['Amazon', 'Microsoft', 'Shopify', 'SAM 대형 메뉴 (검토용)'],
guidelines: '메뉴 진입 지연 200ms (실수 방지). 컬럼 간 명확한 구분. 현재 호버 메뉴 시각적 강조. 모바일에서는 아코디언으로 변환.',
},
{
type: 'pattern', title: '모바일 하단 네비게이션', category: 'navigation', rating: 4,
tags: ['모바일', '하단바', '탭바', '네비게이션'],
memo: 'Instagram, Twitter/X, YouTube 등 모바일 앱. 엄지 손가락이 닿기 쉬운 하단에 핵심 메뉴 배치.',
components: [
{ name: '하단 고정 바', required: true },
{ name: '메뉴 아이콘 + 라벨 (4~5개)', required: true },
{ name: '활성 탭 하이라이트', required: true },
{ name: '알림 배지 (숫자)', required: false },
{ name: '중앙 액션 버튼 (FAB)', required: false },
],
usedIn: ['Instagram', 'Twitter/X', 'YouTube', 'SAM 모바일 뷰'],
guidelines: '항목 4~5개 권장 (초과 시 "더보기"). 아이콘 24px + 라벨 10px. 활성 탭은 색상 + 아이콘 변화. Safe area 하단 여백 (iOS 노치 대응).',
},
// ===== 기타 추가 =====
{
type: 'pattern', title: '드래그 앤 드롭 정렬', category: 'etc', rating: 4,
tags: ['드래그', '정렬', '순서변경', '드래그앤드롭'],
memo: 'Trello, Todoist, Notion, macOS Reminders 등. 마우스로 항목을 끌어서 순서를 변경하는 인터랙션.',
components: [
{ name: '드래그 핸들 (⠿ 아이콘)', required: true },
{ name: '드래그 미리보기 (반투명 복제)', required: true },
{ name: '드롭 위치 인디케이터 (파란 선)', required: true },
{ name: '드래그 중 원래 위치 표시', required: true },
{ name: '키보드 접근성 (Alt+↑↓)', required: false },
],
usedIn: ['Trello', 'Todoist', 'Notion', 'SAM 메뉴 순서, BOM 순서'],
guidelines: '드래그 시작 지연 150ms (클릭과 구분). 드래그 중 오토 스크롤 (목록 끝 근처). 드롭 후 애니메이션. 모바일: 길게 누르기.',
},
{
type: 'pattern', title: '스켈레톤 로딩', category: 'etc', rating: 4,
tags: ['스켈레톤', '로딩', '플레이스홀더', 'UX'],
memo: 'Facebook, LinkedIn, YouTube, Notion 등. 데이터 로딩 중 콘텐츠 구조를 미리 보여주어 체감 속도를 높이는 패턴.',
components: [
{ name: '콘텐츠 형태의 회색 플레이스홀더', required: true },
{ name: '펄스/웨이브 애니메이션', required: true },
{ name: '실제 레이아웃과 동일한 구조', required: true },
{ name: '텍스트 줄 (다양한 너비)', required: true },
{ name: '이미지 영역 (회색 사각형)', required: true },
{ name: '아바타 (회색 원형)', required: false },
],
usedIn: ['Facebook', 'LinkedIn', 'YouTube', 'SAM 목록/상세 로딩'],
guidelines: '실제 UI와 동일한 구조/크기. 텍스트 줄은 60~80% 너비 랜덤. 애니메이션은 좌→우 웨이브. 로딩 1초 미만이면 스켈레톤 불필요.',
},
{
type: 'pattern', title: '알림 배지 시스템', category: 'etc', rating: 3,
tags: ['배지', '알림', '카운터', '인디케이터'],
memo: 'Gmail, Slack, iOS 앱 아이콘 등. 읽지 않은 항목 수나 새 알림을 숫자/점으로 표시하는 패턴.',
components: [
{ name: '숫자 배지 (빨간 원 + 숫자)', required: true },
{ name: '점 배지 (숫자 없이 존재만 표시)', required: true },
{ name: '아이콘 위 배치 (우상단)', required: true },
{ name: '99+ 처리 (큰 숫자)', required: true },
{ name: '애니메이션 (새 알림 시 바운스)', required: false },
],
usedIn: ['Gmail', 'Slack', 'iOS', 'SAM 사이드바 메뉴'],
guidelines: '배지 최소 크기 18px. 숫자 1자리: 원형, 2자리 이상: pill 형태. 99+로 표기 (3자리 이상). 색상: 빨강(긴급), 파랑(정보), 회색(비활성).',
},
{
type: 'pattern', title: '데이터 시각화 차트 컴포넌트', category: 'dashboard', rating: 4,
tags: ['차트', '그래프', '시각화', '데이터'],
memo: 'Chart.js, D3.js, Recharts 등 차트 라이브러리를 활용한 데이터 시각화 컴포넌트 패턴.',
components: [
{ name: '차트 제목 + 범례', required: true },
{ name: '차트 영역 (라인/바/파이/도넛)', required: true },
{ name: '툴팁 (데이터 포인트 hover)', required: true },
{ name: '축 라벨 (X/Y축)', required: true },
{ name: '기간 선택 토글', required: false },
{ name: '데이터 없음 표시', required: true },
],
usedIn: ['Stripe', 'Vercel', 'GitHub Insights', 'SAM 차트 컴포넌트'],
guidelines: '색상 6~8개 팔레트 고정. 접근성: 색맹 대응 패턴 구분. 반응형: 작은 화면에서 범례 하단 이동. 로딩 시 차트 영역 스켈레톤.',
},
{
type: 'pattern', title: '다단계 드롭다운 메뉴', category: 'navigation', rating: 3,
tags: ['드롭다운', '계층', '메뉴', '셀렉트'],
memo: 'macOS 메뉴, VS Code 우클릭, Figma 메뉴 등. 메뉴 항목에 서브메뉴가 있는 계층형 드롭다운.',
components: [
{ name: '1단계 메뉴 (클릭/hover 트리거)', required: true },
{ name: '2단계 서브메뉴 (우측 확장)', required: true },
{ name: '구분선 (그룹 분리)', required: true },
{ name: '단축키 표시 (우측)', required: false },
{ name: '체크 마크 (선택 상태)', required: false },
{ name: '비활성 항목 (회색)', required: true },
],
usedIn: ['macOS', 'VS Code', 'Figma', 'SAM 우클릭 메뉴'],
guidelines: '서브메뉴 전환 지연 200ms. 현재 활성 항목 배경색 하이라이트. 메뉴 외부 클릭 시 닫기. 키보드: 화살표 키 네비게이션.',
},
{
type: 'pattern', title: '날짜/기간 선택기 (Date Picker)', category: 'modal', rating: 4,
tags: ['날짜', '기간', '달력', '피커'],
memo: 'Airbnb, Booking.com, Google Flights 등. 단일 날짜 또는 시작~종료 기간을 선택하는 UI.',
components: [
{ name: '달력 그리드 (월간)', required: true },
{ name: '이전/다음 월 네비게이션', required: true },
{ name: '기간 선택 (시작~종료 하이라이트)', required: false },
{ name: '오늘 버튼 (빠른 이동)', required: true },
{ name: '프리셋 (오늘, 이번 주, 이번 달)', required: false },
{ name: '시간 선택 (선택적)', required: false },
],
usedIn: ['Airbnb', 'Booking.com', 'Google Flights', 'SAM 기간 필터'],
guidelines: '오늘 날짜 강조. 선택 불가 날짜 회색 처리. 기간 선택 시 범위 배경색. 모바일: 전체 화면 달력. 키보드: 화살표 키 이동.',
},
// ===== 추가 50종 (51~100) =====
// ===== 대시보드 추가 (5) =====
{
type: 'pattern', title: '게이지/미터 대시보드', category: 'dashboard', rating: 4,
tags: ['게이지', '미터', '진행률', '속도계'],
memo: 'Grafana, Datadog, 자동차 계기판 UI. 원형 게이지로 목표 달성률이나 시스템 사용률을 직관적으로 표시.',
components: [
{ name: '원형 게이지 (반원 또는 전원)', required: true },
{ name: '현재 값 (중앙 큰 텍스트)', required: true },
{ name: '목표/한계 값 표시', required: true },
{ name: '색상 구간 (초록/노랑/빨강)', required: true },
{ name: '라벨 (측정 항목명)', required: true },
],
usedIn: ['Grafana', 'Datadog', 'Google PageSpeed', 'SAM 생산 달성률'],
guidelines: '0~100% 범위. 초록(0~60%), 노랑(60~80%), 빨강(80~100%). 애니메이션 전환. 숫자는 게이지 중앙 크게 표시.',
},
{
type: 'pattern', title: '히트맵 캘린더 (활동 기록)', category: 'dashboard', rating: 4,
tags: ['히트맵', '캘린더', '활동', '잔디'],
memo: 'GitHub Contribution, Wakatime 등. 일별 활동량을 색상 농도로 표현하여 장기 패턴을 한눈에 파악.',
components: [
{ name: '일별 셀 그리드 (52주 × 7일)', required: true },
{ name: '색상 농도 스케일 (연→진)', required: true },
{ name: '요일 라벨 (좌측)', required: true },
{ name: '월 라벨 (상단)', required: true },
{ name: '셀 hover 시 상세 툴팁', required: true },
],
usedIn: ['GitHub', 'Wakatime', 'Habitica', 'SAM 출근 기록'],
guidelines: '5단계 색상 (없음/연/중/진/최대). 셀 크기 10~12px. 오늘 날짜 테두리 강조. 빈 날짜는 가장 연한 색.',
},
{
type: 'pattern', title: '퍼널/전환율 차트', category: 'dashboard', rating: 4,
tags: ['퍼널', '전환율', '세일즈', '단계'],
memo: 'HubSpot, Salesforce, Mixpanel 등. 단계별 전환율을 시각화하여 이탈 구간을 파악하는 세일즈/마케팅 핵심 차트.',
components: [
{ name: '단계별 사다리꼴 바', required: true },
{ name: '각 단계 수치 + 전환율 (%)', required: true },
{ name: '단계 간 이탈률 표시', required: true },
{ name: '단계 라벨', required: true },
{ name: '기간 필터', required: false },
],
usedIn: ['HubSpot', 'Salesforce', 'Mixpanel', 'SAM 수주 파이프라인'],
guidelines: '최상단이 가장 넓고 아래로 좁아지는 형태. 전환율은 단계 사이에 표시. 이탈률이 큰 구간 빨간 강조.',
},
{
type: 'pattern', title: '비교 대시보드 (전월 vs 당월)', category: 'dashboard', rating: 3,
tags: ['비교', '전월', '당월', '증감'],
memo: 'Google Analytics 비교 뷰, Stripe 비교 등. 두 기간의 데이터를 나란히 비교하여 성과 변화를 분석.',
components: [
{ name: '기간 A / 기간 B 선택기', required: true },
{ name: '병렬 KPI 카드 (A vs B)', required: true },
{ name: '증감률 화살표 + 색상', required: true },
{ name: '겹친 라인 차트 (비교)', required: true },
{ name: '차이 요약 테이블', required: false },
],
usedIn: ['Google Analytics', 'Stripe', 'Shopify', 'SAM 월간 비교'],
guidelines: '기간 A는 실선, B는 점선. 증가=초록, 감소=빨강. 카드에서 양쪽 숫자 나란히 표시. 차이 절대값 + 퍼센트 동시 표시.',
},
{
type: 'pattern', title: '지도 기반 데이터 시각화', category: 'dashboard', rating: 3,
tags: ['지도', '위치', '지리', '분포'],
memo: 'Google Maps, Mapbox, Tableau 지도 등. 지역별 데이터 분포를 지도 위에 시각화.',
components: [
{ name: '지도 배경 (벡터/래스터)', required: true },
{ name: '데이터 마커/핀', required: true },
{ name: '클러스터링 (밀집 지역)', required: false },
{ name: '범례 (색상/크기 의미)', required: true },
{ name: '지역 hover 상세 정보', required: true },
],
usedIn: ['Google Maps', 'Tableau', 'Uber', 'SAM 거래처 분포'],
guidelines: '마커는 데이터 크기에 비례. 밀집 시 클러스터 자동 그룹핑. 지역 클릭 시 드릴다운. 줌 레벨별 상세도 조절.',
},
// ===== 목록 추가 (5) =====
{
type: 'pattern', title: '마스터-디테일 레이아웃', category: 'list', rating: 5,
tags: ['마스터', '디테일', '분할', '목록상세'],
memo: 'Apple Mail, Outlook, Notion 등. 좌측 목록 + 우측 상세의 2패널 구조. ERP에서 가장 빈번히 사용.',
components: [
{ name: '좌측 목록 패널 (검색 + 스크롤)', required: true },
{ name: '우측 상세 패널', required: true },
{ name: '목록 항목 선택 하이라이트', required: true },
{ name: '리사이즈 핸들 (패널 너비 조절)', required: false },
{ name: '상세 패널 탭 (정보/이력/첨부)', required: false },
{ name: '빈 상태 (선택 안 됨)', required: true },
],
usedIn: ['Apple Mail', 'Outlook', 'Notion', 'SAM 수주/거래처 상세'],
guidelines: '좌측 300~400px 고정, 우측 flex:1. 목록에서 현재 선택은 파란 배경. 모바일: 스택 레이아웃 (목록→상세 전환).',
},
{
type: 'pattern', title: '타일/썸네일 뷰', category: 'list', rating: 3,
tags: ['타일', '썸네일', '갤러리', '그리드뷰'],
memo: 'Pinterest, Dribbble, macOS Finder 등. 이미지 중심 데이터를 그리드 타일로 표시하는 시각적 목록.',
components: [
{ name: '이미지 썸네일 (메인)', required: true },
{ name: '타이틀 + 메타정보', required: true },
{ name: 'Masonry/균등 그리드 레이아웃', required: true },
{ name: '뷰 모드 전환 (타일/목록)', required: true },
{ name: '정렬 옵션', required: false },
],
usedIn: ['Pinterest', 'Dribbble', 'macOS Finder', 'SAM 도면/이미지 관리'],
guidelines: '타일 최소 200px, 최대 4열. 이미지 비율 유지 (object-fit: cover). hover 시 오버레이 액션. lazy loading 적용.',
},
{
type: 'pattern', title: '피벗 테이블', category: 'list', rating: 4,
tags: ['피벗', '크로스탭', '집계', '엑셀'],
memo: 'Excel 피벗, Google Sheets, Tableau 등. 행/열 교차 집계로 다차원 데이터를 분석하는 테이블.',
components: [
{ name: '행 헤더 (그룹핑 기준)', required: true },
{ name: '열 헤더 (분석 차원)', required: true },
{ name: '데이터 셀 (집계 값)', required: true },
{ name: '행/열 합계', required: true },
{ name: '필드 드래그 앤 드롭 (행↔열)', required: false },
{ name: '집계 함수 선택 (합계/평균/카운트)', required: false },
],
usedIn: ['Excel', 'Google Sheets', 'Tableau', 'SAM 매출 분석'],
guidelines: '행 그룹 접기/펼치기. 합계 행은 볼드 + 배경색. 금액은 천단위 콤마 우측 정렬. 큰 데이터는 가상 스크롤.',
},
{
type: 'pattern', title: '수평 타임라인', category: 'list', rating: 3,
tags: ['타임라인', '수평', '이력', '연대기'],
memo: 'LinkedIn Experience, 프로젝트 로드맵, 제품 릴리즈 히스토리 등. 시간순 이벤트를 가로로 나열.',
components: [
{ name: '수평 축 (시간선)', required: true },
{ name: '이벤트 노드 (점/아이콘)', required: true },
{ name: '이벤트 카드 (상/하 교대 배치)', required: true },
{ name: '스크롤/드래그 네비게이션', required: true },
{ name: '현재 시점 표시', required: false },
],
usedIn: ['LinkedIn', 'Jira Roadmap', 'Product Hunt', 'SAM 프로젝트 이력'],
guidelines: '카드를 상/하 교대로 배치하여 가독성 확보. 현재 시점 빨간 선. 이벤트 노드 클릭 시 상세 팝업. 좌우 스크롤 또는 드래그.',
},
{
type: 'pattern', title: '트리 테이블 (계층형 데이터)', category: 'list', rating: 4,
tags: ['트리', '계층', '테이블', '접기펼치기'],
memo: 'Jira 이슈 트리, BOM 구조, 파일 탐색기 등. 테이블 행 안에서 부모-자식 관계를 트리로 표현.',
components: [
{ name: '접기/펼치기 화살표 (행 좌측)', required: true },
{ name: '들여쓰기 레벨 (depth 표현)', required: true },
{ name: '테이블 헤더 (일반 컬럼)', required: true },
{ name: '부모 행 볼드/배경색 구분', required: false },
{ name: '전체 펼치기/접기 버튼', required: true },
],
usedIn: ['Jira', 'SAM BOM 트리', 'Windows Explorer', 'macOS Finder'],
guidelines: '레벨당 들여쓰기 16~20px. 리프 노드는 화살표 없음. 접힌 상태에서 자식 수 표시. Shift+클릭으로 재귀 펼치기.',
},
// ===== 폼 추가 (8) =====
{
type: 'pattern', title: '멀티 스텝 폼 (위자드)', category: 'form', rating: 5,
tags: ['멀티스텝', '위자드', '단계', '폼'],
memo: 'Typeform, Stripe Checkout, HubSpot 등. 복잡한 폼을 여러 단계로 나눠 사용자 부담을 줄이는 패턴.',
components: [
{ name: '진행 바/스텝 인디케이터', required: true },
{ name: '현재 단계 폼 필드', required: true },
{ name: '이전/다음 버튼', required: true },
{ name: '단계별 유효성 검사', required: true },
{ name: '요약/확인 단계 (마지막)', required: true },
{ name: '임시 저장 (중간 이탈 방지)', required: false },
],
usedIn: ['Typeform', 'Stripe Checkout', 'HubSpot', 'SAM 견적 등록'],
guidelines: '3~5단계 권장. 각 단계 필드 3~5개 이내. 완료 단계에 확인 아이콘. 진행 바에 퍼센트 또는 단계 번호 표시.',
},
{
type: 'pattern', title: '주소 검색/입력 폼', category: 'form', rating: 3,
tags: ['주소', '검색', '우편번호', 'API'],
memo: '다음 주소 API, Google Places, 네이버 지도 등. 우편번호 검색 후 상세주소를 입력하는 한국형 주소 입력.',
components: [
{ name: '우편번호 검색 버튼', required: true },
{ name: '주소 검색 모달/팝업', required: true },
{ name: '기본 주소 (자동 채워짐)', required: true },
{ name: '상세 주소 입력란', required: true },
{ name: '검색 결과 목록', required: true },
],
usedIn: ['카카오 주소 API', '네이버 주소', 'SAM 거래처 주소'],
guidelines: '검색창에 동/도로명/건물명 입력. 결과 클릭 시 기본주소 자동 채움 + 상세주소 포커스. 모바일: 전체 화면 검색.',
},
{
type: 'pattern', title: '태그/칩 입력', category: 'form', rating: 4,
tags: ['태그', '칩', '다중선택', '입력'],
memo: 'Gmail 수신자, Notion 태그, Jira 라벨 등. 여러 항목을 태그 형태로 추가/삭제하는 입력 UI.',
components: [
{ name: '입력 필드 (텍스트 입력)', required: true },
{ name: '태그 칩 (X 삭제 버튼)', required: true },
{ name: '자동완성 드롭다운', required: true },
{ name: 'Enter/콤마로 추가', required: true },
{ name: 'Backspace로 마지막 태그 삭제', required: true },
{ name: '드래그로 순서 변경', required: false },
],
usedIn: ['Gmail', 'Notion', 'Jira', 'SAM 품목 태그'],
guidelines: '태그 칩 높이 24px, pill 형태. 입력 필드와 태그가 같은 줄에 배치. 중복 태그 방지 (이미 있으면 깜빡임). 최대 개수 제한 안내.',
},
{
type: 'pattern', title: '슬라이더/레인지 입력', category: 'form', rating: 3,
tags: ['슬라이더', '레인지', '범위', '조절'],
memo: 'Airbnb 가격 필터, YouTube 볼륨, Figma 투명도 등. 드래그로 수치를 조절하는 입력 컨트롤.',
components: [
{ name: '트랙 바 (가로 선)', required: true },
{ name: '핸들/썸 (드래그 포인트)', required: true },
{ name: '현재 값 표시 (핸들 위 또는 옆)', required: true },
{ name: '최소/최대 라벨', required: true },
{ name: '범위 선택 (핸들 2개)', required: false },
{ name: '수치 직접 입력 (옆 필드)', required: false },
],
usedIn: ['Airbnb', 'YouTube', 'Figma', 'SAM 가격 필터'],
guidelines: '핸들 크기 최소 20px (터치 영역 44px). 드래그 중 값 실시간 업데이트. 스텝 단위 설정 가능. 키보드: 좌우 화살표로 미세 조정.',
},
{
type: 'pattern', title: '토글 설정 패널', category: 'form', rating: 3,
tags: ['토글', '스위치', '설정', '온오프'],
memo: 'iOS 설정, Android 설정, Notion 설정 등. 각 옵션을 토글 스위치로 켜고 끄는 설정 패널.',
components: [
{ name: '토글 스위치 (ON/OFF)', required: true },
{ name: '옵션 라벨 + 설명', required: true },
{ name: '섹션 그룹핑', required: true },
{ name: '위험한 설정 빨간 강조', required: false },
{ name: '변경 시 즉시 저장 / 저장 버튼', required: true },
],
usedIn: ['iOS 설정', 'Android 설정', 'Notion', 'SAM 알림 설정'],
guidelines: '토글 너비 44px, 높이 24px. ON=파랑/초록, OFF=회색. 라벨 좌측, 토글 우측 정렬. 변경 사항 즉시 저장 또는 하단 저장 버튼.',
},
{
type: 'pattern', title: '서명 패드 (전자 서명)', category: 'form', rating: 3,
tags: ['서명', '전자서명', '캔버스', '필기'],
memo: 'DocuSign, Adobe Sign, SAM 전자결재 등. 마우스/터치로 직접 서명을 작성하는 UI.',
components: [
{ name: '캔버스 영역 (서명 작성)', required: true },
{ name: '지우기 버튼', required: true },
{ name: '펜 굵기/색상 옵션', required: false },
{ name: '서명 완료/확인 버튼', required: true },
{ name: '서명 이미지 미리보기', required: false },
{ name: '텍스트 서명 옵션 (타이핑)', required: false },
],
usedIn: ['DocuSign', 'Adobe Sign', 'HelloSign', 'SAM 전자결재 서명'],
guidelines: '캔버스 배경 연한 줄무늬. 펜 압력 감지 (가능 시). 서명 이미지 PNG/SVG로 저장. 모바일: 가로 모드 권장.',
},
{
type: 'pattern', title: '컬러 피커', category: 'form', rating: 3,
tags: ['컬러', '색상', '피커', '팔레트'],
memo: 'Figma, Photoshop, Notion 등 디자인 도구. 색상을 시각적으로 선택하는 UI 컴포넌트.',
components: [
{ name: '색상 영역 (Saturation/Brightness)', required: true },
{ name: '색상 바 (Hue 슬라이더)', required: true },
{ name: '투명도 슬라이더 (Alpha)', required: false },
{ name: 'HEX/RGB/HSL 입력', required: true },
{ name: '최근 사용 색상', required: false },
{ name: '프리셋 팔레트', required: false },
],
usedIn: ['Figma', 'Photoshop', 'VS Code', 'SAM 테마 설정'],
guidelines: '미리보기 원형 또는 사각형. HEX 6자리 입력. 스포이드 도구 (브라우저 지원 시). 자주 사용 색상 저장.',
},
{
type: 'pattern', title: '평점/별점 입력', category: 'form', rating: 3,
tags: ['별점', '평점', '리뷰', '평가'],
memo: 'Amazon, Google Maps, App Store 등. 별 아이콘을 클릭하여 점수를 매기는 UI.',
components: [
{ name: '별 아이콘 5개 (클릭 가능)', required: true },
{ name: 'hover 시 미리보기', required: true },
{ name: '반별 (0.5점 단위) 지원', required: false },
{ name: '숫자 표시 (4.5/5.0)', required: true },
{ name: '리뷰 텍스트 영역', required: false },
],
usedIn: ['Amazon', 'Google Maps', 'App Store', 'SAM 공급업체 평가'],
guidelines: '별 크기 24~32px. 채워진 별=노랑, 빈 별=회색. hover 시 해당 별까지 채워짐. 읽기 전용 모드: 평균 + 리뷰 수.',
},
// ===== 모달/팝업 추가 (5) =====
{
type: 'pattern', title: '바텀 시트 (모바일)', category: 'modal', rating: 4,
tags: ['바텀시트', '모바일', '시트', '슬라이드업'],
memo: 'Google Maps, Apple Maps, Uber 등 모바일 앱. 화면 하단에서 올라오는 시트형 모달.',
components: [
{ name: '드래그 핸들 (상단 바)', required: true },
{ name: '시트 콘텐츠', required: true },
{ name: '반쯤 열린 / 완전히 열린 상태', required: true },
{ name: '배경 딤 (오버레이)', required: true },
{ name: '아래로 스와이프 닫기', required: true },
],
usedIn: ['Google Maps', 'Apple Maps', 'Uber', 'SAM 모바일 필터'],
guidelines: '핸들 바 너비 40px, 높이 4px, 중앙 배치. 3단계 높이 (peek/half/full). 스와이프 속도에 따라 닫기 판단. 배경 탭 시 닫기.',
},
{
type: 'pattern', title: '컨텍스트 메뉴 (우클릭)', category: 'modal', rating: 3,
tags: ['컨텍스트', '우클릭', '메뉴', '팝업'],
memo: 'VS Code, Figma, macOS 등. 마우스 우클릭 위치에 나타나는 상황별 액션 메뉴.',
components: [
{ name: '메뉴 항목 목록', required: true },
{ name: '키보드 단축키 표시', required: true },
{ name: '구분선 (그룹 분리)', required: true },
{ name: '아이콘 (항목 좌측)', required: false },
{ name: '비활성 항목 (회색)', required: true },
{ name: '서브메뉴 (우측 화살표)', required: false },
],
usedIn: ['VS Code', 'Figma', 'macOS', 'SAM 테이블 우클릭'],
guidelines: '마우스 위치에 생성 (화면 밖으로 나가지 않게 조정). 외부 클릭 시 닫기. 메뉴 항목 높이 28~32px. 위험 항목 빨간색.',
},
{
type: 'pattern', title: '슬라이드오버 상세 패널', category: 'modal', rating: 4,
tags: ['슬라이드오버', '패널', '사이드', '상세'],
memo: 'AWS Console, Jira 상세, Salesforce Record 등. 우측에서 슬라이드하여 상세 정보를 보여주는 패널.',
components: [
{ name: '우측 슬라이드 패널 (320~480px)', required: true },
{ name: '패널 헤더 (제목 + 닫기)', required: true },
{ name: '콘텐츠 영역 (스크롤)', required: true },
{ name: '배경 딤 (반투명)', required: true },
{ name: '하단 액션 버튼 (고정)', required: false },
],
usedIn: ['AWS Console', 'Jira', 'Salesforce', 'SAM 수주 상세'],
guidelines: '패널 너비 400~500px. 뒤 페이지는 어둡게 + 클릭 가능(닫기). 슬라이드 애니메이션 200~300ms. ESC로 닫기.',
},
{
type: 'pattern', title: '쿠키/개인정보 동의 배너', category: 'modal', rating: 3,
tags: ['쿠키', '동의', 'GDPR', '배너'],
memo: 'EU GDPR 대응. 거의 모든 웹사이트에 필수. 쿠키 사용 동의를 받는 하단 배너 또는 모달.',
components: [
{ name: '안내 텍스트 (쿠키 사용 목적)', required: true },
{ name: '동의/거부 버튼', required: true },
{ name: '상세 설정 링크', required: true },
{ name: '개인정보처리방침 링크', required: true },
{ name: '카테고리별 토글 (필수/분석/마케팅)', required: false },
],
usedIn: ['EU 웹사이트 전체', 'Vercel', 'Notion', 'SAM (향후 적용)'],
guidelines: '하단 고정 또는 중앙 모달. "모두 동의" 버튼 강조. 필수 쿠키는 비활성화 불가 (회색 토글). 닫기 버튼 없이 선택 강제.',
},
{
type: 'pattern', title: '풀스크린 모달 (이머시브)', category: 'modal', rating: 3,
tags: ['풀스크린', '전체화면', '모달', '이머시브'],
memo: 'Medium 글쓰기, Notion 전체화면, Google Docs 등. 전체 화면을 차지하는 몰입형 모달.',
components: [
{ name: '전체 화면 오버레이', required: true },
{ name: '상단 툴바 (닫기 + 액션)', required: true },
{ name: '중앙 콘텐츠 영역 (최대 너비 제한)', required: true },
{ name: '닫기 버튼 (좌상단 또는 우상단)', required: true },
{ name: '저장/발행 버튼', required: false },
],
usedIn: ['Medium', 'Notion', 'Google Docs', 'SAM 문서 편집'],
guidelines: '배경 완전 흰색 또는 단색. 콘텐츠 최대 너비 720~800px (가독성). ESC로 닫기 + 저장 안 됨 확인. 애니메이션: fade-in.',
},
// ===== 네비게이션 추가 (5) =====
{
type: 'pattern', title: '탑 네비게이션 바 + 검색', category: 'navigation', rating: 5,
tags: ['탑바', 'GNB', '검색', '헤더'],
memo: 'GitHub, Notion, AWS Console 등. 상단 고정 네비게이션 바. 로고 + 메뉴 + 검색 + 프로필의 표준 구조.',
components: [
{ name: '로고 (좌측)', required: true },
{ name: '메뉴 항목 (중앙 또는 좌측)', required: true },
{ name: '검색 바 (중앙)', required: true },
{ name: '알림 아이콘 + 배지', required: false },
{ name: '프로필 아바타 + 드롭다운', required: true },
],
usedIn: ['GitHub', 'Notion', 'AWS', 'SAM 상단 헤더'],
guidelines: '높이 48~64px. position: sticky. 스크롤 시 그림자 추가. 검색: Ctrl+K 단축키. 프로필 클릭 시 드롭다운 메뉴.',
},
{
type: 'pattern', title: '앵커/스크롤 스파이 네비게이션', category: 'navigation', rating: 3,
tags: ['앵커', '스크롤스파이', '목차', '사이드'],
memo: 'Bootstrap Docs, MDN, Tailwind Docs 등. 긴 문서에서 현재 섹션을 추적하는 사이드 목차.',
components: [
{ name: '섹션 링크 목록 (사이드)', required: true },
{ name: '현재 섹션 하이라이트', required: true },
{ name: '스크롤 연동 (자동 추적)', required: true },
{ name: '클릭 시 스무스 스크롤', required: true },
{ name: '들여쓰기 (서브섹션)', required: false },
],
usedIn: ['Bootstrap Docs', 'MDN', 'Tailwind CSS', 'SAM 가이드 페이지'],
guidelines: '사이드 고정 (position: sticky). IntersectionObserver로 현재 섹션 감지. 현재 섹션 파란색 + 좌측 바. 모바일: 상단 드롭다운.',
},
{
type: 'pattern', title: '페이지네이션 패턴', category: 'navigation', rating: 4,
tags: ['페이지네이션', '페이지', '이동', '넘기기'],
memo: 'Google 검색, GitHub Issues 등. 대량 데이터를 페이지 단위로 나누어 탐색하는 UI.',
components: [
{ name: '이전/다음 버튼', required: true },
{ name: '페이지 번호 (1, 2, 3...)', required: true },
{ name: '말줄임 (1 ... 5 6 7 ... 20)', required: true },
{ name: '첫 페이지/마지막 페이지', required: true },
{ name: '페이지당 표시 수 선택', required: false },
{ name: '총 결과 수 표시', required: true },
],
usedIn: ['Google 검색', 'GitHub', 'AWS Console', 'SAM 목록 페이지'],
guidelines: '현재 페이지 파란 배경. 표시 페이지 5~7개. 양 끝에 말줄임(...). 첫/마지막 페이지는 항상 표시. 모바일: 이전/다음만.',
},
{
type: 'pattern', title: '플로팅 액션 버튼 (FAB)', category: 'navigation', rating: 3,
tags: ['FAB', '플로팅', '액션', '버튼'],
memo: 'Material Design, Gmail, Google Docs 등. 화면 하단에 떠있는 주요 액션 버튼.',
components: [
{ name: '원형 버튼 (고정 위치)', required: true },
{ name: '아이콘 (+ 기호)', required: true },
{ name: '확장 메뉴 (클릭 시 다중 액션)', required: false },
{ name: '스크롤 시 축소/숨기기', required: false },
{ name: '툴팁 라벨', required: false },
],
usedIn: ['Gmail', 'Google Docs', 'Material Design', 'SAM 모바일 빠른 등록'],
guidelines: '우하단 고정 (right: 24px, bottom: 24px). 크기 56px (mini: 40px). 그림자 필수. 확장 시 위로 액션 아이콘 나열.',
},
{
type: 'pattern', title: '사이드 탭 (세로 탭)', category: 'navigation', rating: 3,
tags: ['세로탭', '사이드탭', '수직탭', '패널'],
memo: 'VS Code 사이드바, Slack 좌측, Notion 좌측 등. 좌측에 세로로 배치된 탭으로 콘텐츠를 전환.',
components: [
{ name: '세로 탭 목록 (좌측)', required: true },
{ name: '활성 탭 하이라이트', required: true },
{ name: '아이콘 + 라벨 (선택적)', required: true },
{ name: '콘텐츠 패널 (우측)', required: true },
{ name: '아이콘만 모드 (축소)', required: false },
],
usedIn: ['VS Code', 'Slack', 'Notion', 'SAM 설정 페이지'],
guidelines: '탭 너비: 확장 200px, 축소 48px. 활성 탭: 좌측 파란 바 + 배경색. 아이콘: 24px, 라벨: 12~14px. 키보드: ↑↓ 탭 전환.',
},
// ===== 인증 추가 (3) =====
{
type: 'pattern', title: '역할/권한 관리 (RBAC)', category: 'auth', rating: 4,
tags: ['권한', 'RBAC', '역할', '접근제어'],
memo: 'AWS IAM, GitHub Org Settings, Notion Permissions 등. 역할 기반 접근 제어를 관리하는 UI.',
components: [
{ name: '역할 목록 (관리자, 편집자, 뷰어)', required: true },
{ name: '권한 매트릭스 (역할 × 기능)', required: true },
{ name: '체크박스/토글 (권한 부여)', required: true },
{ name: '역할 생성/편집/삭제', required: true },
{ name: '사용자-역할 배정', required: true },
],
usedIn: ['AWS IAM', 'GitHub', 'Notion', 'SAM 부서 권한 관리'],
guidelines: '매트릭스: 행=권한, 열=역할. 체크/언체크로 즉시 변경. 관리자 역할은 수정 불가 (잠금 아이콘). 변경 시 확인 다이얼로그.',
},
{
type: 'pattern', title: '세션/디바이스 관리', category: 'auth', rating: 3,
tags: ['세션', '디바이스', '보안', '로그아웃'],
memo: 'Google 계정, GitHub Sessions, Notion 등. 로그인된 디바이스/세션을 관리하고 원격 로그아웃.',
components: [
{ name: '활성 세션 목록', required: true },
{ name: '디바이스 정보 (OS, 브라우저, IP)', required: true },
{ name: '마지막 활동 시간', required: true },
{ name: '현재 세션 표시 (이 기기)', required: true },
{ name: '개별/전체 로그아웃 버튼', required: true },
],
usedIn: ['Google 계정', 'GitHub', 'Notion', 'SAM 보안 설정'],
guidelines: '현재 세션은 "이 기기" 초록 배지. 디바이스 아이콘 (데스크톱/모바일). 로그아웃 시 확인 다이얼로그. 마지막 활동 상대 시간 표시.',
},
{
type: 'pattern', title: 'API 키 관리', category: 'auth', rating: 3,
tags: ['API키', '토큰', '인증', '개발자'],
memo: 'Stripe API Keys, OpenAI API, GitHub Token 등. API 키를 생성/관리/폐기하는 개발자 설정.',
components: [
{ name: 'API 키 목록 (마스킹)', required: true },
{ name: '새 키 생성 버튼', required: true },
{ name: '키 복사 버튼', required: true },
{ name: '키 폐기/삭제', required: true },
{ name: '만료일/사용 통계', required: false },
{ name: '생성 시 한 번만 표시 경고', required: true },
],
usedIn: ['Stripe', 'OpenAI', 'GitHub', 'SAM API 키 관리'],
guidelines: '키 마스킹: sk-...xxxx (앞 4자 + 뒤 4자만). 복사 시 "복사됨" 토스트. 새 키 생성 시 전체 표시 (한 번만). 테스트/프로덕션 키 구분.',
},
// ===== 보고서 추가 (4) =====
{
type: 'pattern', title: '간트 차트 (프로젝트 일정)', category: 'report', rating: 5,
tags: ['간트', '일정', '프로젝트', '타임라인'],
memo: 'Jira Roadmap, Monday.com, Microsoft Project 등. 작업 일정을 가로 막대로 시각화하는 프로젝트 관리 핵심 도구.',
components: [
{ name: '작업 목록 (좌측 고정)', required: true },
{ name: '시간축 (상단 헤더, 일/주/월)', required: true },
{ name: '작업 바 (시작~종료)', required: true },
{ name: '의존관계 화살표', required: false },
{ name: '진행률 표시 (바 내부)', required: true },
{ name: '마일스톤 (다이아몬드 아이콘)', required: false },
],
usedIn: ['Jira Roadmap', 'Monday.com', 'MS Project', 'SAM 프로젝트 관리'],
guidelines: '작업 바는 드래그로 이동/리사이즈. 오늘 날짜 빨간 세로선. 주말 배경 회색. 크리티컬 패스 빨간 강조.',
},
{
type: 'pattern', title: '조직도 (트리 구조)', category: 'report', rating: 4,
tags: ['조직도', '트리', '계층', '구조'],
memo: 'SAM 조직도, Lucidchart, OrgChart 등. 부서/직원의 계층 구조를 시각적으로 표현.',
components: [
{ name: '노드 카드 (이름 + 직책)', required: true },
{ name: '연결선 (부모→자식)', required: true },
{ name: '확대/축소 컨트롤', required: true },
{ name: '접기/펼치기 (부서 단위)', required: true },
{ name: '드래그 팬 (이동)', required: true },
],
usedIn: ['SAM 조직도', 'Lucidchart', 'OrgChart', 'HR 시스템'],
guidelines: '노드 카드: 이름, 직책, 부서, 아바타. 연결선: 직선 또는 곡선. 수평/수직 레이아웃 전환. 검색 시 해당 노드 하이라이트.',
},
{
type: 'pattern', title: '워터폴 차트 (재무 분석)', category: 'report', rating: 3,
tags: ['워터폴', '재무', '증감', '누적'],
memo: 'McKinsey, BCG 재무 보고서, Excel 등. 초기 값에서 증가/감소를 순차적으로 보여주는 재무 분석 차트.',
components: [
{ name: '시작 막대 (전체 높이)', required: true },
{ name: '증가 막대 (초록)', required: true },
{ name: '감소 막대 (빨강)', required: true },
{ name: '연결선 (이전 값→현재 시작)', required: true },
{ name: '최종 합계 막대', required: true },
{ name: '각 막대 위 수치 라벨', required: true },
],
usedIn: ['McKinsey 보고서', 'Excel', 'Power BI', 'SAM 재무 분석'],
guidelines: '증가=초록, 감소=빨강, 합계=파랑. 연결선으로 이전 값과 현재 시작점 연결. 막대 위에 절대 값 + 변화량 표시.',
},
{
type: 'pattern', title: '리포트 빌더 (커스텀 보고서)', category: 'report', rating: 4,
tags: ['리포트빌더', '커스텀', '보고서', '생성기'],
memo: 'Salesforce Reports, HubSpot Reports, Metabase 등. 사용자가 직접 필드/필터/차트를 조합하여 보고서를 생성.',
components: [
{ name: '데이터 소스 선택', required: true },
{ name: '필드 선택 (드래그 또는 체크)', required: true },
{ name: '필터 조건 빌더', required: true },
{ name: '차트 유형 선택', required: true },
{ name: '미리보기', required: true },
{ name: '저장/내보내기', required: true },
],
usedIn: ['Salesforce', 'HubSpot', 'Metabase', 'SAM 리포트 빌더 (계획)'],
guidelines: '좌측: 필드 목록 (드래그). 우측: 미리보기 영역. 필터: AND/OR 조합 UI. 차트 유형 아이콘 선택. 저장된 보고서 목록.',
},
// ===== 기타 추가 (15) =====
{
type: 'pattern', title: '댓글/스레드', category: 'etc', rating: 4,
tags: ['댓글', '스레드', '답글', '토론'],
memo: 'GitHub PR Comments, Notion Comments, Figma Comments 등. 특정 항목에 대한 토론을 스레드로 관리.',
components: [
{ name: '댓글 목록 (시간순)', required: true },
{ name: '아바타 + 작성자 + 시간', required: true },
{ name: '답글 (들여쓰기 스레드)', required: true },
{ name: '편집/삭제 (본인 댓글)', required: true },
{ name: '이모지 리액션', required: false },
{ name: '멘션 (@사용자)', required: false },
],
usedIn: ['GitHub', 'Notion', 'Figma', 'SAM 게시판/수주 댓글'],
guidelines: '답글 들여쓰기 24px. 시간은 상대 표시 (3분 전). 본인 댓글만 편집/삭제 가능. 새 댓글 자동 스크롤.',
},
{
type: 'pattern', title: '에러 페이지 (404/500)', category: 'etc', rating: 3,
tags: ['에러', '404', '500', '오류페이지'],
memo: 'GitHub 404, Notion 404, Stripe 등. 페이지를 찾을 수 없거나 서버 오류 시 표시되는 안내 페이지.',
components: [
{ name: '에러 코드 (큰 텍스트)', required: true },
{ name: '안내 메시지', required: true },
{ name: '일러스트/아이콘', required: false },
{ name: '홈으로 가기 버튼', required: true },
{ name: '검색 바 (선택적)', required: false },
{ name: '이전 페이지로 가기', required: true },
],
usedIn: ['GitHub', 'Notion', 'Stripe', 'SAM 에러 페이지'],
guidelines: '화면 중앙 배치. 에러 코드 크게 (72~96px). 친근한 메시지. 홈/뒤로 가기 버튼 명확히. 404는 유머 가능, 500은 진지하게.',
},
{
type: 'pattern', title: '비교 테이블 (기능/제품)', category: 'etc', rating: 4,
tags: ['비교', '기능비교', '제품', '체크마크'],
memo: 'SaaS 가격 페이지, Product Hunt, AWS 서비스 비교 등. 여러 옵션을 기능별로 나란히 비교하는 테이블.',
components: [
{ name: '기능 목록 (좌측 고정)', required: true },
{ name: '옵션 컬럼 (2~4개)', required: true },
{ name: '체크/X 아이콘', required: true },
{ name: '추천 옵션 하이라이트', required: false },
{ name: '가격 행 (상단 또는 하단)', required: true },
{ name: '선택/CTA 버튼', required: true },
],
usedIn: ['SaaS 가격 페이지', 'Product Hunt', 'AWS', 'SAM 요금제 비교'],
guidelines: '기능 행 zebra striping. 추천 열 파란 테두리 + 배지. 체크=초록, X=빨강, 하이픈=회색. 모바일: 좌우 스크롤.',
},
{
type: 'pattern', title: '캐러셀/슬라이더', category: 'etc', rating: 3,
tags: ['캐러셀', '슬라이더', '갤러리', '스와이프'],
memo: 'Amazon 상품 이미지, Netflix 콘텐츠 행, Instagram 다중 이미지 등. 좌우로 넘기는 콘텐츠 슬라이더.',
components: [
{ name: '슬라이드 컨테이너', required: true },
{ name: '좌/우 네비게이션 화살표', required: true },
{ name: '페이지 인디케이터 (점)', required: true },
{ name: '자동 재생 (선택적)', required: false },
{ name: '스와이프 제스처 (모바일)', required: true },
],
usedIn: ['Amazon', 'Netflix', 'Instagram', 'SAM 제품 이미지'],
guidelines: '한 번에 1~3개 표시. 인디케이터 5개 이하. 자동 재생 시 정지 버튼. 마지막 슬라이드 후 첫 번째로 순환. 스와이프 threshold 50px.',
},
{
type: 'pattern', title: '아코디언/FAQ', category: 'etc', rating: 4,
tags: ['아코디언', 'FAQ', '접기', '펼치기'],
memo: 'Apple FAQ, Stripe Help, Bootstrap Accordion 등. 질문/답변을 접기/펼치기로 관리하는 UI.',
components: [
{ name: '질문/제목 (클릭 영역)', required: true },
{ name: '펼치기/접기 아이콘 (+ 또는 ▾)', required: true },
{ name: '답변/내용 (슬라이드 펼침)', required: true },
{ name: '하나만 열기 모드 (선택적)', required: false },
{ name: '전체 펼치기/접기 버튼', required: false },
],
usedIn: ['Apple FAQ', 'Stripe', 'Bootstrap', 'SAM FAQ/도움말'],
guidelines: '기본 모두 접힌 상태. 클릭 영역은 전체 행. 아이콘 회전 애니메이션 (180도). 펼침 시 슬라이드 다운 200ms.',
},
{
type: 'pattern', title: '툴팁/팝오버', category: 'etc', rating: 3,
tags: ['툴팁', '팝오버', '도움말', 'hover'],
memo: 'GitHub 아이콘 hover, Notion 블록 메뉴, AWS 설명 등. 요소에 마우스를 올리면 추가 정보를 표시.',
components: [
{ name: '트리거 요소 (hover/click)', required: true },
{ name: '팝업 컨테이너 (말풍선)', required: true },
{ name: '화살표 (연결 포인터)', required: true },
{ name: '텍스트 내용', required: true },
{ name: '닫기 버튼 (팝오버)', required: false },
],
usedIn: ['GitHub', 'Notion', 'AWS', 'SAM 아이콘 도움말'],
guidelines: '툴팁: hover 후 300ms 지연 표시. 팝오버: 클릭 트리거 + 닫기. 위치: 자동 조정 (화면 밖 방지). 최대 너비 250px.',
},
{
type: 'pattern', title: '프로그레스 트래커 (배송/처리)', category: 'etc', rating: 4,
tags: ['프로그레스', '배송', '처리상태', '추적'],
memo: 'Amazon 배송 추적, Coupang, DHL 등. 주문/처리 단계별 진행 상황을 시각화.',
components: [
{ name: '단계 노드 (원/체크)', required: true },
{ name: '연결선 (완료/진행/대기)', required: true },
{ name: '단계 라벨 + 시간', required: true },
{ name: '현재 단계 강조', required: true },
{ name: '상세 정보 (각 단계 설명)', required: false },
],
usedIn: ['Amazon', 'Coupang', 'DHL', 'SAM 수주 상태 추적'],
guidelines: '완료=초록 체크, 현재=파란 점+애니메이션, 대기=회색. 수평 또는 수직 배치. 각 단계에 시간/날짜 표시.',
},
{
type: 'pattern', title: '데이터 임포트 위자드', category: 'etc', rating: 4,
tags: ['임포트', '가져오기', 'CSV', '엑셀'],
memo: 'Airtable Import, HubSpot Import, Notion Import 등. CSV/엑셀 파일을 업로드하고 필드를 매핑하는 3단계 플로우.',
components: [
{ name: '파일 업로드 (드래그 앤 드롭)', required: true },
{ name: '미리보기 (첫 5행)', required: true },
{ name: '필드 매핑 (소스→대상)', required: true },
{ name: '유효성 검사 결과', required: true },
{ name: '임포트 진행률', required: true },
{ name: '결과 요약 (성공/실패 건수)', required: true },
],
usedIn: ['Airtable', 'HubSpot', 'Notion', 'SAM 데이터 마이그레이션'],
guidelines: '3단계: 업로드→매핑→확인. 미리보기에서 문제 행 빨간 강조. 매핑: 드롭다운으로 대상 필드 선택. 자동 감지 (컬럼명 유사도).',
},
{
type: 'pattern', title: '랜딩 페이지 히어로 섹션', category: 'etc', rating: 4,
tags: ['랜딩', '히어로', 'CTA', '메인배너'],
memo: 'Stripe, Linear, Vercel 등 SaaS. 메인 페이지 첫 화면. 헤드라인 + 서브텍스트 + CTA 버튼의 표준 구조.',
components: [
{ name: '헤드라인 (큰 텍스트)', required: true },
{ name: '서브 텍스트 (설명)', required: true },
{ name: 'CTA 버튼 (1~2개)', required: true },
{ name: '히어로 이미지/영상/일러스트', required: false },
{ name: '소셜 프루프 (고객 로고/통계)', required: false },
],
usedIn: ['Stripe', 'Linear', 'Vercel', 'SAM 소개 페이지'],
guidelines: '헤드라인: 32~56px, 한 줄 권장. CTA 버튼: 명확한 액션 (예: "무료로 시작하기"). 서브 텍스트: 18~20px, 2줄 이내.',
},
{
type: 'pattern', title: '코드 에디터/뷰어', category: 'etc', rating: 3,
tags: ['코드', '에디터', '구문강조', '개발'],
memo: 'VS Code, GitHub Gist, CodePen 등. 코드를 구문 강조하여 보여주거나 편집하는 UI.',
components: [
{ name: '코드 영역 (모노스페이스 폰트)', required: true },
{ name: '줄 번호', required: true },
{ name: '구문 강조 (Syntax Highlighting)', required: true },
{ name: '복사 버튼', required: true },
{ name: '언어 선택 드롭다운', required: false },
{ name: '다크/라이트 테마', required: false },
],
usedIn: ['VS Code', 'GitHub', 'CodePen', 'SAM API 문서'],
guidelines: '폰트: JetBrains Mono 또는 Fira Code. 다크 배경 (#1e1e1e). 복사 시 "복사됨" 피드백. 줄 번호 회색, 코드 색상은 테마에 따름.',
},
{
type: 'pattern', title: '키보드 단축키 도움말', category: 'etc', rating: 3,
tags: ['단축키', '키보드', '도움말', '핫키'],
memo: 'GitHub (? 키), Notion (/), Gmail (?) 등. 키보드 단축키 목록을 보여주는 오버레이.',
components: [
{ name: '키 조합 표시 (Kbd 스타일)', required: true },
{ name: '액션 설명', required: true },
{ name: '카테고리 그룹핑', required: true },
{ name: '검색/필터', required: false },
{ name: 'ESC로 닫기', required: true },
],
usedIn: ['GitHub', 'Notion', 'Gmail', 'SAM 디자인 인사이트'],
guidelines: 'Kbd 스타일: 회색 배경 + 테두리 + 라운드. 그룹별 섹션 분리. 2~3열 레이아웃. ?키로 열기 관례.',
},
{
type: 'pattern', title: '다크 모드 전환', category: 'etc', rating: 3,
tags: ['다크모드', '테마', '라이트', '전환'],
memo: 'macOS, Tailwind, GitHub, VS Code 등. 라이트/다크 테마를 전환하는 UI + 실제 적용.',
components: [
{ name: '토글 스위치/버튼', required: true },
{ name: '해/달 아이콘', required: true },
{ name: '시스템 설정 연동', required: false },
{ name: '전환 애니메이션', required: false },
{ name: 'CSS 변수 기반 테마', required: true },
],
usedIn: ['macOS', 'GitHub', 'Tailwind Docs', 'SAM (계획)'],
guidelines: '3옵션: 라이트/다크/시스템. 전환 시 부드러운 트랜지션 (200ms). prefers-color-scheme 미디어 쿼리. localStorage에 선호 저장.',
},
{
type: 'pattern', title: '알림 설정 패널', category: 'etc', rating: 3,
tags: ['알림설정', '구독', '채널', '선호'],
memo: 'GitHub Notifications, Slack Preferences, Notion Settings 등. 알림 채널별/유형별 수신 여부를 설정.',
components: [
{ name: '알림 유형 목록 (수주, 승인 등)', required: true },
{ name: '채널별 토글 (이메일/푸시/슬랙)', required: true },
{ name: '방해 금지 시간 설정', required: false },
{ name: '전체 켜기/끄기', required: true },
{ name: '미리보기 (알림 샘플)', required: false },
],
usedIn: ['GitHub', 'Slack', 'Notion', 'SAM 알림 설정'],
guidelines: '매트릭스: 행=알림 유형, 열=채널. 토글 ON/OFF. 변경 즉시 저장. 필수 알림(보안)은 비활성화 불가.',
},
{
type: 'pattern', title: '파일 매니저/탐색기', category: 'etc', rating: 4,
tags: ['파일', '탐색기', '폴더', '관리'],
memo: 'Google Drive, Dropbox, macOS Finder 등. 폴더/파일을 계층적으로 탐색하고 관리하는 UI.',
components: [
{ name: '폴더 트리 (좌측)', required: true },
{ name: '파일 목록 (우측, 그리드/리스트)', required: true },
{ name: '경로 표시 (브레드크럼)', required: true },
{ name: '업로드/새 폴더 버튼', required: true },
{ name: '파일 미리보기', required: false },
{ name: '검색/정렬/필터', required: true },
],
usedIn: ['Google Drive', 'Dropbox', 'macOS Finder', 'SAM 문서 관리'],
guidelines: '더블클릭으로 폴더 진입. 파일 유형별 아이콘. 드래그 앤 드롭으로 이동. 우클릭 컨텍스트 메뉴. 다중 선택 (Ctrl+클릭).',
},
{
type: 'pattern', title: '소셜 공유 버튼', category: 'etc', rating: 3,
tags: ['공유', '소셜', 'SNS', '링크'],
memo: 'Medium, Product Hunt, 뉴스 사이트 등. 콘텐츠를 SNS에 공유하는 버튼 세트.',
components: [
{ name: '소셜 미디어 아이콘 (Twitter, LinkedIn 등)', required: true },
{ name: '링크 복사 버튼', required: true },
{ name: '공유 수 카운터', required: false },
{ name: '이메일 공유', required: false },
{ name: '플로팅/고정 위치 선택', required: true },
],
usedIn: ['Medium', 'Product Hunt', '뉴스 사이트', 'SAM 공유 기능'],
guidelines: '아이콘: 각 플랫폼 브랜드 컬러. 링크 복사 시 "복사됨" 토스트. 플로팅: 좌측 고정, 스크롤 따라감. 인라인: 콘텐츠 상/하단.',
},
];
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 패턴 100종이 추가되었습니다');
},
};
}
</script>
@endpush