Files
sam-manage/resources/views/rd/design-insight/index.blade.php
김보곤 57f53ac01e feat: [design-insight] 50종 UI 패턴 와이어프레임 미리보기 추가
- 로그인/인증 5종: 클래식 로그인, 소셜 SSO, 2FA, 비밀번호 재설정, 회원가입
- 보고서 5종: 인쇄용, 인보이스/견적서, 분석 리포트, 업무 보고서, PDF 리포트
- 대시보드 4종: 위젯, 실시간 모니터링, 멀티 차트, 데이터 시각화
- 목록 3종: 무한 스크롤, 그룹/섹션, 벌크 액션
- 폼 3종: 인라인 편집, 리치 텍스트 에디터, 프로필 카드
- 모달 4종: 확인 다이얼로그, 라이트박스, 알림 센터, 날짜 선택기
- 네비게이션 3종: 메가 메뉴, 모바일 하단바, 다단계 드롭다운
- 기타 3종: 드래그 정렬, 스켈레톤 로딩, 알림 배지
2026-03-08 10:41:23 +09:00

4562 lines
301 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

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

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

@extends('layouts.app')
@section('title', '디자인 인사이트')
@section('content')
<style>
/* ===== Design Insight Core ===== */
:root {
--di-sidebar: 280px;
--di-toolbar: 48px;
--di-blue: #3b82f6;
--di-indigo: #6366f1;
--di-green: #10b981;
--di-amber: #f59e0b;
--di-red: #ef4444;
--di-purple: #8b5cf6;
--di-pink: #ec4899;
--di-cyan: #0ea5e9;
--di-slate: #64748b;
--di-bg: #f8fafc;
--di-card-bg: #ffffff;
--di-border: #e2e8f0;
--di-text: #1e293b;
--di-text-secondary: #64748b;
--di-radius: 10px;
--di-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
--di-shadow-lg: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);
}
/* Wrap */
.di-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 56px);
background: var(--di-bg);
overflow: hidden;
font-family: 'Pretendard', -apple-system, sans-serif;
}
/* Toolbar */
.di-toolbar {
display: flex;
align-items: center;
height: var(--di-toolbar);
padding: 0 16px;
background: #fff;
border-bottom: 1px solid var(--di-border);
gap: 12px;
flex-shrink: 0;
z-index: 20;
}
.di-toolbar .di-title-input {
border: none;
background: transparent;
font-size: 15px;
font-weight: 600;
color: var(--di-text);
padding: 4px 8px;
border-radius: 6px;
min-width: 200px;
}
.di-toolbar .di-title-input:hover { background: #f1f5f9; }
.di-toolbar .di-title-input:focus { outline: 2px solid var(--di-blue); background: #fff; }
.di-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 6px;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--di-border);
background: #fff;
color: var(--di-text);
transition: all .15s;
white-space: nowrap;
}
.di-btn:hover { background: #f1f5f9; border-color: #cbd5e1; }
.di-btn.primary { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
.di-btn.primary:hover { background: #2563eb; }
.di-btn.sm { padding: 4px 8px; font-size: 11.5px; }
.di-btn.ghost { border: none; background: transparent; }
.di-btn.ghost:hover { background: #f1f5f9; }
.di-view-tabs {
display: flex;
gap: 2px;
background: #f1f5f9;
border-radius: 8px;
padding: 3px;
}
.di-view-tab {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
color: var(--di-text-secondary);
transition: all .15s;
border: none;
background: transparent;
}
.di-view-tab:hover { color: var(--di-text); }
.di-view-tab.active { background: #fff; color: var(--di-text); box-shadow: 0 1px 2px rgba(0,0,0,.08); font-weight: 600; }
/* Body */
.di-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.di-sidebar {
width: var(--di-sidebar);
background: #fff;
border-right: 1px solid var(--di-border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
transition: width .2s;
}
.di-sidebar.collapsed { width: 0; overflow: hidden; padding: 0; border-right: none; }
.di-sidebar-section {
padding: 12px 14px;
border-bottom: 1px solid var(--di-border);
}
.di-sidebar-section h4 {
font-size: 10.5px;
font-weight: 700;
color: var(--di-text-secondary);
text-transform: uppercase;
letter-spacing: .5px;
margin-bottom: 8px;
}
.di-sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--di-text);
cursor: pointer;
transition: all .12s;
}
.di-sidebar-item:hover { background: #f1f5f9; }
.di-sidebar-item.active { background: #eff6ff; color: var(--di-blue); font-weight: 600; }
.di-sidebar-item .cnt {
margin-left: auto;
font-size: 11px;
color: var(--di-text-secondary);
background: #f1f5f9;
padding: 1px 7px;
border-radius: 10px;
}
.di-tag-chip {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 11.5px;
cursor: pointer;
border: 1px solid var(--di-border);
background: #fff;
color: var(--di-text-secondary);
margin: 2px;
transition: all .12s;
}
.di-tag-chip:hover { border-color: var(--di-blue); color: var(--di-blue); }
.di-tag-chip.active { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
.di-search-input {
width: 100%;
padding: 7px 10px 7px 32px;
border: 1px solid var(--di-border);
border-radius: 8px;
font-size: 12.5px;
background: #f8fafc;
transition: all .15s;
}
.di-search-input:focus { outline: none; border-color: var(--di-blue); background: #fff; box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
/* Main */
.di-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Category Tabs */
.di-cat-tabs {
display: flex;
gap: 1px;
padding: 10px 20px 0;
background: #fff;
border-bottom: 1px solid var(--di-border);
overflow-x: auto;
flex-shrink: 0;
}
.di-cat-tab {
padding: 8px 16px;
font-size: 13px;
color: var(--di-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all .15s;
white-space: nowrap;
background: none;
border-top: none;
border-left: none;
border-right: none;
}
.di-cat-tab:hover { color: var(--di-text); }
.di-cat-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
/* Content */
.di-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
/* Board View */
.di-board {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
/* Card */
.di-card {
background: var(--di-card-bg);
border-radius: var(--di-radius);
border: 1px solid var(--di-border);
overflow: hidden;
cursor: pointer;
transition: all .2s;
position: relative;
}
.di-card:hover { box-shadow: var(--di-shadow-lg); border-color: #cbd5e1; transform: translateY(-1px); }
.di-card .card-img {
width: 100%;
height: 180px;
object-fit: cover;
background: #f1f5f9;
display: block;
}
.di-card .card-img-placeholder {
width: 100%;
height: 180px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 32px;
}
.di-card .card-body {
padding: 14px;
}
.di-card .card-type {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.5px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 6px;
}
.di-card .card-type.ref { background: #eff6ff; color: #2563eb; }
.di-card .card-type.analysis { background: #fef3c7; color: #d97706; }
.di-card .card-type.pattern { background: #ecfdf5; color: #059669; }
.di-card .card-type.comparison { background: #fae8ff; color: #a855f7; }
.di-card .card-title {
font-size: 14px;
font-weight: 600;
color: var(--di-text);
margin-bottom: 6px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.di-card .card-memo {
font-size: 12.5px;
color: var(--di-text-secondary);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
}
.di-card .card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.di-card .card-tag {
font-size: 10.5px;
padding: 2px 7px;
border-radius: 4px;
background: #f1f5f9;
color: var(--di-text-secondary);
}
.di-card .card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 8px;
border-top: 1px solid #f1f5f9;
}
.di-card .card-rating { color: #f59e0b; font-size: 12px; letter-spacing: 1px; }
.di-card .card-date { font-size: 11px; color: #94a3b8; }
.di-card .card-pin {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255,255,255,.9);
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
opacity: 0;
transition: opacity .15s;
border: 1px solid var(--di-border);
}
.di-card:hover .card-pin { opacity: 1; }
.di-card .card-pin.pinned { opacity: 1; color: var(--di-amber); }
/* Add Card Button */
.di-add-card {
background: var(--di-card-bg);
border-radius: var(--di-radius);
border: 2px dashed var(--di-border);
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all .2s;
gap: 8px;
color: var(--di-text-secondary);
}
.di-add-card:hover { border-color: var(--di-blue); color: var(--di-blue); background: #f0f7ff; }
.di-add-card i { font-size: 28px; }
.di-add-card span { font-size: 13px; font-weight: 500; }
/* Gallery View */
.di-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
}
.di-gallery .di-card .card-img { height: 220px; }
.di-gallery .di-card .card-body { padding: 10px; }
/* List View */
.di-list { display: flex; flex-direction: column; gap: 4px; }
.di-list-item {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
background: #fff;
border-radius: 8px;
border: 1px solid var(--di-border);
cursor: pointer;
transition: all .12s;
}
.di-list-item:hover { background: #f8fafc; box-shadow: var(--di-shadow); }
.di-list-item .li-thumb {
width: 48px;
height: 48px;
border-radius: 6px;
object-fit: cover;
background: #f1f5f9;
flex-shrink: 0;
}
.di-list-item .li-thumb-empty {
width: 48px;
height: 48px;
border-radius: 6px;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 18px;
flex-shrink: 0;
}
.di-list-item .li-info { flex: 1; min-width: 0; }
.di-list-item .li-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.di-list-item .li-memo { font-size: 12px; color: var(--di-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.di-list-item .li-tags { display: flex; gap: 4px; flex-shrink: 0; }
.di-list-item .li-date { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
/* Modal */
.di-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.4);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.di-modal {
background: #fff;
border-radius: 14px;
width: 680px;
max-width: 95vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,.2);
}
.di-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--di-border);
}
.di-modal-header h3 { font-size: 16px; font-weight: 700; color: var(--di-text); }
.di-modal-body { padding: 20px; }
.di-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 14px 20px;
border-top: 1px solid var(--di-border);
}
.di-field { margin-bottom: 16px; }
.di-field label {
display: block;
font-size: 12.5px;
font-weight: 600;
color: var(--di-text);
margin-bottom: 5px;
}
.di-field input[type="text"],
.di-field textarea,
.di-field select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--di-border);
border-radius: 8px;
font-size: 13px;
transition: all .15s;
background: #fff;
}
.di-field input:focus, .di-field textarea:focus, .di-field select:focus {
outline: none;
border-color: var(--di-blue);
box-shadow: 0 0 0 3px rgba(59,130,246,.1);
}
.di-field textarea { min-height: 80px; resize: vertical; }
/* Image Drop Zone */
.di-drop-zone {
border: 2px dashed var(--di-border);
border-radius: 10px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all .2s;
background: #fafbfc;
}
.di-drop-zone:hover, .di-drop-zone.dragover {
border-color: var(--di-blue);
background: #eff6ff;
}
.di-drop-zone img {
max-width: 100%;
max-height: 250px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 10px;
}
/* Before/After comparison */
.di-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.di-comparison .comp-side {
border: 2px dashed var(--di-border);
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all .2s;
background: #fafbfc;
min-height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.di-comparison .comp-side:hover { border-color: var(--di-blue); background: #eff6ff; }
.di-comparison .comp-side img { max-width: 100%; max-height: 200px; object-fit: contain; border-radius: 8px; }
.di-comparison .comp-label {
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 4px;
margin-bottom: 8px;
}
.di-comparison .comp-before .comp-label { background: #fef2f2; color: #dc2626; }
.di-comparison .comp-after .comp-label { background: #ecfdf5; color: #059669; }
/* CRAP Checklist */
.di-crap-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.di-crap-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--di-border);
cursor: pointer;
transition: all .12s;
font-size: 12.5px;
}
.di-crap-item:hover { background: #f8fafc; }
.di-crap-item.pass { background: #ecfdf5; border-color: #a7f3d0; }
.di-crap-item.fail { background: #fef2f2; border-color: #fecaca; }
.di-crap-item.warn { background: #fffbeb; border-color: #fde68a; }
/* Rating Stars */
.di-stars {
display: flex;
gap: 2px;
}
.di-star {
font-size: 20px;
cursor: pointer;
color: #e2e8f0;
transition: color .1s;
}
.di-star.filled { color: #f59e0b; }
.di-star:hover { color: #fbbf24; }
/* Help Modal */
.di-help-modal { width: 740px; }
.di-help-modal .di-modal-body { padding: 0; }
.di-help-tabs {
display: flex;
border-bottom: 1px solid var(--di-border);
padding: 0 20px;
gap: 0;
overflow-x: auto;
}
.di-help-tab {
padding: 12px 16px;
font-size: 13px;
color: var(--di-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
white-space: nowrap;
background: none;
border-top: none; border-left: none; border-right: none;
transition: all .15s;
}
.di-help-tab:hover { color: var(--di-text); }
.di-help-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
.di-help-content { padding: 24px; max-height: 60vh; overflow-y: auto; }
.di-help-content h4 { font-size: 15px; font-weight: 700; color: var(--di-text); margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }
.di-help-content h4 i { font-size: 18px; color: var(--di-blue); }
.di-help-content p { font-size: 13px; color: var(--di-text-secondary); line-height: 1.7; margin: 0 0 16px; }
.di-help-section { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #f1f5f9; }
.di-help-section:last-child { border-bottom: none; margin-bottom: 0; }
.di-help-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
.di-help-item {
display: flex;
gap: 10px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #f1f5f9;
}
.di-help-item .h-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.di-help-item .h-body { flex: 1; }
.di-help-item .h-title { font-size: 13px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
.di-help-item .h-desc { font-size: 11.5px; color: var(--di-text-secondary); line-height: 1.5; }
.di-help-kbd {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 11.5px;
font-family: monospace;
color: var(--di-text);
margin: 2px;
}
.di-help-step {
display: flex;
gap: 12px;
margin-bottom: 12px;
align-items: flex-start;
}
.di-help-step .step-num {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--di-blue);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.di-help-step .step-body { flex: 1; padding-top: 3px; }
.di-help-step .step-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
.di-help-step .step-desc { font-size: 12.5px; color: var(--di-text-secondary); line-height: 1.6; }
/* Preview Modal */
.di-preview { width: 820px; max-width: 95vw; }
.di-preview .di-modal-body { padding: 0; }
.di-preview-layout { display: flex; gap: 0; }
.di-preview-left { flex: 1; min-width: 0; border-right: 1px solid var(--di-border); }
.di-preview-right { width: 280px; flex-shrink: 0; padding: 20px; overflow-y: auto; max-height: 70vh; }
.di-preview-wireframe {
background: #f8fafc;
min-height: 320px;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
overflow: hidden;
}
.di-preview-wireframe img {
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 6px;
}
.di-preview-info-item { margin-bottom: 16px; }
.di-preview-info-item label {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .5px;
color: var(--di-text-secondary);
margin-bottom: 4px;
display: block;
}
.di-preview-info-item .val { font-size: 13px; color: var(--di-text); line-height: 1.6; }
.di-preview-comp {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 5px 0;
font-size: 12.5px;
color: var(--di-text);
border-bottom: 1px solid #f1f5f9;
}
.di-preview-comp:last-child { border-bottom: none; }
.di-preview-comp .chk { color: #10b981; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
.di-preview-comp .opt { color: #94a3b8; font-size: 13px; flex-shrink: 0; margin-top: 1px; }
/* Wireframe Styles */
.wf-wrap {
width: 100%;
max-width: 480px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e2e8f0;
background: #fff;
font-family: 'Pretendard', sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.wf-bar { height: 10px; border-radius: 3px; background: #e2e8f0; }
.wf-bar.sm { width: 40px; }
.wf-bar.md { width: 80px; }
.wf-bar.lg { width: 120px; }
.wf-bar.xl { width: 180px; }
.wf-bar.blue { background: #bfdbfe; }
.wf-bar.green { background: #bbf7d0; }
.wf-bar.amber { background: #fde68a; }
.wf-bar.red { background: #fecaca; }
.wf-bar.purple { background: #ddd6fe; }
.wf-bar.dark { background: #cbd5e1; }
.wf-circle { border-radius: 50%; background: #e2e8f0; flex-shrink: 0; }
.wf-box { border-radius: 6px; background: #f1f5f9; border: 1px solid #e2e8f0; }
.wf-text { font-size: 8px; color: #94a3b8; font-weight: 600; letter-spacing: .3px; white-space: nowrap; }
/* Status Bar */
.di-statusbar {
display: flex;
align-items: center;
gap: 16px;
padding: 6px 20px;
background: #fff;
border-top: 1px solid var(--di-border);
font-size: 11.5px;
color: var(--di-text-secondary);
flex-shrink: 0;
}
.di-statusbar .stat { display: flex; align-items: center; gap: 4px; }
/* Toast */
.di-toast {
position: fixed;
bottom: 24px;
right: 24px;
background: #1e293b;
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-size: 13px;
z-index: 200;
box-shadow: 0 8px 20px rgba(0,0,0,.2);
animation: diToastIn .3s ease;
}
@keyframes diToastIn { from { opacity: 0; transform: translateY(10px); } }
/* Empty State */
.di-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: var(--di-text-secondary);
text-align: center;
}
.di-empty i { font-size: 48px; margin-bottom: 16px; color: #cbd5e1; }
.di-empty h3 { font-size: 16px; font-weight: 600; color: var(--di-text); margin-bottom: 6px; }
.di-empty p { font-size: 13px; margin-bottom: 16px; }
/* Paste hint */
.di-paste-hint {
position: fixed;
bottom: 80px;
right: 24px;
background: #1e293b;
color: #fff;
padding: 8px 14px;
border-radius: 8px;
font-size: 12px;
z-index: 50;
opacity: .8;
}
.di-paste-hint kbd {
background: rgba(255,255,255,.2);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
}
/* Projects Modal */
.di-proj-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all .12s;
border: 1px solid transparent;
}
.di-proj-item:hover { background: #f8fafc; border-color: var(--di-border); }
.di-proj-item.active { background: #eff6ff; border-color: var(--di-blue); }
.di-proj-item .proj-title { font-size: 13.5px; font-weight: 600; flex: 1; }
.di-proj-item .proj-count { font-size: 11px; color: var(--di-text-secondary); }
.di-proj-item .proj-date { font-size: 11px; color: #94a3b8; }
</style>
<!-- ===== Alpine.js App ===== -->
<div class="di-wrap" x-data="designInsight()" x-init="init()" @paste.window="handlePaste($event)" @keydown.window="handleKeydown($event)">
<!-- Toolbar -->
<div class="di-toolbar">
<button class="di-btn ghost" @click="sidebarOpen = !sidebarOpen" title="사이드바 토글">
<i class="ri-layout-left-line" style="font-size: 16px;"></i>
</button>
<input type="text" class="di-title-input"
x-model="currentProject.title"
@change="saveProject()"
placeholder="프로젝트 제목">
<div style="flex: 1;"></div>
<button class="di-btn sm" @click="saveProject()" title="저장 (Ctrl+S)">
<i class="ri-save-line"></i> 저장
</button>
<button class="di-btn sm" @click="showExportMenu = !showExportMenu" title="내보내기">
<i class="ri-download-line"></i> 내보내기
</button>
<template x-if="showExportMenu">
<div style="position: absolute; top: 44px; right: 80px; background: #fff; border: 1px solid var(--di-border); border-radius: 8px; box-shadow: var(--di-shadow-lg); z-index: 30; padding: 4px;"
@click.outside="showExportMenu = false">
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="exportJSON(); showExportMenu=false">
<i class="ri-code-s-slash-line"></i> JSON 내보내기
</button>
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="importJSON(); showExportMenu=false">
<i class="ri-upload-line"></i> JSON 가져오기
</button>
<div style="border-top: 1px solid var(--di-border); margin: 4px 0;"></div>
<button class="di-btn ghost sm" style="width: 100%; justify-content: flex-start;" @click="loadPresetTemplates(); showExportMenu=false">
<i class="ri-magic-line"></i> 인기 UI 패턴 20
</button>
</div>
</template>
<div class="di-view-tabs">
<button class="di-view-tab" :class="viewMode === 'board' && 'active'" @click="viewMode = 'board'" title="보드 뷰">
<i class="ri-layout-grid-line"></i>
</button>
<button class="di-view-tab" :class="viewMode === 'gallery' && 'active'" @click="viewMode = 'gallery'" title="갤러리 뷰">
<i class="ri-image-line"></i>
</button>
<button class="di-view-tab" :class="viewMode === 'list' && 'active'" @click="viewMode = 'list'" title="리스트 뷰">
<i class="ri-list-unordered"></i>
</button>
</div>
<button class="di-btn sm" @click="showProjectsModal = true" title="프로젝트 관리">
<i class="ri-folder-line"></i>
</button>
<button class="di-btn sm" @click="showHelpModal = true" title="도움말 (?)">
<i class="ri-question-line"></i>
</button>
</div>
<!-- Category Tabs -->
<div class="di-cat-tabs">
<button class="di-cat-tab" :class="categoryFilter === 'all' && 'active'" @click="categoryFilter = 'all'">
전체 <span x-text="'(' + filteredCards.length + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
</button>
<template x-for="type in cardTypes" :key="type.code">
<button class="di-cat-tab" :class="categoryFilter === type.code && 'active'" @click="categoryFilter = type.code">
<span x-text="type.icon + ' ' + type.label"></span>
<span x-text="'(' + getCardCountByType(type.code) + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
</button>
</template>
</div>
<!-- Body -->
<div class="di-body">
<!-- Sidebar -->
<div class="di-sidebar" :class="!sidebarOpen && 'collapsed'">
<!-- Search -->
<div class="di-sidebar-section">
<div style="position: relative;">
<i class="ri-search-line" style="position: absolute; left: 10px; top: 8px; font-size: 14px; color: #94a3b8;"></i>
<input type="text" class="di-search-input" x-model="searchQuery" placeholder="검색..." @keyup.escape="searchQuery = ''">
</div>
</div>
<!-- Categories -->
<div class="di-sidebar-section">
<h4>카테고리</h4>
<div class="di-sidebar-item" :class="screenFilter === 'all' && 'active'" @click="screenFilter = 'all'">
<span>전체</span>
<span class="cnt" x-text="currentProject.cards?.length || 0"></span>
</div>
<template x-for="cat in categories" :key="cat.code">
<div class="di-sidebar-item" :class="screenFilter === cat.code && 'active'" @click="screenFilter = cat.code">
<span x-text="cat.icon + ' ' + cat.label"></span>
<span class="cnt" x-text="getCardCountByCat(cat.code)"></span>
</div>
</template>
</div>
<!-- Tags -->
<div class="di-sidebar-section">
<h4>태그</h4>
<div style="display: flex; flex-wrap: wrap; gap: 2px;">
<template x-for="tag in allTags" :key="tag">
<span class="di-tag-chip" :class="selectedTags.includes(tag) && 'active'"
@click="toggleTag(tag)" x-text="tag"></span>
</template>
<template x-if="allTags.length === 0">
<span style="font-size: 12px; color: #94a3b8;">태그 없음</span>
</template>
</div>
</div>
<!-- Sort -->
<div class="di-sidebar-section">
<h4>정렬</h4>
<select style="width: 100%; padding: 6px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12px;" x-model="sortBy" @change="$nextTick()">
<option value="newest">최신순</option>
<option value="oldest">오래된순</option>
<option value="rating">평점순</option>
<option value="title">이름순</option>
</select>
</div>
</div>
<!-- Main Content -->
<div class="di-main">
<div class="di-content">
<!-- Empty State -->
<template x-if="filteredCards.length === 0 && !searchQuery && selectedTags.length === 0 && screenFilter === 'all' && categoryFilter === 'all'">
<div class="di-empty">
<i class="ri-palette-line"></i>
<h3>인사이트를 수집해보세요</h3>
<p><kbd>Ctrl+V</kbd> 스크린샷을 붙여넣거나, 아래 버튼으로 시작하세요</p>
<div style="display: flex; gap: 8px;">
<button class="di-btn primary" @click="openNewCardModal('reference')">
<i class="ri-add-line"></i> 번째 카드 추가
</button>
<button class="di-btn" @click="loadPresetTemplates()" style="border-color: var(--di-indigo); color: var(--di-indigo);">
<i class="ri-magic-line"></i> 인기 UI 패턴 20 불러오기
</button>
</div>
</div>
</template>
<!-- No Results -->
<template x-if="filteredCards.length === 0 && (searchQuery || selectedTags.length > 0 || screenFilter !== 'all' || categoryFilter !== 'all')">
<div class="di-empty">
<i class="ri-search-line"></i>
<h3>검색 결과 없음</h3>
<p>필터를 변경하거나 검색어를 수정하세요</p>
<button class="di-btn" @click="clearFilters()">
<i class="ri-refresh-line"></i> 필터 초기화
</button>
</div>
</template>
<!-- Board View -->
<template x-if="viewMode === 'board' && filteredCards.length > 0">
<div class="di-board">
<template x-for="card in filteredCards" :key="card.id">
<div class="di-card" @click="openPreviewModal(card)">
<!-- Pin -->
<div class="card-pin" :class="card.pinned && 'pinned'"
@click.stop="togglePin(card)" x-text="card.pinned ? '&#x1F4CC;' : '&#x1F4CC;'"></div>
<!-- Image -->
<template x-if="card.type === 'comparison'">
<div style="display: grid; grid-template-columns: 1fr 1fr; height: 180px;">
<template x-if="card.beforeImage">
<img :src="card.beforeImage" style="width: 100%; height: 180px; object-fit: cover; border-right: 1px solid var(--di-border);">
</template>
<template x-if="!card.beforeImage">
<div class="card-img-placeholder" style="border-right: 1px solid var(--di-border);"><i class="ri-arrow-left-line"></i></div>
</template>
<template x-if="card.afterImage">
<img :src="card.afterImage" style="width: 100%; height: 180px; object-fit: cover;">
</template>
<template x-if="!card.afterImage">
<div class="card-img-placeholder"><i class="ri-arrow-right-line"></i></div>
</template>
</div>
</template>
<template x-if="card.type !== 'comparison' && card.image">
<img :src="card.image" class="card-img">
</template>
<template x-if="card.type !== 'comparison' && !card.image">
<div class="card-img-placeholder">
<i :class="getTypeIcon(card.type)"></i>
</div>
</template>
<!-- Body -->
<div class="card-body">
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type">
<span x-text="getTypeLabel(card.type)"></span>
</span>
<div class="card-title" x-text="card.title || '(제목 없음)'"></div>
<div class="card-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
<div class="card-tags" x-show="card.tags && card.tags.length > 0">
<template x-for="tag in (card.tags || []).slice(0, 4)" :key="tag">
<span class="card-tag" x-text="tag"></span>
</template>
<template x-if="(card.tags || []).length > 4">
<span class="card-tag" x-text="'+' + ((card.tags || []).length - 4)"></span>
</template>
</div>
<div class="card-footer">
<div class="card-rating" x-text="getRatingStars(card.rating || 0)"></div>
<div class="card-date" x-text="formatDate(card.createdAt)"></div>
</div>
</div>
</div>
</template>
<!-- Add New Card -->
<div class="di-add-card" @click="openNewCardModal('reference')">
<i class="ri-add-circle-line"></i>
<span> 카드 추가</span>
</div>
</div>
</template>
<!-- Gallery View -->
<template x-if="viewMode === 'gallery' && filteredCards.length > 0">
<div class="di-gallery">
<template x-for="card in filteredCards" :key="card.id">
<div class="di-card" @click="openPreviewModal(card)">
<template x-if="card.image">
<img :src="card.image" class="card-img">
</template>
<template x-if="!card.image && card.beforeImage">
<img :src="card.beforeImage" class="card-img">
</template>
<template x-if="!card.image && !card.beforeImage">
<div class="card-img-placeholder"><i :class="getTypeIcon(card.type)"></i></div>
</template>
<div class="card-body">
<div class="card-title" x-text="card.title || '(제목 없음)'"></div>
<div class="card-rating" x-text="getRatingStars(card.rating || 0)"></div>
</div>
</div>
</template>
<div class="di-add-card" @click="openNewCardModal('reference')" style="min-height: 260px;">
<i class="ri-add-circle-line"></i>
</div>
</div>
</template>
<!-- List View -->
<template x-if="viewMode === 'list' && filteredCards.length > 0">
<div class="di-list">
<template x-for="card in filteredCards" :key="card.id">
<div class="di-list-item" @click="openPreviewModal(card)">
<template x-if="card.image">
<img :src="card.image" class="li-thumb">
</template>
<template x-if="!card.image">
<div class="li-thumb-empty"><i :class="getTypeIcon(card.type)"></i></div>
</template>
<div class="li-info">
<div class="li-title">
<span class="card-type" :class="card.type === 'reference' ? 'ref' : card.type"
x-text="getTypeLabel(card.type)" style="font-size: 10px; margin-right: 6px;"></span>
<span x-text="card.title || '(제목 없음)'"></span>
</div>
<div class="li-memo" x-text="card.memo || card.suggestion || card.effect || ''"></div>
</div>
<div class="li-tags">
<template x-for="tag in (card.tags || []).slice(0, 3)" :key="tag">
<span class="card-tag" x-text="tag"></span>
</template>
</div>
<div class="card-rating" x-text="getRatingStars(card.rating || 0)" style="flex-shrink: 0;"></div>
<div class="li-date" x-text="formatDate(card.createdAt)"></div>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="di-statusbar">
<span class="stat"><i class="ri-sticky-note-line"></i> 카드 <strong x-text="currentProject.cards?.length || 0"></strong></span>
<span class="stat"><i class="ri-price-tag-3-line"></i> 태그 <strong x-text="allTags.length"></strong></span>
<span class="stat"><i class="ri-time-line"></i> 마지막 저장 <span x-text="lastSaved || '-'"></span></span>
<div style="flex:1;"></div>
<span class="di-paste-hint" style="position: static; opacity: .6; background: transparent; color: var(--di-text-secondary); padding: 0;">
<kbd>Ctrl+V</kbd> 붙여넣기
</span>
</div>
<!-- ===== Card Edit/Create Modal ===== -->
<template x-if="showCardModal">
<div class="di-modal-overlay" @click.self="closeCardModal()">
<div class="di-modal">
<div class="di-modal-header">
<h3 x-text="editingCard.id ? '카드 편집' : '새 카드'"></h3>
<div style="display: flex; gap: 6px; align-items: center;">
<template x-if="editingCard.id">
<button class="di-btn sm" style="color: var(--di-red); border-color: #fecaca;" @click="deleteCard(editingCard.id)">
<i class="ri-delete-bin-line"></i> 삭제
</button>
</template>
<button class="di-btn ghost" @click="closeCardModal()"><i class="ri-close-line" style="font-size: 18px;"></i></button>
</div>
</div>
<div class="di-modal-body">
<!-- Card Type -->
<div class="di-field">
<label>카드 유형</label>
<div style="display: flex; gap: 6px;">
<template x-for="type in cardTypes" :key="type.code">
<button class="di-btn sm" :class="editingCard.type === type.code && 'primary'"
@click="editingCard.type = type.code">
<span x-text="type.icon + ' ' + type.label"></span>
</button>
</template>
</div>
</div>
<!-- Title -->
<div class="di-field">
<label>제목</label>
<input type="text" x-model="editingCard.title" placeholder="인사이트 제목">
</div>
<!-- Image (Reference / Analysis / Pattern) -->
<template x-if="editingCard.type !== 'comparison'">
<div class="di-field">
<label>이미지</label>
<div class="di-drop-zone"
@click="$refs.fileInput.click()"
@drop.prevent="handleDrop($event)"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<template x-if="editingCard.image">
<img :src="editingCard.image">
</template>
<template x-if="!editingCard.image">
<div>
<i class="ri-image-add-line" style="font-size: 32px; color: #94a3b8; display: block; margin-bottom: 8px;"></i>
<span style="font-size: 13px; color: var(--di-text-secondary);">클릭 또는 드래그로 이미지 추가</span>
</div>
</template>
</div>
<input type="file" x-ref="fileInput" accept="image/*" style="display: none;" @change="handleFileSelect($event)">
</div>
</template>
<!-- Before/After Images (Comparison) -->
<template x-if="editingCard.type === 'comparison'">
<div class="di-field">
<label>Before / After 이미지</label>
<div class="di-comparison">
<div class="comp-side comp-before"
@click="$refs.beforeInput.click()"
@drop.prevent="handleCompDrop($event, 'before')"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<span class="comp-label">Before</span>
<template x-if="editingCard.beforeImage">
<img :src="editingCard.beforeImage">
</template>
<template x-if="!editingCard.beforeImage">
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
</template>
</div>
<div class="comp-side comp-after"
@click="$refs.afterInput.click()"
@drop.prevent="handleCompDrop($event, 'after')"
@dragover.prevent="$event.currentTarget.classList.add('dragover')"
@dragleave="$event.currentTarget.classList.remove('dragover')">
<span class="comp-label">After</span>
<template x-if="editingCard.afterImage">
<img :src="editingCard.afterImage">
</template>
<template x-if="!editingCard.afterImage">
<i class="ri-image-add-line" style="font-size: 24px; color: #94a3b8;"></i>
</template>
</div>
</div>
<input type="file" x-ref="beforeInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'before')">
<input type="file" x-ref="afterInput" accept="image/*" style="display: none;" @change="handleCompFileSelect($event, 'after')">
</div>
</template>
<!-- Memo (Reference) -->
<template x-if="editingCard.type === 'reference'">
<div>
<div class="di-field">
<label>인사이트 메모</label>
<textarea x-model="editingCard.memo" placeholder="이 화면/패턴이 왜 좋은가? (또는 나쁜가?)"></textarea>
</div>
<div class="di-field">
<label>출처</label>
<input type="text" x-model="editingCard.source" placeholder="URL, 앱 이름, 서비스명 등">
</div>
</div>
</template>
<!-- Analysis Fields -->
<template x-if="editingCard.type === 'analysis'">
<div>
<div class="di-field">
<label>CRAP 디자인 원칙 체크</label>
<div class="di-crap-grid">
<template x-for="p in designPrinciples" :key="p.key">
<div class="di-crap-item"
:class="getPrincipleStatus(p.key)"
@click="cyclePrinciple(p.key)">
<span x-text="p.icon" style="font-size: 16px;"></span>
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 12px;" x-text="p.label"></div>
<div style="font-size: 10.5px; color: var(--di-text-secondary);" x-text="p.desc"></div>
</div>
<span x-text="getPrincipleIcon(p.key)" style="font-size: 14px;"></span>
</div>
</template>
</div>
</div>
<div class="di-field">
<label>개선 제안</label>
<textarea x-model="editingCard.suggestion" placeholder="어떻게 개선할 수 있는가?"></textarea>
</div>
<div class="di-field">
<label>심각도</label>
<div style="display: flex; gap: 6px;">
<button class="di-btn sm" :class="editingCard.severity === 'info' && 'primary'" @click="editingCard.severity = 'info'"> 정보</button>
<button class="di-btn sm" :class="editingCard.severity === 'warning' && 'primary'" @click="editingCard.severity = 'warning'" style="--di-blue: #f59e0b;">⚠️ 경고</button>
<button class="di-btn sm" :class="editingCard.severity === 'critical' && 'primary'" @click="editingCard.severity = 'critical'" style="--di-blue: #ef4444;">🔴 심각</button>
</div>
</div>
</div>
</template>
<!-- Pattern Fields -->
<template x-if="editingCard.type === 'pattern'">
<div>
<div class="di-field">
<label>사용처</label>
<input type="text" x-model="editingCard.usedInText" placeholder="수주 목록, 거래처 목록 (콤마 구분)">
</div>
<div class="di-field">
<label>구성 요소</label>
<div style="display: flex; flex-direction: column; gap: 4px;">
<template x-for="(comp, ci) in (editingCard.components || [])" :key="ci">
<div style="display: flex; gap: 6px; align-items: center;">
<input type="checkbox" x-model="comp.required">
<input type="text" x-model="comp.name" style="flex: 1; padding: 5px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12.5px;" placeholder="구성 요소명">
<button class="di-btn ghost sm" @click="editingCard.components.splice(ci, 1)"><i class="ri-close-line"></i></button>
</div>
</template>
<button class="di-btn sm" @click="editingCard.components = [...(editingCard.components || []), {name: '', required: true}]">
<i class="ri-add-line"></i> 구성 요소 추가
</button>
</div>
</div>
<div class="di-field">
<label>사용 가이드라인</label>
<textarea x-model="editingCard.guidelines" placeholder="이 패턴을 사용할 때 주의사항이나 가이드"></textarea>
</div>
</div>
</template>
<!-- Comparison Fields -->
<template x-if="editingCard.type === 'comparison'">
<div>
<div class="di-field">
<label>변경 포인트</label>
<textarea x-model="editingCard.changesText" placeholder="1. 탭 구조 → 섹션 접기/펼치기 변경&#10;2. 좌우 2컬럼 → 단일 컬럼" style="min-height: 100px;"></textarea>
</div>
<div class="di-field">
<label>개선 효과</label>
<input type="text" x-model="editingCard.effect" placeholder="스크롤 40% 감소, 작업 완료 시간 단축">
</div>
</div>
</template>
<!-- Common Fields -->
<div class="di-field">
<label>카테고리</label>
<select x-model="editingCard.category">
<template x-for="cat in categories" :key="cat.code">
<option :value="cat.code" x-text="cat.icon + ' ' + cat.label"></option>
</template>
</select>
</div>
<div class="di-field">
<label>태그 (콤마 구분)</label>
<input type="text" x-model="editingCard.tagsText" placeholder="대시보드, 카드, 레이아웃"
@keydown.enter.prevent>
</div>
<div class="di-field">
<label>평점</label>
<div class="di-stars">
<template x-for="s in [1,2,3,4,5]" :key="s">
<span class="di-star" :class="s <= (editingCard.rating || 0) && 'filled'"
@click="editingCard.rating = editingCard.rating === s ? 0 : s"
x-text="s <= (editingCard.rating || 0) ? '\u2605' : '\u2606'"></span>
</template>
</div>
</div>
</div>
<div class="di-modal-footer">
<button class="di-btn" @click="closeCardModal()">취소</button>
<button class="di-btn primary" @click="saveCard()">
<i class="ri-check-line"></i> <span x-text="editingCard.id ? '수정' : '추가'"></span>
</button>
</div>
</div>
</div>
</template>
<!-- ===== Projects Modal ===== -->
<template x-if="showProjectsModal">
<div class="di-modal-overlay" @click.self="showProjectsModal = false">
<div class="di-modal" style="width: 500px;">
<div class="di-modal-header">
<h3>프로젝트 관리</h3>
<button class="di-btn ghost" @click="showProjectsModal = false"><i class="ri-close-line" style="font-size: 18px;"></i></button>
</div>
<div class="di-modal-body">
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<input type="text" x-model="newProjectTitle" placeholder="새 프로젝트 이름"
style="flex: 1; padding: 8px 12px; border: 1px solid var(--di-border); border-radius: 8px; font-size: 13px;"
@keydown.enter="createProject()">
<button class="di-btn primary" @click="createProject()"><i class="ri-add-line"></i> 생성</button>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
<template x-for="proj in projects" :key="proj.id">
<div class="di-proj-item" :class="proj.id === currentProject.id && 'active'" @click="switchProject(proj.id)">
<i class="ri-folder-line" style="font-size: 16px; color: var(--di-blue);"></i>
<span class="proj-title" x-text="proj.title"></span>
<span class="proj-count" x-text="(proj.cards?.length || 0) + '개'"></span>
<span class="proj-date" x-text="formatDate(proj.createdAt)"></span>
<button class="di-btn ghost sm" @click.stop="deleteProject(proj.id)" x-show="projects.length > 1"
title="삭제"><i class="ri-delete-bin-line" style="color: var(--di-red);"></i></button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<!-- ===== Preview Modal ===== -->
<template x-if="showPreviewModal && previewCard">
<div class="di-modal-overlay" @click.self="showPreviewModal = false">
<div class="di-modal di-preview">
<div class="di-modal-header">
<div style="display: flex; align-items: center; gap: 8px;">
<span class="card-type" :class="previewCard.type === 'reference' ? 'ref' : previewCard.type"
x-text="getTypeLabel(previewCard.type)" style="font-size: 11px;"></span>
<h3 x-text="previewCard.title || '(제목 없음)'" style="font-size: 15px;"></h3>
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<button class="di-btn sm" @click="showPreviewModal = false; openEditCardModal(previewCard)">
<i class="ri-edit-line"></i> 편집
</button>
<button class="di-btn ghost" @click="showPreviewModal = false">
<i class="ri-close-line" style="font-size: 18px;"></i>
</button>
</div>
</div>
<div class="di-preview-layout">
<!-- Left: Wireframe / Image -->
<div class="di-preview-left">
<!-- User Image -->
<template x-if="previewCard.image">
<div class="di-preview-wireframe">
<img :src="previewCard.image">
</div>
</template>
<!-- Before/After Images -->
<template x-if="previewCard.type === 'comparison' && !previewCard.image">
<div style="display: grid; grid-template-columns: 1fr 1fr; min-height: 320px;">
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-right: 1px solid var(--di-border); background: #fef2f2;">
<span style="font-size: 10px; font-weight: 700; color: #dc2626; margin-bottom: 8px;">BEFORE</span>
<template x-if="previewCard.beforeImage"><img :src="previewCard.beforeImage" style="max-width: 100%; max-height: 260px; border-radius: 6px;"></template>
<template x-if="!previewCard.beforeImage"><span style="color: #94a3b8;">이미지 없음</span></template>
</div>
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; background: #ecfdf5;">
<span style="font-size: 10px; font-weight: 700; color: #059669; margin-bottom: 8px;">AFTER</span>
<template x-if="previewCard.afterImage"><img :src="previewCard.afterImage" style="max-width: 100%; max-height: 260px; border-radius: 6px;"></template>
<template x-if="!previewCard.afterImage"><span style="color: #94a3b8;">이미지 없음</span></template>
</div>
</div>
</template>
<!-- Auto Wireframe (no user image) -->
<template x-if="!previewCard.image && previewCard.type !== 'comparison'">
<div class="di-preview-wireframe" x-html="getWireframe(previewCard)"></div>
</template>
<!-- Guidelines (below wireframe) -->
<template x-if="previewCard.guidelines || previewCard.memo || previewCard.suggestion || previewCard.effect">
<div style="padding: 16px 20px; border-top: 1px solid var(--di-border); background: #fff;">
<template x-if="previewCard.memo">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">인사이트 메모</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7;" x-text="previewCard.memo"></div>
</div>
</template>
<template x-if="previewCard.guidelines">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">사용 가이드라인</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #f0fdf4; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #10b981;" x-text="previewCard.guidelines"></div>
</div>
</template>
<template x-if="previewCard.suggestion">
<div style="margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">개선 제안</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #fffbeb; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #f59e0b;" x-text="previewCard.suggestion"></div>
</div>
</template>
<template x-if="previewCard.effect">
<div>
<div style="font-size: 11px; font-weight: 700; color: var(--di-text-secondary); margin-bottom: 4px;">개선 효과</div>
<div style="font-size: 13px; color: var(--di-text); line-height: 1.7; background: #eff6ff; padding: 10px 14px; border-radius: 8px; border-left: 3px solid #3b82f6;" x-text="previewCard.effect"></div>
</div>
</template>
</div>
</template>
</div>
<!-- Right: Info Panel -->
<div class="di-preview-right">
<!-- Rating -->
<div class="di-preview-info-item">
<label>평점</label>
<div class="card-rating" style="font-size: 18px; letter-spacing: 2px;" x-text="getRatingStars(previewCard.rating || 0)"></div>
</div>
<!-- Category -->
<div class="di-preview-info-item">
<label>카테고리</label>
<div class="val" x-text="getCategoryLabel(previewCard.category)"></div>
</div>
<!-- Tags -->
<template x-if="(previewCard.tags || []).length > 0">
<div class="di-preview-info-item">
<label>태그</label>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
<template x-for="tag in previewCard.tags" :key="tag">
<span class="card-tag" x-text="tag" style="font-size: 11.5px; padding: 3px 8px;"></span>
</template>
</div>
</div>
</template>
<!-- Source -->
<template x-if="previewCard.source">
<div class="di-preview-info-item">
<label>출처</label>
<div class="val" x-text="previewCard.source"></div>
</div>
</template>
<!-- Used In (Pattern) -->
<template x-if="(previewCard.usedIn || []).length > 0">
<div class="di-preview-info-item">
<label>사용처</label>
<div style="display: flex; flex-direction: column; gap: 2px;">
<template x-for="u in previewCard.usedIn" :key="u">
<div style="font-size: 12.5px; color: var(--di-text); padding: 2px 0;"> <span x-text="u"></span></div>
</template>
</div>
</div>
</template>
<!-- Components (Pattern) -->
<template x-if="(previewCard.components || []).length > 0 && previewCard.components[0]?.name">
<div class="di-preview-info-item">
<label>구성 요소</label>
<div>
<template x-for="comp in previewCard.components" :key="comp.name">
<div class="di-preview-comp">
<span :class="comp.required ? 'chk' : 'opt'" x-text="comp.required ? '✓' : '○'"></span>
<span x-text="comp.name"></span>
</div>
</template>
</div>
</div>
</template>
<!-- CRAP Principles (Analysis) -->
<template x-if="previewCard.type === 'analysis' && previewCard.principles">
<div class="di-preview-info-item">
<label>디자인 원칙</label>
<div style="display: flex; flex-direction: column; gap: 3px;">
<template x-for="p in designPrinciples" :key="p.key">
<template x-if="previewCard.principles[p.key]">
<div style="font-size: 12px; display: flex; gap: 6px; align-items: center;">
<span x-text="({pass:'✅',warn:'⚠️',fail:'❌'})[previewCard.principles[p.key]] || '—'"></span>
<span x-text="p.label"></span>
</div>
</template>
</template>
</div>
</div>
</template>
<!-- Changes (Comparison) -->
<template x-if="(previewCard.changes || []).length > 0">
<div class="di-preview-info-item">
<label>변경 포인트</label>
<div style="display: flex; flex-direction: column; gap: 3px;">
<template x-for="(c, i) in previewCard.changes" :key="i">
<div style="font-size: 12.5px; color: var(--di-text); line-height: 1.5;" x-text="c"></div>
</template>
</div>
</div>
</template>
<!-- Date -->
<div class="di-preview-info-item" style="margin-top: auto; padding-top: 12px; border-top: 1px solid #f1f5f9;">
<label>생성일</label>
<div class="val" x-text="formatDate(previewCard.createdAt)"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- ===== Help Modal ===== -->
<template x-if="showHelpModal">
<div class="di-modal-overlay" @click.self="showHelpModal = false">
<div class="di-modal di-help-modal">
<div class="di-modal-header">
<h3><i class="ri-question-line" style="color: var(--di-blue);"></i> 디자인 인사이트 도움말</h3>
<button class="di-btn ghost" @click="showHelpModal = false"><i class="ri-close-line" style="font-size: 18px;"></i></button>
</div>
<div class="di-help-tabs">
<button class="di-help-tab" :class="helpTab === 'overview' && 'active'" @click="helpTab = 'overview'">개요</button>
<button class="di-help-tab" :class="helpTab === 'toolbar' && 'active'" @click="helpTab = 'toolbar'">툴바</button>
<button class="di-help-tab" :class="helpTab === 'cards' && 'active'" @click="helpTab = 'cards'">카드 유형</button>
<button class="di-help-tab" :class="helpTab === 'views' && 'active'" @click="helpTab = 'views'"> 모드</button>
<button class="di-help-tab" :class="helpTab === 'sidebar' && 'active'" @click="helpTab = 'sidebar'">사이드바</button>
<button class="di-help-tab" :class="helpTab === 'shortcuts' && 'active'" @click="helpTab = 'shortcuts'">단축키</button>
<button class="di-help-tab" :class="helpTab === 'workflow' && 'active'" @click="helpTab = 'workflow'">워크플로우</button>
</div>
<div class="di-help-content">
<!-- 개요 -->
<template x-if="helpTab === 'overview'">
<div>
<div class="di-help-section">
<h4><i class="ri-palette-line"></i> 디자인 인사이트란?</h4>
<p>SAM 화면을 만들 참고할 <strong>UI/UX 디자인 레퍼런스를 수집하고, 분석하고, 패턴으로 축적</strong>하는 연구 도구입니다. 외부 서비스(Dribbble, Mobbin ) 기존 SAM 화면의 스크린샷을 캡처하여 인사이트를 기록하고, 팀과 공유할 있습니다.</p>
</div>
<div class="di-help-section">
<h4><i class="ri-lightbulb-line"></i> 핵심 가치</h4>
<div class="di-help-grid">
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;">📷</div>
<div class="h-body">
<div class="h-title">레퍼런스 수집</div>
<div class="h-desc">좋은 화면을 스크린샷으로 수집하고 좋은지 메모</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fef3c7;">🔍</div>
<div class="h-body">
<div class="h-title">화면 분석</div>
<div class="h-desc">CRAP 디자인 원칙으로 화면의 장단점 분석</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #ecfdf5;">📐</div>
<div class="h-body">
<div class="h-title">패턴 라이브러리</div>
<div class="h-desc">반복 사용할 UI 패턴을 템플릿으로 등록</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fae8ff;">🔄</div>
<div class="h-body">
<div class="h-title">Before/After 비교</div>
<div class="h-desc">개선 전후를 비교하여 디자인 결정 근거 기록</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4><i class="ri-rocket-line"></i> 빠른 시작</h4>
<div class="di-help-step">
<div class="step-num">1</div>
<div class="step-body">
<div class="step-title">스크린샷 캡처</div>
<div class="step-desc">참고할 화면을 <span class="di-help-kbd">Win + Shift + S</span> 또는 캡처 도구로 스크린샷을 찍으세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">2</div>
<div class="step-body">
<div class="step-title">Ctrl+V 붙여넣기</div>
<div class="step-desc"> 화면에서 <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span> 누르면 자동으로 카드가 생성되고 이미지가 붙여넣어집니다.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">3</div>
<div class="step-body">
<div class="step-title">정보 입력</div>
<div class="step-desc">제목, 인사이트 메모, 출처, 태그, 카테고리, 평점을 입력하고 저장하세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">4</div>
<div class="step-body">
<div class="step-title">분류 검색</div>
<div class="step-desc">카테고리 , 태그 필터, 검색으로 원하는 인사이트를 빠르게 찾으세요.</div>
</div>
</div>
</div>
</div>
</template>
<!-- 툴바 -->
<template x-if="helpTab === 'toolbar'">
<div>
<div class="di-help-section">
<h4><i class="ri-tools-line"></i> 상단 툴바 기능</h4>
<p>화면 최상단의 도구 모음입니다. 프로젝트 관리, 저장, 전환 주요 기능에 접근합니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-layout-left-line"></i></div>
<div class="h-body">
<div class="h-title">사이드바 토글 (좌측 번째 아이콘)</div>
<div class="h-desc">좌측 사이드바(카테고리, 태그, 검색, 정렬) 접거나 펼칩니다. 넓은 화면이 필요할 접으세요.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;">📝</div>
<div class="h-body">
<div class="h-title">프로젝트 제목 (입력 필드)</div>
<div class="h-desc">현재 프로젝트의 이름입니다. 클릭하여 직접 수정할 있습니다. : "SAM v2 디자인 연구"</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;"><i class="ri-save-line"></i></div>
<div class="h-body">
<div class="h-title">저장 버튼</div>
<div class="h-desc">현재 프로젝트를 브라우저 localStorage에 저장합니다. <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">S</span>로도 가능합니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;"><i class="ri-download-line"></i></div>
<div class="h-body">
<div class="h-title">내보내기 버튼</div>
<div class="h-desc"><strong>JSON 내보내기</strong>: 프로젝트 전체를 JSON 파일로 다운로드 (백업용)<br><strong>JSON 가져오기</strong>: 이전에 내보낸 JSON 파일을 불러와 복원</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-layout-grid-line"></i></div>
<div class="h-body">
<div class="h-title"> 전환 (보드 / 갤러리 / 리스트)</div>
<div class="h-desc">카드 표시 방식을 변경합니다. 보드(격자), 갤러리(이미지 중심), 리스트(테이블형) 선택하세요.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-folder-line"></i></div>
<div class="h-body">
<div class="h-title">프로젝트 관리 버튼</div>
<div class="h-desc">여러 연구 프로젝트를 만들고 전환할 있습니다. : "대시보드 연구", "목록 화면 연구" 주제별 분리.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #dbeafe;"><i class="ri-question-line"></i></div>
<div class="h-body">
<div class="h-title">도움말 버튼 (지금 보고 있는 화면)</div>
<div class="h-desc"> 도움말 모달을 엽니다.</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4><i class="ri-layout-top-line"></i> 카테고리 (툴바 아래)</h4>
<p>카드의 <strong>유형별 필터</strong>입니다. "전체" 누르면 모든 카드를, 특정 유형을 누르면 해당 유형만 표시합니다. 옆의 숫자는 해당 유형의 카드 수입니다.</p>
<div class="di-help-grid">
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;">📷</div>
<div class="h-body"><div class="h-title">레퍼런스</div><div class="h-desc">외부/내부 화면 스크린샷 수집</div></div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fef3c7;">🔍</div>
<div class="h-body"><div class="h-title">분석</div><div class="h-desc">화면 분석 + CRAP 디자인 원칙 체크</div></div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #ecfdf5;">📐</div>
<div class="h-body"><div class="h-title">패턴</div><div class="h-desc">반복 사용할 UI 패턴 등록</div></div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fae8ff;">🔄</div>
<div class="h-body"><div class="h-title">Before/After</div><div class="h-desc">개선 전후 비교</div></div>
</div>
</div>
</div>
</div>
</template>
<!-- 카드 유형 -->
<template x-if="helpTab === 'cards'">
<div>
<div class="di-help-section">
<h4 style="color: #2563eb;"><i class="ri-camera-line"></i> 1. 레퍼런스 카드</h4>
<p>외부 서비스나 내부 화면의 <strong>스크린샷을 수집</strong>하고 좋은지(또는 나쁜지) 메모를 남깁니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-body">
<div class="h-title">입력 항목</div>
<div class="h-desc">
<strong>이미지</strong> 스크린샷 (클릭/드래그/Ctrl+V)<br>
<strong>인사이트 메모</strong> "카드형 레이아웃이 정보 밀도를 유지하면서도 깔끔"<br>
<strong>출처</strong> 출처 URL이나 이름 (: notion.so, Figma)<br>
<strong>태그</strong> 자유 태그 (콤마 구분)<br>
<strong>카테고리</strong> 대시보드, 목록, 상세/ 화면 유형<br>
<strong>평점</strong> 1~5 (참고 가치 평가)
</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4 style="color: #d97706;"><i class="ri-search-eye-line"></i> 2. 분석 카드</h4>
<p>화면을 <strong>디자인 원칙(CRAP)으로 분석</strong>하고 개선 제안을 기록합니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-body">
<div class="h-title">CRAP 디자인 원칙</div>
<div class="h-desc">
원칙을 클릭하면 상태가 순환합니다: <strong> 통과 ⚠️ 주의 미달</strong><br><br>
<strong>C</strong>ontrast (대비) 중요 요소가 시각적으로 구분되는가?<br>
<strong>R</strong>epetition (반복) 일관된 스타일이 반복 적용되는가?<br>
<strong>A</strong>lignment (정렬) 요소들이 논리적으로 정렬되어 있는가?<br>
<strong>P</strong>roximity (근접성) 관련 요소가 가까이 그룹핑되어 있는가?<br>
+ 여백, 계층, 일관성, 접근성 체크
</div>
</div>
</div>
<div class="di-help-item">
<div class="h-body">
<div class="h-title">추가 입력</div>
<div class="h-desc">
<strong>개선 제안</strong> "검색 영역을 접을 수 있게 하고 버튼 그룹을 우측 정렬"<br>
<strong>심각도</strong> 정보() / 경고(⚠️) / 심각(🔴)
</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4 style="color: #059669;"><i class="ri-layout-masonry-line"></i> 3. 패턴 카드</h4>
<p>반복 사용할 <strong>UI 패턴을 템플릿으로 등록</strong>하여 화면 설계 재사용합니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-body">
<div class="h-title">입력 항목</div>
<div class="h-desc">
<strong>사용처</strong> 패턴이 사용된 화면 (: "수주 목록, 거래처 목록")<br>
<strong>구성 요소</strong> 패턴을 이루는 요소 체크리스트 (검색바, 필터 , 테이블 )<br>
<strong>사용 가이드라인</strong> 패턴 사용 주의사항
</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4 style="color: #a855f7;"><i class="ri-arrow-left-right-line"></i> 4. Before/After 카드</h4>
<p>디자인 <strong>개선 전후를 나란히 비교</strong>하여 변경 근거와 효과를 기록합니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-body">
<div class="h-title">입력 항목</div>
<div class="h-desc">
<strong>Before 이미지</strong> 개선 스크린샷<br>
<strong>After 이미지</strong> 개선 스크린샷<br>
<strong>변경 포인트</strong> 무엇을 어떻게 바꿨는지 ( 단위 입력)<br>
<strong>개선 효과</strong> "스크롤 40% 감소, 작업 완료 시간 단축"
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 모드 -->
<template x-if="helpTab === 'views'">
<div>
<div class="di-help-section">
<h4><i class="ri-layout-grid-line"></i> 모드 (3)</h4>
<p>우측 상단의 아이콘 탭으로 카드 표시 방식을 전환합니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;"><i class="ri-layout-grid-line" style="font-size: 20px; color: var(--di-blue);"></i></div>
<div class="h-body">
<div class="h-title">보드 (기본)</div>
<div class="h-desc">카드를 격자(그리드) 배열합니다. 이미지 썸네일 + 제목 + 태그 + 평점이 한눈에 보여 <strong>전체 현황 파악</strong> 적합합니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #ecfdf5;"><i class="ri-image-line" style="font-size: 20px; color: #059669;"></i></div>
<div class="h-body">
<div class="h-title">갤러리 </div>
<div class="h-desc">이미지를 크게 표시하는 뷰입니다. <strong>시각적 비교</strong> <strong>레퍼런스 브라우징</strong> 최적화되어 있습니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fef3c7;"><i class="ri-list-unordered" style="font-size: 20px; color: #d97706;"></i></div>
<div class="h-body">
<div class="h-title">리스트 </div>
<div class="h-desc">테이블 형태로 줄씩 표시합니다. <strong>대량 데이터 관리</strong>, 빠른 스캔, 태그 확인에 효율적입니다.</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 사이드바 -->
<template x-if="helpTab === 'sidebar'">
<div>
<div class="di-help-section">
<h4><i class="ri-layout-left-line"></i> 좌측 사이드바</h4>
<p>카드를 <strong>필터링하고 검색</strong>하는 패널입니다. 사이드바 토글 버튼으로 접거나 있습니다.</p>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-search-line"></i></div>
<div class="h-body">
<div class="h-title">검색</div>
<div class="h-desc">제목, 메모, 태그, 출처에서 키워드를 검색합니다. <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">F</span> 포커스 가능.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;">📊</div>
<div class="h-body">
<div class="h-title">카테고리 (화면 유형)</div>
<div class="h-desc">대시보드, 목록, 상세/, 모달, 네비게이션, 로그인, 보고서, 기타 화면 유형별로 카드를 필터링합니다. 숫자는 해당 카테고리의 카드 .</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;">🏷️</div>
<div class="h-body">
<div class="h-title">태그 필터</div>
<div class="h-desc">카드에 추가한 태그 목록이 표시됩니다. <strong>태그를 클릭하면 해당 태그가 포함된 카드만</strong> 표시합니다. 여러 태그를 선택하면 OR 조건으로 필터링됩니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #f1f5f9;"><i class="ri-sort-asc"></i></div>
<div class="h-body">
<div class="h-title">정렬</div>
<div class="h-desc">최신순 / 오래된순 / 평점순 / 이름순으로 카드를 정렬합니다. (📌) 고정된 카드는 항상 최상단에 표시됩니다.</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 단축키 -->
<template x-if="helpTab === 'shortcuts'">
<div>
<div class="di-help-section">
<h4><i class="ri-keyboard-line"></i> 키보드 단축키</h4>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span></div>
<div class="h-desc"><strong>클립보드 이미지 붙여넣기</strong> 스크린샷 캡처 화면에서 Ctrl+V 하면 자동으로 레퍼런스 카드가 생성됩니다. 가장 핵심적인 기능!</div>
</div>
</div>
<div class="di-help-item">
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">S</span></div>
<div class="h-desc"><strong>프로젝트 저장</strong> 현재 상태를 localStorage에 저장합니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">N</span></div>
<div class="h-desc"><strong> 카드 추가</strong> 레퍼런스 카드 생성 모달을 엽니다.</div>
</div>
</div>
<div class="di-help-item">
<div class="h-body" style="display: flex; align-items: center; gap: 16px;">
<div style="flex-shrink: 0;"><span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">F</span></div>
<div class="h-desc"><strong>검색 포커스</strong> 사이드바 검색 입력란으로 포커스를 이동합니다.</div>
</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4><i class="ri-mouse-line"></i> 이미지 입력 방법 (3가지)</h4>
<div class="di-help-grid" style="grid-template-columns: 1fr;">
<div class="di-help-item">
<div class="h-icon" style="background: #eff6ff;"><i class="ri-clipboard-line"></i></div>
<div class="h-body">
<div class="h-title">1. 클립보드 붙여넣기 (가장 빠름)</div>
<div class="h-desc">메인 화면에서 <span class="di-help-kbd">Ctrl</span>+<span class="di-help-kbd">V</span> 카드 자동 생성</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #ecfdf5;"><i class="ri-upload-line"></i></div>
<div class="h-body">
<div class="h-title">2. 파일 업로드</div>
<div class="h-desc">카드 편집 모달에서 이미지 영역을 클릭 파일 선택</div>
</div>
</div>
<div class="di-help-item">
<div class="h-icon" style="background: #fef3c7;"><i class="ri-drag-drop-line"></i></div>
<div class="h-body">
<div class="h-title">3. 드래그 드롭</div>
<div class="h-desc">카드 편집 모달에서 이미지 영역에 파일을 끌어다 놓기</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 워크플로우 -->
<template x-if="helpTab === 'workflow'">
<div>
<div class="di-help-section">
<h4><i class="ri-flow-chart"></i> 추천 워크플로우</h4>
<p>디자인 인사이트를 효과적으로 활용하는 단계별 흐름입니다.</p>
<div class="di-help-step">
<div class="step-num">1</div>
<div class="step-body">
<div class="step-title">프로젝트 생성</div>
<div class="step-desc">연구 주제별로 프로젝트를 생성합니다. : "SAM 대시보드 리뉴얼", "목록 화면 개선"</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">2</div>
<div class="step-body">
<div class="step-title">레퍼런스 수집</div>
<div class="step-desc">Dribbble, Mobbin, 경쟁 서비스 등에서 좋은 화면을 스크린샷으로 수집합니다. Ctrl+V로 빠르게 추가하고, 태그와 카테고리를 분류하세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">3</div>
<div class="step-body">
<div class="step-title">화면 분석</div>
<div class="step-desc">SAM 기존 화면을 분석 카드로 만들어 CRAP 원칙을 체크합니다. 어떤 부분이 부족한지 개선 제안을 기록하세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">4</div>
<div class="step-body">
<div class="step-title">패턴 추출</div>
<div class="step-desc">레퍼런스에서 반복되는 좋은 패턴을 발견하면 패턴 카드로 등록합니다. 구성 요소와 사용 가이드라인을 정리하세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">5</div>
<div class="step-body">
<div class="step-title">Before/After 기록</div>
<div class="step-desc">화면을 개선한 전후 비교 카드를 만들어 변경 포인트와 효과를 기록합니다. 회의에서 근거 자료로 활용하세요.</div>
</div>
</div>
<div class="di-help-step">
<div class="step-num">6</div>
<div class="step-body">
<div class="step-title">기획디자인 연계</div>
<div class="step-desc">축적된 패턴과 인사이트를 참고하여 기획디자인 메뉴에서 스토리보드를 작성합니다.</div>
</div>
</div>
</div>
<div class="di-help-section">
<h4><i class="ri-information-line"></i> 데이터 저장 안내</h4>
<p>
모든 데이터는 <strong>브라우저 localStorage</strong> 저장됩니다.<br>
브라우저 데이터를 삭제하면 인사이트도 함께 삭제되므로, 중요한 프로젝트는 <strong>JSON 내보내기</strong> 백업해두세요.<br>
다른 PC에서 작업하려면 JSON 내보내기 가져오기로 이동할 있습니다.
</p>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- Toast -->
<template x-if="toastMsg">
<div class="di-toast" x-text="toastMsg" x-init="setTimeout(() => toastMsg = '', 2500)"></div>
</template>
</div>
@endsection
@push('scripts')
<script>
function designInsight() {
return {
// State
projects: [],
currentProject: { id: '', title: '', cards: [], createdAt: '', updatedAt: '' },
viewMode: 'board', // board | gallery | list
categoryFilter: 'all', // card type filter (tabs)
screenFilter: 'all', // screen category filter (sidebar)
searchQuery: '',
selectedTags: [],
sortBy: 'newest',
sidebarOpen: true,
showCardModal: false,
showProjectsModal: false,
showExportMenu: false,
showHelpModal: false,
helpTab: 'overview',
showPreviewModal: false,
previewCard: null,
editingCard: {},
newProjectTitle: '',
toastMsg: '',
lastSaved: '',
undoStack: [],
redoStack: [],
// Constants
cardTypes: [
{ code: 'reference', label: '레퍼런스', icon: '📷' },
{ code: 'analysis', label: '분석', icon: '🔍' },
{ code: 'pattern', label: '패턴', icon: '📐' },
{ code: 'comparison', label: 'Before/After', icon: '🔄' },
],
categories: [
{ code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' },
{ code: 'list', label: '목록', icon: '📋', color: '#3b82f6' },
{ code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' },
{ code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' },
{ code: 'navigation', label: '네비게이션', icon: '🧭', color: '#8b5cf6' },
{ code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' },
{ code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' },
{ code: 'etc', label: '기타', icon: '📎', color: '#64748b' },
],
designPrinciples: [
{ key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' },
{ key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' },
{ key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' },
{ key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' },
{ key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' },
{ key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' },
{ key: 'consistency', label: '일관성 (Consistency)', icon: '🔗', desc: '다른 화면과의 일관성' },
{ key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' },
],
// Init
init() {
this.loadProjects();
if (this.projects.length === 0) {
this.createDefaultProject();
}
this.loadCurrentProject();
},
// ===== Projects =====
loadProjects() {
try {
const data = localStorage.getItem('di_projects');
this.projects = data ? JSON.parse(data) : [];
} catch { this.projects = []; }
},
saveProjects() {
localStorage.setItem('di_projects', JSON.stringify(this.projects));
},
createDefaultProject() {
const proj = {
id: 'diproj_' + Date.now(),
title: 'SAM 디자인 연구',
description: '',
cards: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.projects.push(proj);
localStorage.setItem('di_current', proj.id);
this.saveProjects();
},
loadCurrentProject() {
const currentId = localStorage.getItem('di_current');
const proj = this.projects.find(p => p.id === currentId) || this.projects[0];
if (proj) {
this.currentProject = proj;
localStorage.setItem('di_current', proj.id);
}
},
createProject() {
if (!this.newProjectTitle.trim()) return;
const proj = {
id: 'diproj_' + Date.now(),
title: this.newProjectTitle.trim(),
description: '',
cards: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.projects.push(proj);
this.saveProjects();
this.switchProject(proj.id);
this.newProjectTitle = '';
this.toast('프로젝트 생성됨');
},
switchProject(id) {
this.saveProject();
const proj = this.projects.find(p => p.id === id);
if (proj) {
this.currentProject = proj;
localStorage.setItem('di_current', id);
this.clearFilters();
this.showProjectsModal = false;
}
},
deleteProject(id) {
if (this.projects.length <= 1) return;
if (!confirm('프로젝트를 삭제하시겠습니까?')) return;
this.projects = this.projects.filter(p => p.id !== id);
if (this.currentProject.id === id) {
this.currentProject = this.projects[0];
localStorage.setItem('di_current', this.currentProject.id);
}
this.saveProjects();
this.toast('프로젝트 삭제됨');
},
saveProject() {
this.currentProject.updatedAt = new Date().toISOString();
const idx = this.projects.findIndex(p => p.id === this.currentProject.id);
if (idx >= 0) this.projects[idx] = { ...this.currentProject };
this.saveProjects();
this.lastSaved = this.formatTime(new Date());
},
// ===== Cards =====
openNewCardModal(type) {
this.editingCard = {
id: '',
type: type || 'reference',
title: '',
image: '',
memo: '',
source: '',
tags: [],
tagsText: '',
category: 'etc',
rating: 0,
pinned: false,
archived: false,
// Analysis
principles: {},
suggestion: '',
severity: 'info',
// Pattern
usedIn: [],
usedInText: '',
components: [{ name: '', required: true }],
guidelines: '',
frequency: 0,
// Comparison
beforeImage: '',
afterImage: '',
changes: [],
changesText: '',
effect: '',
};
this.showCardModal = true;
},
openPreviewModal(card) {
this.previewCard = card;
this.showPreviewModal = true;
},
getCategoryLabel(code) {
const c = this.categories.find(cat => cat.code === code);
return c ? c.icon + ' ' + c.label : code;
},
getWireframe(card) {
const t = (card.title || '').toLowerCase();
const tags = (card.tags || []).join(' ').toLowerCase();
const key = t + ' ' + tags;
// KPI 대시보드
if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<div class="wf-circle" style="width: 16px; height: 16px; background: #bfdbfe;"></div>
<div class="wf-bar md dark"></div><div style="flex:1;"></div>
<div class="wf-bar sm" style="background: #bfdbfe; border-radius: 4px; height: 14px;"></div>
</div>
<div style="padding: 12px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">매출</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">₩24.5M</div><div style="font-size: 8px; color: #10b981;">▲ 12.5%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">수주</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">148건</div><div style="font-size: 8px; color: #10b981;">▲ 8.2%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">미수금</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">₩3.2M</div><div style="font-size: 8px; color: #ef4444;">▲ 2.1%</div></div>
<div class="wf-box" style="padding: 10px; text-align: center;"><div class="wf-text">거래처</div><div style="font-size: 14px; font-weight: 800; color: #1e293b; margin: 4px 0;">52개</div><div style="font-size: 8px; color: #10b981;">▲ 3건</div></div>
</div>
<div style="padding: 0 12px 12px; display: grid; grid-template-columns: 2fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 12px; height: 100px;">
<div class="wf-text" style="margin-bottom: 8px;">월별 매출 추이</div>
<svg width="100%" height="50" viewBox="0 0 200 50"><polyline points="10,40 40,30 70,35 100,20 130,25 160,10 190,15" fill="none" stroke="#3b82f6" stroke-width="2"/><polyline points="10,40 40,30 70,35 100,20 130,25 160,10 190,15" fill="url(#grad)" stroke="none"/><defs><linearGradient id="grad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3b82f6" stop-opacity="0.2"/><stop offset="100%" stop-color="#3b82f6" stop-opacity="0"/></linearGradient></defs></svg>
</div>
<div class="wf-box" style="padding: 12px; height: 100px;">
<div class="wf-text" style="margin-bottom: 8px;">최근 활동</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#10b981;"></div><div class="wf-bar lg"></div></div>
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#3b82f6;"></div><div class="wf-bar xl"></div></div>
<div style="display:flex;gap:6px;align-items:center;"><div class="wf-circle" style="width:8px;height:8px;background:#f59e0b;"></div><div class="wf-bar md"></div></div>
</div>
</div>
</div>
</div>`;
// 데이터 테이블
if (key.includes('테이블') || key.includes('검색/필터') || key.includes('페이지네이션')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 10px 12px; border-bottom: 1px solid #e2e8f0; display: flex; gap: 8px; align-items: center;">
<div style="flex:1; height: 28px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; padding: 0 10px;"><span style="color: #94a3b8; font-size: 9px;">🔍 검색어를 입력하세요...</span></div>
<div style="display: flex; gap: 4px;">
<div style="padding: 4px 10px; background: #eff6ff; border-radius: 4px; font-size: 8px; color: #3b82f6;">상태 ▾</div>
<div style="padding: 4px 10px; background: #f1f5f9; border-radius: 4px; font-size: 8px; color: #64748b;">날짜 ▾</div>
</div>
</div>
<div style="padding: 0 12px;">
<div style="display: grid; grid-template-columns: 30px 2fr 1fr 1fr 80px; gap: 8px; padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-size: 8px; font-weight: 700; color: #94a3b8;">
<div>☐</div><div>이름 ↕</div><div>상태</div><div>날짜</div><div>액션</div>
</div>
${[1,2,3,4,5].map(i => `<div style="display: grid; grid-template-columns: 30px 2fr 1fr 1fr 80px; gap: 8px; padding: 7px 0; border-bottom: 1px solid #f1f5f9; font-size: 8px; color: #475569; align-items: center;">
<div></div><div class="wf-bar" style="width:${60+i*15}px;height:8px;"></div>
<div><span style="padding:2px 6px;border-radius:3px;background:${['#ecfdf5','#fef3c7','#eff6ff'][i%3]};font-size:7px;">${['완료','진행중','대기'][i%3]}</span></div>
<div style="color:#94a3b8;">2026-03-0${i}</div>
<div style="display:flex;gap:4px;"><span style="color:#3b82f6;font-size:7px;">보기</span><span style="color:#94a3b8;font-size:7px;">삭제</span></div>
</div>`).join('')}
</div>
<div style="padding: 8px 12px; border-top: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 8px; color: #94a3b8;">1-5 / 128건</span>
<div style="display: flex; gap: 3px;">
${['◀','1','2','3','...','26','▶'].map(p => `<span style="padding:2px 6px;border-radius:3px;background:${p==='1'?'#3b82f6':'#f1f5f9'};color:${p==='1'?'#fff':'#64748b'};font-size:8px;">${p}</span>`).join('')}
</div>
</div>
</div>`;
// 칸반 보드
if (key.includes('칸반') || key.includes('kanban')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<div class="wf-bar md dark"></div><div style="flex:1;"></div>
<div style="font-size: 8px; color: #94a3b8;">필터 </div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0; min-height: 200px;">
${['할 일|3|#94a3b8','진행 중|2|#3b82f6','검토|1|#f59e0b','완료|4|#10b981'].map(col => {
const [name,cnt,color] = col.split('|');
return `<div style="border-right: 1px solid #f1f5f9; padding: 8px 6px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:8px;font-weight:700;color:${color};">${name}</span>
<span style="font-size:7px;background:#f1f5f9;padding:1px 5px;border-radius:8px;color:#94a3b8;">${cnt}</span>
</div>
${Array.from({length:parseInt(cnt)},(_,i)=>`<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:6px 8px;margin-bottom:4px;box-shadow:0 1px 2px rgba(0,0,0,.04);">
<div class="wf-bar" style="width:${50+i*20}px;height:7px;margin-bottom:4px;"></div>
<div style="display:flex;gap:3px;"><span style="font-size:6px;padding:1px 4px;background:#f1f5f9;border-radius:2px;">태그</span></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:4px;">
<div class="wf-circle" style="width:12px;height:12px;background:${color};opacity:.3;"></div>
<span style="font-size:6px;color:#94a3b8;">3/8</span>
</div>
</div>`).join('')}
<div style="text-align:center;padding:4px;font-size:8px;color:#94a3b8;border:1px dashed #e2e8f0;border-radius:4px;cursor:pointer;">+ 추가</div>
</div>`;
}).join('')}
</div>
</div>`;
// Command Palette
if (key.includes('command') || key.includes('cmd+k') || key.includes('커맨드')) return `
<div style="width: 100%; max-width: 400px; background: rgba(0,0,0,.5); border-radius: 12px; padding: 40px 30px; display: flex; justify-content: center;">
<div style="width: 100%; background: #fff; border-radius: 10px; box-shadow: 0 20px 60px rgba(0,0,0,.3); overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid #e2e8f0;">
<div style="background: #f8fafc; border-radius: 6px; padding: 8px 12px; display: flex; align-items: center; gap: 8px;">
<span style="color: #94a3b8; font-size: 12px;">🔍</span>
<span style="font-size: 10px; color: #94a3b8;">명령어 또는 페이지 검색...</span>
</div>
</div>
<div style="padding: 6px;">
<div style="font-size: 8px; color: #94a3b8; padding: 4px 10px; font-weight: 600;">최근</div>
${['수주 관리|📋','거래처 목록|🏢','품목 기준관리|📦'].map(item => {
const [n,ic] = item.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;cursor:pointer;"><span>${ic}</span><span style="font-size:10px;flex:1;">${n}</span><span style="font-size:8px;color:#94a3b8;">↵</span></div>`;
}).join('')}
<div style="font-size: 8px; color: #94a3b8; padding: 4px 10px; font-weight: 600; margin-top: 4px;">액션</div>
${['새 수주 등록|','설정 열기|⚙️','도움말|❓'].map(item => {
const [n,ic] = item.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;cursor:pointer;"><span>${ic}</span><span style="font-size:10px;flex:1;">${n}</span></div>`;
}).join('')}
</div>
<div style="padding: 6px 12px; border-top: 1px solid #f1f5f9; display: flex; gap: 12px;">
<span style="font-size: 7px; color: #94a3b8;">↑↓ 이동</span>
<span style="font-size: 7px; color: #94a3b8;"> 선택</span>
<span style="font-size: 7px; color: #94a3b8;">ESC 닫기</span>
</div>
</div>
</div>`;
// 사이드바
if (key.includes('사이드바') && key.includes('네비게이션')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 240px;">
<div style="width: 160px; background: #1e293b; padding: 12px 8px; display: flex; flex-direction: column; gap: 2px; flex-shrink: 0;">
<div style="display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-bottom: 8px;"><div class="wf-circle" style="width: 20px; height: 20px; background: #3b82f6;"></div><span style="font-size: 10px; color: #fff; font-weight: 700;">SAM</span></div>
<div style="font-size: 7px; color: #64748b; padding: 4px 8px; font-weight: 600;">메인</div>
${['📊 대시보드|true','📋 수주관리|false','🏢 거래처|false','📦 품목관리|false'].map(m => {
const [n,a] = m.split('|');
return `<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:4px;font-size:9px;color:${a==='true'?'#fff':'#94a3b8'};background:${a==='true'?'rgba(59,130,246,.2)':'transparent'};${a==='true'?'border-left:2px solid #3b82f6;':''}">${n}</div>`;
}).join('')}
<div style="font-size: 7px; color: #64748b; padding: 4px 8px; font-weight: 600; margin-top: 6px;">관리</div>
${['⚙️ 설정','👥 사용자','🔔 알림'].map(n => `<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;font-size:9px;color:#94a3b8;">${n}</div>`).join('')}
<div style="flex:1;"></div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;border-top:1px solid #334155;margin-top:8px;"><div class="wf-circle" style="width:18px;height:18px;background:#64748b;"></div><span style="font-size:8px;color:#94a3b8;">홍길동</span></div>
</div>
<div style="flex: 1; padding: 16px; background: #f8fafc;">
<div class="wf-bar xl dark" style="height: 12px; margin-bottom: 12px;"></div>
<div class="wf-bar" style="width: 90%; height: 8px; margin-bottom: 6px;"></div>
<div class="wf-bar" style="width: 70%; height: 8px; margin-bottom: 16px;"></div>
<div class="wf-box" style="height: 120px; padding: 12px;"><div class="wf-text">콘텐츠 영역</div></div>
</div>
</div>`;
// 모달 폼
if (key.includes('모달') && (key.includes('폼') || key.includes('생성'))) return `
<div style="width: 100%; max-width: 420px; background: rgba(0,0,0,.4); border-radius: 12px; padding: 30px; display: flex; justify-content: center;">
<div style="width: 100%; background: #fff; border-radius: 10px; box-shadow: 0 20px 60px rgba(0,0,0,.2); overflow: hidden;">
<div style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 12px; font-weight: 700;"> 항목 등록</span><span style="color: #94a3b8; cursor: pointer;"></span>
</div>
<div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
${['이름 *','카테고리','설명'].map(f => `<div>
<div style="font-size: 9px; font-weight: 600; color: #475569; margin-bottom: 4px;">${f}</div>
<div style="height: ${f==='설명'?'48px':'28px'}; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;"></div>
</div>`).join('')}
</div>
<div style="padding: 12px 16px; border-top: 1px solid #e2e8f0; display: flex; justify-content: flex-end; gap: 6px;">
<div style="padding: 5px 14px; border-radius: 6px; font-size: 10px; color: #64748b; border: 1px solid #e2e8f0;">취소</div>
<div style="padding: 5px 14px; border-radius: 6px; font-size: 10px; color: #fff; background: #3b82f6;">저장</div>
</div>
</div>
</div>`;
// 설정
if (key.includes('설정') && key.includes('섹션')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 130px; border-right: 1px solid #e2e8f0; padding: 12px 6px; display: flex; flex-direction: column; gap: 1px;">
${['👤 프로필|true','🔔 알림','🔐 보안','🎨 테마','⚡ 연동','🗑️ 위험'].map(m => {
const [n,a] = m.split('|');
return `<div style="padding:5px 8px;border-radius:4px;font-size:9px;color:${a?'#3b82f6':'#64748b'};background:${a?'#eff6ff':'transparent'};font-weight:${a?'600':'400'};">${n}</div>`;
}).join('')}
</div>
<div style="flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 12px; overflow: hidden;">
<div style="font-size: 12px; font-weight: 700; color: #1e293b;">프로필 설정</div>
${['프로필 사진','이름','이메일'].map(f => `<div class="wf-box" style="padding: 10px 12px; display: flex; justify-content: space-between; align-items: center;">
<div><div style="font-size:9px;font-weight:600;color:#475569;">${f}</div><div class="wf-bar md" style="margin-top:3px;"></div></div>
<div style="font-size:8px;color:#3b82f6;">변경</div>
</div>`).join('')}
<div style="padding: 5px 12px; background: #3b82f6; color: #fff; border-radius: 6px; font-size: 9px; align-self: flex-start;">저장</div>
</div>
</div>`;
// 타임라인
if (key.includes('타임라인') || key.includes('활동') && key.includes('피드')) return `
<div class="wf-wrap" style="padding: 16px 16px 16px 32px;">
<div style="font-size: 10px; font-weight: 700; color: #1e293b; margin-bottom: 12px;">활동 기록</div>
<div style="position: relative; padding-left: 20px;">
<div style="position: absolute; left: 5px; top: 0; bottom: 0; width: 2px; background: #e2e8f0;"></div>
${[
{color:'#10b981',text:'수주 #1024 등록 완료',time:'10분 전',user:'김영업'},
{color:'#3b82f6',text:'거래처 "ABC상사" 정보 수정',time:'1시간 전',user:'이관리'},
{color:'#f59e0b',text:'견적서 #502 승인 대기',time:'3시간 전',user:'박대리'},
{color:'#8b5cf6',text:'품목 "BL-200" 단가 변경',time:'어제',user:'최기준'},
].map(e => `<div style="position:relative;margin-bottom:16px;">
<div style="position:absolute;left:-18px;top:2px;width:12px;height:12px;border-radius:50%;background:${e.color};border:2px solid #fff;"></div>
<div class="wf-box" style="padding:8px 12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
<span style="font-size:10px;font-weight:600;color:#1e293b;">${e.text}</span>
<span style="font-size:8px;color:#94a3b8;">${e.time}</span>
</div>
<span style="font-size:8px;color:#64748b;">${e.user}</span>
</div>
</div>`).join('')}
</div>
</div>`;
// 트리 + 상세
if (key.includes('트리') && key.includes('분할')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 150px; border-right: 1px solid #e2e8f0; padding: 10px 6px; font-size: 9px; overflow: hidden;">
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;font-weight:600;color:#1e293b;">📁 전체 메뉴</div>
<div style="padding-left:12px;">
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 영업관리</div>
<div style="padding-left:14px;">
<div style="padding:3px 4px;color:#3b82f6;background:#eff6ff;border-radius:3px;font-weight:600;">📄 수주관리</div>
<div style="padding:3px 4px;color:#64748b;">📄 거래처</div>
<div style="padding:3px 4px;color:#64748b;">📄 견적서</div>
</div>
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 생산관리</div>
<div style="display:flex;align-items:center;gap:4px;padding:3px 4px;color:#64748b;"> 📂 품질관리</div>
</div>
</div>
<div style="flex: 1; padding: 14px;">
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:10px;">📄 수주관리</div>
<div style="display:flex;flex-direction:column;gap:8px;">
${['메뉴명|수주관리','URL|/sales/orders','아이콘|shopping-cart','정렬|3'].map(f => {
const [k,v] = f.split('|');
return `<div style="display:flex;gap:8px;align-items:center;"><span style="font-size:8px;color:#94a3b8;width:40px;">${k}</span><div style="flex:1;height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 8px;font-size:9px;line-height:24px;color:#475569;">${v}</div></div>`;
}).join('')}
</div>
</div>
</div>`;
// 캘린더
if (key.includes('캘린더') || key.includes('일정')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="padding: 8px 12px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px;">
<span style="font-size:10px;color:#94a3b8;"></span>
<span style="font-size:11px;font-weight:700;color:#1e293b;">2026 3</span>
<span style="font-size:10px;color:#94a3b8;"></span>
<div style="flex:1;"></div>
<div style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">오늘</div>
<div style="display:flex;gap:2px;"><span style="font-size:8px;padding:2px 6px;background:#f1f5f9;border-radius:3px;"></span><span style="font-size:8px;padding:2px 6px;background:#eff6ff;color:#3b82f6;border-radius:3px;"></span></div>
</div>
<div style="display: grid; grid-template-columns: repeat(7, 1fr); font-size: 8px; text-align: center;">
${['일','월','화','수','목','금','토'].map(d => `<div style="padding:4px;color:#94a3b8;font-weight:600;border-bottom:1px solid #f1f5f9;">${d}</div>`).join('')}
${Array.from({length:35},(_, i) => {
const day = i - 5;
const isToday = day === 8;
const hasEvent = [3,8,12,15,22].includes(day);
return `<div style="padding:4px 2px;min-height:28px;border-bottom:1px solid #f8fafc;border-right:1px solid #f8fafc;${day<1||day>31?'color:#cbd5e1;':''}${isToday?'background:#eff6ff;':''}">
<div style="${isToday?'background:#3b82f6;color:#fff;width:16px;height:16px;border-radius:50%;margin:0 auto;line-height:16px;':'color:#475569;'}">${day<1?'':day>31?'':day}</div>
${hasEvent?`<div style="margin-top:1px;height:3px;background:${isToday?'#3b82f6':'#10b981'};border-radius:2px;"></div>`:''}
</div>`;
}).join('')}
</div>
</div>`;
// 카드 그리드
if (key.includes('카드') && key.includes('그리드')) return `
<div class="wf-wrap" style="padding: 12px;">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
${Array.from({length:6},(_,i) => `<div style="border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;">
<div style="height:60px;background:${['#bfdbfe','#bbf7d0','#fde68a','#ddd6fe','#fecaca','#e0e7ff'][i]};"></div>
<div style="padding:6px 8px;">
<div class="wf-bar" style="width:${50+i*10}px;height:7px;margin-bottom:4px;"></div>
<div class="wf-bar sm" style="height:6px;"></div>
</div>
</div>`).join('')}
</div>
</div>`;
// 가격표
if (key.includes('가격') || key.includes('플랜')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="text-align: center; margin-bottom: 12px;"><span style="font-size:11px;font-weight:700;color:#1e293b;">요금제 선택</span></div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
${[{name:'Basic',price:'₩49,000',color:'#64748b',pop:false},{name:'Pro',price:'₩99,000',color:'#3b82f6',pop:true},{name:'Enterprise',price:'문의',color:'#8b5cf6',pop:false}].map(p => `
<div style="border-radius:8px;border:${p.pop?'2px solid #3b82f6':'1px solid #e2e8f0'};padding:12px;text-align:center;position:relative;${p.pop?'box-shadow:0 4px 12px rgba(59,130,246,.15);':''}">
${p.pop?'<div style="position:absolute;top:-8px;left:50%;transform:translateX(-50%);background:#3b82f6;color:#fff;font-size:7px;padding:1px 8px;border-radius:8px;">인기</div>':''}
<div style="font-size:10px;font-weight:600;color:${p.color};margin-bottom:4px;">${p.name}</div>
<div style="font-size:16px;font-weight:800;color:#1e293b;margin-bottom:8px;">${p.price}</div>
${['기능 A','기능 B','기능 C'].map(f => `<div style="font-size:8px;color:#64748b;padding:2px 0;">✓ ${f}</div>`).join('')}
<div style="margin-top:8px;padding:4px;background:${p.pop?p.color:'#f1f5f9'};color:${p.pop?'#fff':'#64748b'};border-radius:4px;font-size:8px;">선택</div>
</div>
`).join('')}
</div>
</div>`;
// 채팅
if (key.includes('채팅') || key.includes('메시징')) return `
<div class="wf-wrap" style="display: flex; padding: 0; min-height: 220px;">
<div style="width: 120px; background: #f8fafc; border-right: 1px solid #e2e8f0; padding: 8px 4px;">
<div style="font-size:8px;font-weight:600;color:#94a3b8;padding:4px 6px;">채널</div>
${['# 일반','# 영업팀','# 개발팀'].map((c,i) => `<div style="padding:4px 6px;font-size:9px;border-radius:4px;${i===1?'background:#eff6ff;color:#3b82f6;font-weight:600;':'color:#64748b;'}">${c}</div>`).join('')}
<div style="font-size:8px;font-weight:600;color:#94a3b8;padding:4px 6px;margin-top:6px;">DM</div>
${['김영업','이관리'].map(c => `<div style="display:flex;align-items:center;gap:4px;padding:4px 6px;font-size:9px;color:#64748b;"><div class="wf-circle" style="width:12px;height:12px;"></div>${c}</div>`).join('')}
</div>
<div style="flex:1;display:flex;flex-direction:column;">
<div style="padding:6px 12px;border-bottom:1px solid #e2e8f0;font-size:10px;font-weight:600;">🔒 # 영업팀</div>
<div style="flex:1;padding:10px 12px;display:flex;flex-direction:column;gap:8px;overflow:hidden;">
<div style="display:flex;gap:6px;"><div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div><div><div style="font-size:8px;font-weight:600;color:#1e293b;">김영업 <span style="color:#94a3b8;font-weight:400;">10:30</span></div><div style="font-size:9px;color:#475569;background:#f1f5f9;padding:4px 8px;border-radius:0 8px 8px 8px;margin-top:2px;">수주건 확인 부탁드립니다</div></div></div>
<div style="display:flex;gap:6px;justify-content:flex-end;"><div><div style="font-size:9px;color:#fff;background:#3b82f6;padding:4px 8px;border-radius:8px 0 8px 8px;">확인했습니다 👍</div></div></div>
<div style="display:flex;gap:6px;"><div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div><div><div style="font-size:8px;font-weight:600;color:#1e293b;">이관리 <span style="color:#94a3b8;font-weight:400;">11:05</span></div><div style="font-size:9px;color:#475569;background:#f1f5f9;padding:4px 8px;border-radius:0 8px 8px 8px;margin-top:2px;">거래처 정보 업데이트 완료</div></div></div>
</div>
<div style="padding:8px;border-top:1px solid #e2e8f0;display:flex;gap:6px;align-items:center;">
<div style="flex:1;height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:0 8px;font-size:9px;line-height:28px;color:#94a3b8;">메시지 입력...</div>
<div style="font-size:10px;">📎</div><div style="font-size:10px;">😊</div>
</div>
</div>
</div>`;
// 파일 업로드
if (key.includes('파일') && key.includes('업로드')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="border: 2px dashed #cbd5e1; border-radius: 10px; padding: 20px; text-align: center; margin-bottom: 12px;">
<div style="font-size: 24px; margin-bottom: 4px;">📁</div>
<div style="font-size: 10px; color: #64748b;">파일을 드래그하거나 <span style="color: #3b82f6; text-decoration: underline;">클릭</span>하여 업로드</div>
<div style="font-size: 8px; color: #94a3b8; margin-top: 4px;">PNG, JPG, PDF (최대 10MB)</div>
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
${[{name:'견적서_v2.pdf',size:'2.4MB',pct:100,status:'완료'},{name:'도면_A-101.png',size:'5.1MB',pct:67,status:'업로드 중...'},{name:'스펙시트.xlsx',size:'1.8MB',pct:30,status:'대기'}].map(f => `
<div style="display:flex;align-items:center;gap:8px;padding:8px;border:1px solid #e2e8f0;border-radius:6px;">
<span style="font-size:16px;">${f.name.includes('.pdf')?'📄':f.name.includes('.png')?'🖼️':'📊'}</span>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;"><span style="font-size:9px;font-weight:600;color:#1e293b;">${f.name}</span><span style="font-size:8px;color:#94a3b8;">${f.size}</span></div>
<div style="height:4px;background:#f1f5f9;border-radius:2px;margin-top:4px;overflow:hidden;"><div style="height:100%;width:${f.pct}%;background:${f.pct===100?'#10b981':'#3b82f6'};border-radius:2px;"></div></div>
</div>
<span style="font-size:8px;color:${f.pct===100?'#10b981':'#64748b'};">${f.status}</span>
</div>
`).join('')}
</div>
</div>`;
// 브레드크럼
if (key.includes('브레드크럼') || key.includes('breadcrumb')) return `
<div class="wf-wrap" style="padding: 16px;">
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 16px;">
${['🏠 홈','/','📋 영업관리','/','📊 수주관리','/','<b>#1024</b>'].map(b => b==='/'?`<span style="color:#cbd5e1;font-size:10px;">/</span>`:`<span style="font-size:10px;color:${b.includes('<b>')?'#1e293b':'#3b82f6'};${b.includes('<b>')?'font-weight:700;':''}">${b.replace(/<\/?b>/g,'')}</span>`).join('')}
</div>
<div style="font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;">수주 #1024</div>
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;"></div>
<div class="wf-bar lg" style="height:8px;"></div>
</div>`;
// 온보딩 스테퍼
if (key.includes('온보딩') || key.includes('스테퍼') || key.includes('위자드')) return `
<div class="wf-wrap" style="padding: 20px;">
<div style="display: flex; align-items: center; gap: 0; margin-bottom: 20px; padding: 0 20px;">
${[{n:'1',t:'회사 정보',done:true},{n:'2',t:'팀 설정',active:true},{n:'3',t:'데이터 연동'},{n:'4',t:'완료'}].map((s,i) => `
<div style="display:flex;align-items:center;gap:4px;${i>0?'flex:1;':''}">
${i>0?`<div style="flex:1;height:2px;background:${s.done||s.active?'#3b82f6':'#e2e8f0'};"></div>`:''}
<div style="width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;
${s.done?'background:#3b82f6;color:#fff;':s.active?'background:#fff;border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${s.done?'✓':s.n}</div>
</div>
`).join('')}
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:16px;padding:0 10px;">
${['회사 정보','팀 설정','데이터 연동','완료'].map((t,i) => `<span style="font-size:8px;color:${i<=1?'#3b82f6':'#94a3b8'};font-weight:${i===1?'600':'400'};text-align:center;">${t}</span>`).join('')}
</div>
<div class="wf-box" style="padding: 16px; text-align: center;">
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:4px;">팀 구성원을 초대하세요</div>
<div style="font-size:9px;color:#64748b;margin-bottom:12px;">함께 사용할 팀원의 이메일을 입력하세요</div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:8px;"></div>
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:12px;"></div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:9px;color:#3b82f6;"> 이전</span>
<div style="padding:4px 14px;background:#3b82f6;color:#fff;border-radius:4px;font-size:9px;">다음 </div>
</div>
</div>
</div>`;
// 토스트
if (key.includes('토스트') || key.includes('알림') && key.includes('시스템')) return `
<div style="width: 100%; max-width: 380px; position: relative; height: 240px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; overflow: hidden;">
<div style="padding: 12px; opacity: .3;">
<div class="wf-bar xl" style="height: 10px; margin-bottom: 8px;"></div>
<div class="wf-bar lg" style="height: 8px;"></div>
</div>
<div style="position: absolute; bottom: 12px; right: 12px; display: flex; flex-direction: column; gap: 6px; width: 220px;">
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #10b981;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;"></span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 완료</div><div style="font-size:8px;color:#64748b;">수주 정보가 저장되었습니다</div></div>
<span style="color:#94a3b8;font-size:10px;cursor:pointer;"></span>
</div>
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #ef4444;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;"></span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 실패</div><div style="font-size:8px;color:#64748b;">네트워크 오류가 발생했습니다</div></div>
<span style="color:#94a3b8;font-size:10px;cursor:pointer;"></span>
</div>
<div style="background:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 4px 12px rgba(0,0,0,.1);border-left:3px solid #f59e0b;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;">⚠️</span>
<div style="flex:1;"><div style="font-size:9px;font-weight:600;color:#1e293b;">저장 변경</div><div style="font-size:8px;color:#64748b;">변경사항을 저장하세요</div></div>
<span style="font-size:8px;color:#3b82f6;cursor:pointer;">되돌리기</span>
</div>
</div>
</div>`;
// Empty State
if (key.includes('empty') || key.includes('빈') && key.includes('상태')) return `
<div class="wf-wrap" style="padding: 40px; text-align: center;">
<div style="font-size: 48px; margin-bottom: 12px; opacity: .6;">📭</div>
<div style="font-size: 13px; font-weight: 700; color: #1e293b; margin-bottom: 6px;">아직 수주가 없습니다</div>
<div style="font-size: 10px; color: #94a3b8; margin-bottom: 16px; line-height: 1.6;"> 번째 수주를 등록하여<br>영업 관리를 시작하세요</div>
<div style="display:inline-flex;align-items:center;gap:4px;padding:6px 16px;background:#3b82f6;color:#fff;border-radius:6px;font-size:10px;font-weight:600;">+ 수주 등록</div>
<div style="margin-top:10px;font-size:9px;color:#3b82f6;">도움말 보기 </div>
</div>`;
// 검색 자동완성
if (key.includes('자동완성') || key.includes('서제스트')) return `
<div style="width: 100%; max-width: 400px;">
<div class="wf-wrap" style="padding: 12px; overflow: visible; position: relative;">
<div style="background:#f8fafc;border:2px solid #3b82f6;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;position:relative;z-index:2;">
<span style="color:#94a3b8;">🔍</span><span style="font-size:11px;color:#1e293b;">블라인</span><span style="animation:blink 1s infinite;color:#3b82f6;">|</span>
</div>
<div style="position:absolute;left:12px;right:12px;top:44px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:3;overflow:hidden;">
${['블라인드 50mm 수동|품목','블라인드 25mm 전동|품목','블라인드 롤스크린 조합|품목','블라인드코리아|거래처'].map((r,i) => {
const [name,cat] = r.split('|');
return `<div style="display:flex;align-items:center;gap:8px;padding:7px 12px;${i===0?'background:#f8fafc;':''}cursor:pointer;">
<span style="font-size:9px;padding:1px 6px;background:#f1f5f9;border-radius:3px;color:#64748b;">${cat}</span>
<span style="font-size:10px;color:#1e293b;"><b style="color:#3b82f6;">블라인</b>${name.replace('블라인','')}</span>
${i===0?'<span style="margin-left:auto;font-size:8px;color:#94a3b8;">↵</span>':''}
</div>`;
}).join('')}
</div>
</div>
<div style="height: 80px;"></div>
</div>`;
// 탭 레이아웃
if (key.includes('탭') && key.includes('레이아웃')) return `
<div class="wf-wrap" style="padding: 0;">
<div style="display: flex; border-bottom: 2px solid #e2e8f0; padding: 0 12px;">
${['📊 개요|true','📋 상세|false','📎 첨부|false','💬 댓글|false','📜 이력|false'].map(t => {
const [n,a] = t.split('|');
return `<div style="padding:8px 12px;font-size:9px;${a==='true'?'color:#3b82f6;border-bottom:2px solid #3b82f6;font-weight:600;margin-bottom:-2px;':'color:#94a3b8;'}">${n}${n.includes('댓글')?'<span style="background:#ef4444;color:#fff;font-size:7px;padding:0 4px;border-radius:8px;margin-left:3px;">3</span>':''}</div>`;
}).join('')}
</div>
<div style="padding: 14px;">
<div class="wf-bar xl dark" style="height: 10px; margin-bottom: 10px;"></div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div class="wf-box" style="padding: 10px; height: 60px;"><div class="wf-text">요약 정보</div></div>
<div class="wf-box" style="padding: 10px; height: 60px;"><div class="wf-text">차트</div></div>
</div>
</div>
</div>`;
// ===== 로그인/인증 =====
// 로그인 폼 (클래식)
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>`;
// 기본 와이어프레임 (매칭 안 됨)
return `
<div class="wf-wrap" style="padding: 24px; text-align: center;">
<div style="font-size: 32px; margin-bottom: 12px; opacity: .5;"><i class="${this.getTypeIcon(card.type)}" style="font-size:32px;color:#94a3b8;"></i></div>
<div style="font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 4px;">${card.title || ''}</div>
<div style="font-size: 10px; color: #94a3b8;">이미지를 추가하면 여기에 표시됩니다</div>
</div>`;
},
openEditCardModal(card) {
this.editingCard = {
...card,
tagsText: (card.tags || []).join(', '),
usedInText: (card.usedIn || []).join(', '),
changesText: (card.changes || []).join('\n'),
components: card.components ? [...card.components.map(c => ({...c}))] : [{ name: '', required: true }],
principles: card.principles ? { ...card.principles } : {},
};
this.showCardModal = true;
},
closeCardModal() {
this.showCardModal = false;
this.editingCard = {};
},
saveCard() {
const card = this.editingCard;
// Parse text fields
card.tags = card.tagsText ? card.tagsText.split(',').map(t => t.trim()).filter(Boolean) : [];
card.usedIn = card.usedInText ? card.usedInText.split(',').map(t => t.trim()).filter(Boolean) : [];
card.changes = card.changesText ? card.changesText.split('\n').map(t => t.trim()).filter(Boolean) : [];
// Clean temp fields
const { tagsText, usedInText, changesText, ...cleanCard } = card;
// Remove empty components
if (cleanCard.components) {
cleanCard.components = cleanCard.components.filter(c => c.name.trim());
}
if (!cleanCard.id) {
// New card
cleanCard.id = 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
cleanCard.createdAt = new Date().toISOString();
cleanCard.updatedAt = new Date().toISOString();
if (!this.currentProject.cards) this.currentProject.cards = [];
this.currentProject.cards.unshift(cleanCard);
this.toast('카드 추가됨');
} else {
// Update card
cleanCard.updatedAt = new Date().toISOString();
const idx = this.currentProject.cards.findIndex(c => c.id === cleanCard.id);
if (idx >= 0) this.currentProject.cards[idx] = cleanCard;
this.toast('카드 수정됨');
}
this.saveProject();
this.closeCardModal();
},
deleteCard(id) {
if (!confirm('이 카드를 삭제하시겠습니까?')) return;
this.currentProject.cards = this.currentProject.cards.filter(c => c.id !== id);
this.saveProject();
this.closeCardModal();
this.toast('카드 삭제됨');
},
togglePin(card) {
card.pinned = !card.pinned;
this.saveProject();
},
// ===== Images =====
handlePaste(e) {
if (this.showCardModal || this.showProjectsModal) return;
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
this.readImageFile(file, (dataUrl) => {
this.openNewCardModal('reference');
this.$nextTick(() => { this.editingCard.image = dataUrl; });
});
break;
}
}
},
handleDrop(e) {
e.currentTarget.classList.remove('dragover');
const file = e.dataTransfer?.files?.[0];
if (file && file.type.startsWith('image/')) {
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
}
},
handleFileSelect(e) {
const file = e.target.files?.[0];
if (file) {
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
}
e.target.value = '';
},
handleCompDrop(e, side) {
e.currentTarget.classList.remove('dragover');
const file = e.dataTransfer?.files?.[0];
if (file && file.type.startsWith('image/')) {
const key = side === 'before' ? 'beforeImage' : 'afterImage';
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
}
},
handleCompFileSelect(e, side) {
const file = e.target.files?.[0];
if (file) {
const key = side === 'before' ? 'beforeImage' : 'afterImage';
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
}
e.target.value = '';
},
readImageFile(file, callback) {
const reader = new FileReader();
reader.onload = (e) => callback(e.target.result);
reader.readAsDataURL(file);
},
// ===== CRAP Principles =====
cyclePrinciple(key) {
if (!this.editingCard.principles) this.editingCard.principles = {};
const current = this.editingCard.principles[key] || '';
const cycle = { '': 'pass', 'pass': 'warn', 'warn': 'fail', 'fail': '' };
this.editingCard.principles[key] = cycle[current] || '';
},
getPrincipleStatus(key) {
return (this.editingCard.principles || {})[key] || '';
},
getPrincipleIcon(key) {
const status = this.getPrincipleStatus(key);
return { pass: '✅', warn: '⚠️', fail: '❌', '': '—' }[status] || '—';
},
// ===== Filters =====
get filteredCards() {
let cards = this.currentProject.cards || [];
// Card type filter (tabs)
if (this.categoryFilter !== 'all') {
cards = cards.filter(c => c.type === this.categoryFilter);
}
// Screen category filter (sidebar)
if (this.screenFilter !== 'all') {
cards = cards.filter(c => c.category === this.screenFilter);
}
// Tag filter
if (this.selectedTags.length > 0) {
cards = cards.filter(c => {
const cardTags = c.tags || [];
return this.selectedTags.some(t => cardTags.includes(t));
});
}
// Search
if (this.searchQuery.trim()) {
const q = this.searchQuery.toLowerCase();
cards = cards.filter(c =>
(c.title || '').toLowerCase().includes(q) ||
(c.memo || '').toLowerCase().includes(q) ||
(c.suggestion || '').toLowerCase().includes(q) ||
(c.effect || '').toLowerCase().includes(q) ||
(c.source || '').toLowerCase().includes(q) ||
(c.tags || []).some(t => t.toLowerCase().includes(q))
);
}
// Sort — pinned first
cards = [...cards].sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return 0;
});
// Sort by
cards.sort((a, b) => {
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
switch (this.sortBy) {
case 'newest': return new Date(b.createdAt) - new Date(a.createdAt);
case 'oldest': return new Date(a.createdAt) - new Date(b.createdAt);
case 'rating': return (b.rating || 0) - (a.rating || 0);
case 'title': return (a.title || '').localeCompare(b.title || '');
default: return 0;
}
});
return cards;
},
get allTags() {
const tags = new Set();
(this.currentProject.cards || []).forEach(c => {
(c.tags || []).forEach(t => tags.add(t));
});
return [...tags].sort();
},
toggleTag(tag) {
const idx = this.selectedTags.indexOf(tag);
if (idx >= 0) this.selectedTags.splice(idx, 1);
else this.selectedTags.push(tag);
},
clearFilters() {
this.categoryFilter = 'all';
this.screenFilter = 'all';
this.searchQuery = '';
this.selectedTags = [];
this.sortBy = 'newest';
},
getCardCountByType(type) {
return (this.currentProject.cards || []).filter(c => c.type === type).length;
},
getCardCountByCat(cat) {
return (this.currentProject.cards || []).filter(c => c.category === cat).length;
},
// ===== Export / Import =====
exportJSON() {
const data = JSON.stringify(this.currentProject, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (this.currentProject.title || 'design-insight') + '.json';
a.click();
URL.revokeObjectURL(url);
this.toast('JSON 내보내기 완료');
},
importJSON() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
if (data.id && data.cards) {
// Check if project already exists
const existIdx = this.projects.findIndex(p => p.id === data.id);
if (existIdx >= 0) {
if (confirm('동일 ID 프로젝트가 있습니다. 덮어쓰시겠습니까?')) {
this.projects[existIdx] = data;
} else return;
} else {
this.projects.push(data);
}
this.saveProjects();
this.switchProject(data.id);
this.toast('프로젝트 가져오기 완료');
}
} catch { this.toast('JSON 파일 오류'); }
};
reader.readAsText(file);
};
input.click();
},
// ===== Keyboard =====
handleKeydown(e) {
if (this.showCardModal || this.showProjectsModal) return;
// Ctrl+S — Save
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
this.saveProject();
this.toast('저장됨');
}
// Ctrl+N — New card
if (e.ctrlKey && e.key === 'n') {
e.preventDefault();
this.openNewCardModal('reference');
}
// Ctrl+F — Focus search
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.querySelector('.di-search-input')?.focus();
}
},
// ===== Helpers =====
getTypeLabel(type) {
const t = this.cardTypes.find(ct => ct.code === type);
return t ? t.icon + ' ' + t.label : type;
},
getTypeIcon(type) {
const map = {
reference: 'ri-camera-line',
analysis: 'ri-search-eye-line',
pattern: 'ri-layout-masonry-line',
comparison: 'ri-arrow-left-right-line',
};
return map[type] || 'ri-file-line';
},
getRatingStars(rating) {
return '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
},
formatTime(d) {
return String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
},
toast(msg) {
this.toastMsg = msg;
setTimeout(() => { this.toastMsg = ''; }, 2500);
},
// ===== Preset Templates =====
loadPresetTemplates() {
const existing = (this.currentProject.cards || []).length;
if (existing > 0 && !confirm('현재 프로젝트에 인기 UI 패턴 20종을 추가합니다. 계속하시겠습니까?')) return;
const now = new Date().toISOString();
const mkId = () => 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
const presets = [
{
type: 'pattern', title: 'KPI 대시보드', category: 'dashboard', rating: 5,
tags: ['대시보드', 'KPI', '통계', '차트'],
memo: 'Stripe, Shopify, Vercel 등 SaaS 서비스에서 사용하는 핵심 패턴. 로그인 직후 전체 현황을 3초 안에 파악할 수 있어야 한다.',
components: [
{ name: 'KPI 요약 카드 (4~6개, 상단 고정)', required: true },
{ name: '추이 차트 (라인/바 차트, 기간 선택)', required: true },
{ name: '최근 활동 피드 / 알림', required: false },
{ name: '빠른 액션 버튼', required: false },
{ name: '기간 필터 (오늘/주/월/커스텀)', required: true },
],
usedIn: ['Stripe Dashboard', 'Shopify Admin', 'Vercel Dashboard', 'SAM 메인 대시보드'],
guidelines: 'KPI 카드는 숫자 + 변화율(▲▼) + 미니 스파크라인 조합. 가장 중요한 지표를 좌상단에 배치. 색상은 긍정(초록)/부정(빨강)으로 즉시 인지 가능하게.',
},
{
type: 'pattern', title: '데이터 테이블 + 검색/필터', category: 'list', rating: 5,
tags: ['테이블', '검색', '필터', '정렬', '페이지네이션'],
memo: 'Airtable, Notion Database, GitHub Issues 등 데이터 중심 서비스의 핵심 패턴. CRUD 목록 화면의 표준.',
components: [
{ name: '검색바 (상단 고정, 플레이스홀더 힌트)', required: true },
{ name: '필터 칩/드롭다운 (상태, 날짜, 카테고리)', required: true },
{ name: '데이터 테이블 (컬럼 정렬, 행 선택)', required: true },
{ name: '페이지네이션 / 무한 스크롤', required: true },
{ name: '벌크 액션 바 (선택 시 나타남)', required: false },
{ name: '컬럼 커스터마이징 (표시/숨기기)', required: false },
],
usedIn: ['Airtable', 'Notion', 'GitHub Issues', 'SAM 수주목록, 거래처목록, 품목목록'],
guidelines: '검색은 debounce 300ms 적용. 필터 상태는 URL 파라미터로 유지 (뒤로가기 대응). 빈 상태 시 "결과 없음" + 필터 초기화 버튼 제공.',
},
{
type: 'pattern', title: '칸반 보드 (Kanban)', category: 'dashboard', rating: 5,
tags: ['칸반', '드래그앤드롭', '워크플로우', '상태관리'],
memo: 'Trello, Jira, Linear, Notion Board 등 프로젝트 관리 도구의 핵심. 작업 상태를 시각적으로 한눈에 파악.',
components: [
{ name: '컬럼 헤더 (상태명 + 카드 수)', required: true },
{ name: '드래그 가능 카드 (제목 + 라벨 + 담당자)', required: true },
{ name: '컬럼 간 드래그 앤 드롭', required: true },
{ name: '카드 추가 버튼 (각 컬럼 하단)', required: true },
{ name: '필터 (담당자, 라벨, 우선순위)', required: false },
{ name: 'WIP 제한 표시', required: false },
],
usedIn: ['Trello', 'Jira', 'Linear', 'Notion Board', 'GitHub Projects'],
guidelines: '컬럼은 3~6개 권장 (너무 많으면 가독성 저하). 카드에는 핵심 정보만 표시 (제목 + 라벨 + 아바타). 드래그 시 시각적 피드백 필수.',
},
{
type: 'pattern', title: 'Command Palette (Cmd+K)', category: 'navigation', rating: 5,
tags: ['커맨드팔레트', '검색', '네비게이션', '키보드'],
memo: 'Linear, Vercel, GitHub, VS Code, Figma 등 파워유저 대상 서비스의 필수 패턴. 키보드만으로 모든 기능에 접근.',
components: [
{ name: '오버레이 모달 (화면 중앙 상단)', required: true },
{ name: '검색 입력란 (자동 포커스)', required: true },
{ name: '결과 목록 (아이콘 + 이름 + 단축키)', required: true },
{ name: '카테고리 그룹핑 (페이지, 액션, 최근)', required: true },
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
{ name: '최근 사용 기록', required: false },
],
usedIn: ['Linear', 'Vercel', 'GitHub', 'VS Code', 'Figma', 'Raycast'],
guidelines: 'Cmd+K 또는 Ctrl+K로 열기. 타이핑 즉시 퍼지 검색. 결과는 최대 8~10개 표시. ESC로 닫기. 최근 사용 항목 상단 표시.',
},
{
type: 'pattern', title: '사이드바 네비게이션', category: 'navigation', rating: 5,
tags: ['사이드바', '메뉴', '네비게이션', '트리'],
memo: 'Slack, Discord, Notion, Linear, VS Code 등 거의 모든 SaaS 앱의 기본 네비게이션. 접기/펼치기 + 트리 구조.',
components: [
{ name: '로고/앱 이름 (상단)', required: true },
{ name: '메인 메뉴 그룹 (아이콘 + 라벨)', required: true },
{ name: '접기/펼치기 토글', required: true },
{ name: '현재 위치 하이라이트', required: true },
{ name: '트리 구조 (하위 메뉴 들여쓰기)', required: false },
{ name: '즐겨찾기/고정 섹션', required: false },
{ name: '사용자 프로필 (하단)', required: false },
],
usedIn: ['Slack', 'Discord', 'Notion', 'Linear', 'VS Code', 'SAM MNG 사이드바'],
guidelines: '너비 240~280px 권장. 접힌 상태에서는 아이콘만 표시 (56px). 메뉴 그룹 간 구분선. 활성 항목은 배경색 + 좌측 인디케이터.',
},
{
type: 'pattern', title: '모달 폼 (생성/편집)', category: 'modal', rating: 4,
tags: ['모달', '폼', 'CRUD', '입력'],
memo: '대부분의 SaaS에서 레코드 생성/편집에 사용. 페이지 이동 없이 빠르게 데이터 입력. Notion, Linear, Jira 등.',
components: [
{ name: '오버레이 배경 (클릭 시 닫기)', required: true },
{ name: '모달 헤더 (제목 + 닫기 버튼)', required: true },
{ name: '폼 필드 (라벨 + 입력 + 검증 메시지)', required: true },
{ name: '액션 버튼 (저장/취소, 우하단)', required: true },
{ name: '키보드 지원 (ESC 닫기, Enter 저장)', required: true },
{ name: '로딩 상태 표시', required: false },
],
usedIn: ['Linear', 'Notion', 'Jira', 'SAM 등록/수정 팝업'],
guidelines: '너비 480~640px 권장. 필드 5개 이하면 모달, 그 이상이면 전체 페이지 고려. 필수 필드 * 표시. Tab 순서 정확히 설정.',
},
{
type: 'pattern', title: '설정 페이지 (그룹 섹션)', category: 'form', rating: 4,
tags: ['설정', '프로필', '섹션', '그룹'],
memo: 'GitHub Settings, Vercel Settings, Notion Settings 등. 좌측 탭 메뉴 + 우측 섹션별 설정 카드.',
components: [
{ name: '좌측 탭 메뉴 (세로 목록)', required: true },
{ name: '섹션 카드 (제목 + 설명 + 입력 필드)', required: true },
{ name: '개별 저장 버튼 (섹션마다)', required: true },
{ name: '위험 영역 (빨간 테두리, 하단 배치)', required: false },
{ name: '변경사항 감지 (저장 안 된 변경 알림)', required: false },
],
usedIn: ['GitHub Settings', 'Vercel Settings', 'Notion Settings', 'SAM 시스템 설정'],
guidelines: '좌측 탭은 고정, 우측은 스크롤. 섹션 간 명확한 구분선. 위험 작업(삭제, 비활성화)은 페이지 최하단 빨간 영역에 배치.',
},
{
type: 'pattern', title: '타임라인/활동 피드', category: 'list', rating: 4,
tags: ['타임라인', '피드', '활동로그', '히스토리'],
memo: 'GitHub Activity, Twitter/X Feed, Slack Messages 등. 시간순 이벤트 흐름을 표시하는 패턴.',
components: [
{ name: '타임라인 세로선 (좌측)', required: true },
{ name: '이벤트 노드 (아이콘 + 시간)', required: true },
{ name: '이벤트 카드 (내용 + 작성자 + 시간)', required: true },
{ name: '날짜 구분선', required: false },
{ name: '더 보기 / 무한 스크롤', required: false },
{ name: '이벤트 타입별 아이콘/색상', required: true },
],
usedIn: ['GitHub', 'Twitter/X', 'Slack', 'Jira', 'SAM 변경이력'],
guidelines: '최신 이벤트 상단. 아이콘과 색상으로 이벤트 타입 즉시 구분. 시간 표시는 상대 시간(3분 전) + hover 시 절대 시간.',
},
{
type: 'pattern', title: '트리 + 상세 분할 뷰', category: 'form', rating: 4,
tags: ['트리', '분할뷰', '마스터-디테일', '패널'],
memo: 'VS Code 파일 탐색기, Figma 레이어 패널, macOS Finder 컬럼 뷰. 계층 구조를 탐색하며 상세를 확인.',
components: [
{ name: '좌측 트리 패널 (접기/펼치기 노드)', required: true },
{ name: '우측 상세 패널 (선택 항목 정보)', required: true },
{ name: '분할선 (드래그 리사이즈)', required: false },
{ name: '빈 상태 (선택 안 했을 때)', required: true },
{ name: '트리 검색/필터', required: false },
{ name: '드래그 앤 드롭 재정렬', required: false },
],
usedIn: ['VS Code', 'Figma', 'macOS Finder', 'SAM 메뉴관리, 조직도'],
guidelines: '좌측 패널 240~320px, 최소 너비 제한. 선택 항목 시각적 하이라이트. 트리 깊이 4단계 이하 권장. 키보드 화살표 네비게이션 지원.',
},
{
type: 'pattern', title: '온보딩 스테퍼/위자드', category: 'form', rating: 4,
tags: ['온보딩', '스테퍼', '위자드', '단계별'],
memo: 'Notion 초기 설정, Stripe 계정 생성, Linear 워크스페이스 설정 등. 복잡한 프로세스를 단계별로 안내.',
components: [
{ name: '진행 표시줄 (상단, 현재 단계 강조)', required: true },
{ name: '단계별 카드 (제목 + 설명 + 입력)', required: true },
{ name: '이전/다음 버튼', required: true },
{ name: '단계 건너뛰기 옵션', required: false },
{ name: '완료 축하 화면 (마지막 단계)', required: false },
{ name: '현재 진행률 (예: 3/5)', required: true },
],
usedIn: ['Notion', 'Stripe', 'Linear', 'Vercel', 'SAM 초기 설정'],
guidelines: '단계는 3~5개 권장. 각 단계는 하나의 주제에 집중. 이전 단계 데이터 유지. 진행률 시각적 표시 필수. 마지막 단계에서 전체 요약.',
},
{
type: 'pattern', title: '토스트 알림 시스템', category: 'etc', rating: 4,
tags: ['토스트', '알림', '피드백', '노티피케이션'],
memo: 'Vercel, Linear, Notion 등 거의 모든 SaaS 앱. 사용자 액션에 대한 즉각적인 피드백을 비침습적으로 제공.',
components: [
{ name: '토스트 컨테이너 (우하단 고정)', required: true },
{ name: '타입별 아이콘+색상 (성공/오류/경고/정보)', required: true },
{ name: '메시지 텍스트 + 선택적 액션 버튼', required: true },
{ name: '자동 사라짐 (3~5초)', required: true },
{ name: '수동 닫기 (X 버튼)', required: true },
{ name: '다중 토스트 스택', required: false },
],
usedIn: ['Vercel', 'Linear', 'Notion', 'Stripe', 'SAM 전역'],
guidelines: '성공=초록, 오류=빨강, 경고=노랑, 정보=파랑. 텍스트는 1줄 이내. 되돌리기(Undo) 액션 제공 시 사용자 신뢰도 향상. 오류는 자동 사라짐 비활성화.',
},
{
type: 'pattern', title: 'Empty State (빈 상태)', category: 'etc', rating: 4,
tags: ['빈상태', '온보딩', '가이드', 'CTA'],
memo: 'Dropbox, Mailchimp, Notion, Linear 등. 데이터가 없을 때 사용자를 안내하는 핵심 패턴. 이탈을 방지하고 첫 행동을 유도.',
components: [
{ name: '일러스트/아이콘 (시각적 안내)', required: true },
{ name: '제목 (상황 설명)', required: true },
{ name: '설명 텍스트 (다음 단계 안내)', required: true },
{ name: 'CTA 버튼 (첫 행동 유도)', required: true },
{ name: '대안 링크 (도움말, 템플릿 등)', required: false },
],
usedIn: ['Dropbox', 'Mailchimp', 'Notion', 'Linear', 'SAM 각 목록 화면'],
guidelines: '일러스트는 브랜드 톤 유지. CTA 버튼은 1개만 (선택 피로 방지). "데이터 없음" 대신 긍정적 표현 사용 ("첫 프로젝트를 시작하세요!").',
},
{
type: 'pattern', title: '검색 + 자동완성 드롭다운', category: 'navigation', rating: 4,
tags: ['검색', '자동완성', '드롭다운', '서제스트'],
memo: 'Google, Algolia, GitHub Search, Amazon 등. 타이핑과 동시에 결과를 보여주어 탐색 속도를 극대화.',
components: [
{ name: '검색 입력란 (돋보기 아이콘 + placeholder)', required: true },
{ name: '자동완성 드롭다운 (입력 시 표시)', required: true },
{ name: '결과 하이라이팅 (매칭 텍스트 볼드)', required: true },
{ name: '카테고리별 그룹핑', required: false },
{ name: '최근 검색 기록', required: false },
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
],
usedIn: ['Google', 'Algolia', 'GitHub', 'Amazon', 'SAM 품목 검색'],
guidelines: 'Debounce 200~300ms. 최소 2글자부터 검색. 결과 최대 8개. 키워드 하이라이팅 필수. ESC로 닫기, 외부 클릭 시 닫기.',
},
{
type: 'pattern', title: '탭 레이아웃', category: 'navigation', rating: 4,
tags: ['탭', '네비게이션', '섹션', '컨텐츠전환'],
memo: 'Google Analytics, Stripe Dashboard, GitHub Repo 등. 같은 페이지 내에서 컨텐츠 섹션을 전환.',
components: [
{ name: '탭 바 (가로 목록, 활성 탭 하이라이트)', required: true },
{ name: '탭 콘텐츠 영역', required: true },
{ name: '탭 카운트 배지', required: false },
{ name: '탭 아이콘 (선택적)', required: false },
{ name: '탭 스크롤 (많을 때 화살표)', required: false },
],
usedIn: ['Google Analytics', 'Stripe', 'GitHub', 'SAM 품목기준관리, 설정'],
guidelines: '탭 5개 이하 권장 (7개 초과 시 드롭다운 또는 더보기). 활성 탭은 하단 인디케이터 + 볼드. 탭 전환 시 URL 해시 변경 (북마크 가능).',
},
{
type: 'pattern', title: '카드 그리드 레이아웃', category: 'dashboard', rating: 4,
tags: ['카드', '그리드', '갤러리', '레이아웃'],
memo: 'Pinterest, Dribbble, Notion Gallery, YouTube 등. 시각적 콘텐츠를 격자로 배열. 정보 밀도와 시각적 매력 균형.',
components: [
{ name: '반응형 그리드 컨테이너', required: true },
{ name: '카드 (이미지 + 제목 + 메타정보)', required: true },
{ name: '호버 오버레이 (액션 버튼)', required: false },
{ name: '무한 스크롤 / 더 보기', required: false },
{ name: '필터/정렬 툴바', required: false },
],
usedIn: ['Pinterest', 'Dribbble', 'YouTube', 'Notion Gallery', 'SAM 대시보드 카드'],
guidelines: '카드 최소 너비 280px, 간격 16px. 이미지 비율 고정 (16:9 또는 4:3). 호버 시 그림자 + 미세 상승 효과. Skeleton 로딩 적용.',
},
{
type: 'pattern', title: '가격표/플랜 비교', category: 'etc', rating: 4,
tags: ['가격', '플랜', '비교', '테이블', 'CTA'],
memo: 'Stripe, Vercel, Notion, Slack 등 SaaS 가격 페이지. 플랜 간 차이를 한눈에 비교하여 구매 결정을 유도.',
components: [
{ name: '플랜 카드 (이름 + 가격 + 주요 기능)', required: true },
{ name: '추천 플랜 강조 (배지, 테두리)', required: true },
{ name: 'CTA 버튼 (각 플랜마다)', required: true },
{ name: '상세 기능 비교 테이블 (하단)', required: false },
{ name: '월간/연간 토글', required: false },
],
usedIn: ['Stripe', 'Vercel', 'Notion', 'Slack', 'SAM 요금 안내'],
guidelines: '3~4개 플랜 권장. 가장 인기 플랜을 시각적으로 강조. 가격은 크고 볼드하게. 무료 체험 CTA 제공. 기능 비교는 ✓/✗ 아이콘.',
},
{
type: 'pattern', title: '캘린더 뷰', category: 'dashboard', rating: 4,
tags: ['캘린더', '일정', '날짜', '이벤트'],
memo: 'Google Calendar, Calendly, Notion Calendar 등. 시간 기반 데이터를 날짜 격자에 시각화.',
components: [
{ name: '월/주/일 뷰 전환 탭', required: true },
{ name: '날짜 격자 (이벤트 표시)', required: true },
{ name: '이벤트 카드 (색상 코딩)', required: true },
{ name: '이전/다음 네비게이션', required: true },
{ name: '오늘 버튼 (빠른 이동)', required: true },
{ name: '이벤트 생성 (날짜 클릭)', required: false },
],
usedIn: ['Google Calendar', 'Calendly', 'Notion Calendar', 'SAM 일정관리, 근태'],
guidelines: '오늘 날짜 강조 (원형 배경). 이벤트 색상은 카테고리별 구분. 주말 배경색 다르게. 이벤트 3개 초과 시 "+N more" 표시.',
},
{
type: 'pattern', title: '채팅/메시징 인터페이스', category: 'etc', rating: 4,
tags: ['채팅', '메시징', '실시간', '대화'],
memo: 'Slack, Discord, Intercom, WhatsApp Web 등. 실시간 대화를 위한 인터페이스. 고객 지원 위젯에도 활용.',
components: [
{ name: '채널/대화 목록 (좌측)', required: true },
{ name: '메시지 영역 (시간순 스크롤)', required: true },
{ name: '메시지 입력란 (하단 고정)', required: true },
{ name: '메시지 버블 (내 메시지 우측, 상대 좌측)', required: true },
{ name: '첨부파일/이모지 버튼', required: false },
{ name: '읽음 표시 / 타이핑 인디케이터', required: false },
],
usedIn: ['Slack', 'Discord', 'Intercom', 'WhatsApp Web'],
guidelines: '새 메시지 자동 스크롤. 날짜 구분선. 연속 메시지는 아바타 한 번만 표시. 링크 미리보기. 이미지 인라인 표시.',
},
{
type: 'pattern', title: '파일 업로드 + 진행률', category: 'modal', rating: 3,
tags: ['업로드', '파일', '진행률', '드래그앤드롭'],
memo: 'Dropbox, Google Drive, WeTransfer, Figma 등. 파일 업로드 과정을 시각적으로 안내하여 사용자 불안 해소.',
components: [
{ name: '드롭존 (점선 테두리, 드래그 안내)', required: true },
{ name: '파일 선택 버튼', required: true },
{ name: '업로드 진행률 바', required: true },
{ name: '파일 목록 (이름 + 크기 + 상태)', required: true },
{ name: '개별 취소/삭제 버튼', required: true },
{ name: '미리보기 (이미지 썸네일)', required: false },
],
usedIn: ['Dropbox', 'Google Drive', 'WeTransfer', 'SAM 파일 첨부'],
guidelines: '드래그 시 드롭존 하이라이트. 파일 크기/형식 제한 사전 안내. 실패 시 재시도 버튼. 다중 파일 동시 업로드 지원.',
},
{
type: 'pattern', title: '브레드크럼 네비게이션', category: 'navigation', rating: 3,
tags: ['브레드크럼', '경로', '네비게이션', '계층'],
memo: 'AWS Console, Shopify Admin, Jira, SAM 등. 현재 위치를 계층적으로 표시하여 깊은 네비게이션에서 길을 잃지 않게.',
components: [
{ name: '경로 항목 (클릭 가능 링크)', required: true },
{ name: '구분자 (/ 또는 > 아이콘)', required: true },
{ name: '현재 페이지 (비링크, 볼드)', required: true },
{ name: '긴 경로 말줄임 (중간 생략)', required: false },
{ name: '드롭다운 (형제 페이지 선택)', required: false },
],
usedIn: ['AWS Console', 'Shopify', 'Jira', 'SAM 상세 페이지'],
guidelines: '페이지 상단 좌측 배치. 글꼴 크기 12~13px. 현재 페이지는 클릭 불가 (시각적 구분). 3단계 이상일 때 가장 유용.',
},
// ===== 로그인/인증 (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: '오늘 날짜 강조. 선택 불가 날짜 회색 처리. 기간 선택 시 범위 배경색. 모바일: 전체 화면 달력. 키보드: 화살표 키 이동.',
},
];
const cards = presets.map(p => ({
id: mkId(),
type: p.type,
title: p.title,
image: '',
memo: p.memo,
source: '',
tags: p.tags,
category: p.category,
rating: p.rating,
pinned: false,
archived: false,
components: p.components || [],
usedIn: p.usedIn || [],
guidelines: p.guidelines || '',
frequency: 0,
principles: {},
suggestion: '',
severity: 'info',
beforeImage: '',
afterImage: '',
changes: [],
effect: '',
createdAt: now,
updatedAt: now,
}));
if (!this.currentProject.cards) this.currentProject.cards = [];
this.currentProject.cards = [...cards, ...this.currentProject.cards];
this.saveProject();
this.categoryFilter = 'pattern';
this.toast('인기 UI 패턴 20종이 추가되었습니다');
},
};
}
</script>
@endpush