- 로그인/인증 5종: 클래식 로그인, 소셜 SSO, 2FA, 비밀번호 재설정, 회원가입 - 보고서 5종: 인쇄용, 인보이스/견적서, 분석 리포트, 업무 보고서, PDF 리포트 - 대시보드 4종: 위젯, 실시간 모니터링, 멀티 차트, 데이터 시각화 - 목록 3종: 무한 스크롤, 그룹/섹션, 벌크 액션 - 폼 3종: 인라인 편집, 리치 텍스트 에디터, 프로필 카드 - 모달 4종: 확인 다이얼로그, 라이트박스, 알림 센터, 날짜 선택기 - 네비게이션 3종: 메가 메뉴, 모바일 하단바, 다단계 드롭다운 - 기타 3종: 드래그 정렬, 스켈레톤 로딩, 알림 배지
4562 lines
301 KiB
PHP
4562 lines
301 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', '디자인 인사이트')
|
||
|
||
@section('content')
|
||
<style>
|
||
/* ===== Design Insight Core ===== */
|
||
:root {
|
||
--di-sidebar: 280px;
|
||
--di-toolbar: 48px;
|
||
--di-blue: #3b82f6;
|
||
--di-indigo: #6366f1;
|
||
--di-green: #10b981;
|
||
--di-amber: #f59e0b;
|
||
--di-red: #ef4444;
|
||
--di-purple: #8b5cf6;
|
||
--di-pink: #ec4899;
|
||
--di-cyan: #0ea5e9;
|
||
--di-slate: #64748b;
|
||
--di-bg: #f8fafc;
|
||
--di-card-bg: #ffffff;
|
||
--di-border: #e2e8f0;
|
||
--di-text: #1e293b;
|
||
--di-text-secondary: #64748b;
|
||
--di-radius: 10px;
|
||
--di-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
||
--di-shadow-lg: 0 4px 12px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.06);
|
||
}
|
||
|
||
/* Wrap */
|
||
.di-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 56px);
|
||
background: var(--di-bg);
|
||
overflow: hidden;
|
||
font-family: 'Pretendard', -apple-system, sans-serif;
|
||
}
|
||
|
||
/* Toolbar */
|
||
.di-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
height: var(--di-toolbar);
|
||
padding: 0 16px;
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--di-border);
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
z-index: 20;
|
||
}
|
||
.di-toolbar .di-title-input {
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
min-width: 200px;
|
||
}
|
||
.di-toolbar .di-title-input:hover { background: #f1f5f9; }
|
||
.di-toolbar .di-title-input:focus { outline: 2px solid var(--di-blue); background: #fff; }
|
||
.di-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: 1px solid var(--di-border);
|
||
background: #fff;
|
||
color: var(--di-text);
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
}
|
||
.di-btn:hover { background: #f1f5f9; border-color: #cbd5e1; }
|
||
.di-btn.primary { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
|
||
.di-btn.primary:hover { background: #2563eb; }
|
||
.di-btn.sm { padding: 4px 8px; font-size: 11.5px; }
|
||
.di-btn.ghost { border: none; background: transparent; }
|
||
.di-btn.ghost:hover { background: #f1f5f9; }
|
||
.di-view-tabs {
|
||
display: flex;
|
||
gap: 2px;
|
||
background: #f1f5f9;
|
||
border-radius: 8px;
|
||
padding: 3px;
|
||
}
|
||
.di-view-tab {
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
color: var(--di-text-secondary);
|
||
transition: all .15s;
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
.di-view-tab:hover { color: var(--di-text); }
|
||
.di-view-tab.active { background: #fff; color: var(--di-text); box-shadow: 0 1px 2px rgba(0,0,0,.08); font-weight: 600; }
|
||
|
||
/* Body */
|
||
.di-body {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Sidebar */
|
||
.di-sidebar {
|
||
width: var(--di-sidebar);
|
||
background: #fff;
|
||
border-right: 1px solid var(--di-border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
overflow-y: auto;
|
||
transition: width .2s;
|
||
}
|
||
.di-sidebar.collapsed { width: 0; overflow: hidden; padding: 0; border-right: none; }
|
||
.di-sidebar-section {
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--di-border);
|
||
}
|
||
.di-sidebar-section h4 {
|
||
font-size: 10.5px;
|
||
font-weight: 700;
|
||
color: var(--di-text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-sidebar-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 7px 10px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
color: var(--di-text);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
}
|
||
.di-sidebar-item:hover { background: #f1f5f9; }
|
||
.di-sidebar-item.active { background: #eff6ff; color: var(--di-blue); font-weight: 600; }
|
||
.di-sidebar-item .cnt {
|
||
margin-left: auto;
|
||
font-size: 11px;
|
||
color: var(--di-text-secondary);
|
||
background: #f1f5f9;
|
||
padding: 1px 7px;
|
||
border-radius: 10px;
|
||
}
|
||
.di-tag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 3px 10px;
|
||
border-radius: 20px;
|
||
font-size: 11.5px;
|
||
cursor: pointer;
|
||
border: 1px solid var(--di-border);
|
||
background: #fff;
|
||
color: var(--di-text-secondary);
|
||
margin: 2px;
|
||
transition: all .12s;
|
||
}
|
||
.di-tag-chip:hover { border-color: var(--di-blue); color: var(--di-blue); }
|
||
.di-tag-chip.active { background: var(--di-blue); color: #fff; border-color: var(--di-blue); }
|
||
.di-search-input {
|
||
width: 100%;
|
||
padding: 7px 10px 7px 32px;
|
||
border: 1px solid var(--di-border);
|
||
border-radius: 8px;
|
||
font-size: 12.5px;
|
||
background: #f8fafc;
|
||
transition: all .15s;
|
||
}
|
||
.di-search-input:focus { outline: none; border-color: var(--di-blue); background: #fff; box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
|
||
|
||
/* Main */
|
||
.di-main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Category Tabs */
|
||
.di-cat-tabs {
|
||
display: flex;
|
||
gap: 1px;
|
||
padding: 10px 20px 0;
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--di-border);
|
||
overflow-x: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-cat-tab {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
color: var(--di-text-secondary);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
background: none;
|
||
border-top: none;
|
||
border-left: none;
|
||
border-right: none;
|
||
}
|
||
.di-cat-tab:hover { color: var(--di-text); }
|
||
.di-cat-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
|
||
|
||
/* Content */
|
||
.di-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* Board View */
|
||
.di-board {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
/* Card */
|
||
.di-card {
|
||
background: var(--di-card-bg);
|
||
border-radius: var(--di-radius);
|
||
border: 1px solid var(--di-border);
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
position: relative;
|
||
}
|
||
.di-card:hover { box-shadow: var(--di-shadow-lg); border-color: #cbd5e1; transform: translateY(-1px); }
|
||
.di-card .card-img {
|
||
width: 100%;
|
||
height: 180px;
|
||
object-fit: cover;
|
||
background: #f1f5f9;
|
||
display: block;
|
||
}
|
||
.di-card .card-img-placeholder {
|
||
width: 100%;
|
||
height: 180px;
|
||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #94a3b8;
|
||
font-size: 32px;
|
||
}
|
||
.di-card .card-body {
|
||
padding: 14px;
|
||
}
|
||
.di-card .card-type {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10.5px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
margin-bottom: 6px;
|
||
}
|
||
.di-card .card-type.ref { background: #eff6ff; color: #2563eb; }
|
||
.di-card .card-type.analysis { background: #fef3c7; color: #d97706; }
|
||
.di-card .card-type.pattern { background: #ecfdf5; color: #059669; }
|
||
.di-card .card-type.comparison { background: #fae8ff; color: #a855f7; }
|
||
.di-card .card-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
margin-bottom: 6px;
|
||
line-height: 1.4;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
.di-card .card-memo {
|
||
font-size: 12.5px;
|
||
color: var(--di-text-secondary);
|
||
line-height: 1.5;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-card .card-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-card .card-tag {
|
||
font-size: 10.5px;
|
||
padding: 2px 7px;
|
||
border-radius: 4px;
|
||
background: #f1f5f9;
|
||
color: var(--di-text-secondary);
|
||
}
|
||
.di-card .card-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
.di-card .card-rating { color: #f59e0b; font-size: 12px; letter-spacing: 1px; }
|
||
.di-card .card-date { font-size: 11px; color: #94a3b8; }
|
||
.di-card .card-pin {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: rgba(255,255,255,.9);
|
||
border-radius: 50%;
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity .15s;
|
||
border: 1px solid var(--di-border);
|
||
}
|
||
.di-card:hover .card-pin { opacity: 1; }
|
||
.di-card .card-pin.pinned { opacity: 1; color: var(--di-amber); }
|
||
|
||
/* Add Card Button */
|
||
.di-add-card {
|
||
background: var(--di-card-bg);
|
||
border-radius: var(--di-radius);
|
||
border: 2px dashed var(--di-border);
|
||
min-height: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
gap: 8px;
|
||
color: var(--di-text-secondary);
|
||
}
|
||
.di-add-card:hover { border-color: var(--di-blue); color: var(--di-blue); background: #f0f7ff; }
|
||
.di-add-card i { font-size: 28px; }
|
||
.di-add-card span { font-size: 13px; font-weight: 500; }
|
||
|
||
/* Gallery View */
|
||
.di-gallery {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
.di-gallery .di-card .card-img { height: 220px; }
|
||
.di-gallery .di-card .card-body { padding: 10px; }
|
||
|
||
/* List View */
|
||
.di-list { display: flex; flex-direction: column; gap: 4px; }
|
||
.di-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 10px 14px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--di-border);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
}
|
||
.di-list-item:hover { background: #f8fafc; box-shadow: var(--di-shadow); }
|
||
.di-list-item .li-thumb {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 6px;
|
||
object-fit: cover;
|
||
background: #f1f5f9;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-list-item .li-thumb-empty {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 6px;
|
||
background: #f1f5f9;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #94a3b8;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-list-item .li-info { flex: 1; min-width: 0; }
|
||
.di-list-item .li-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.di-list-item .li-memo { font-size: 12px; color: var(--di-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.di-list-item .li-tags { display: flex; gap: 4px; flex-shrink: 0; }
|
||
.di-list-item .li-date { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
|
||
|
||
/* Modal */
|
||
.di-modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,.4);
|
||
z-index: 100;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.di-modal {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
width: 680px;
|
||
max-width: 95vw;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||
}
|
||
.di-modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--di-border);
|
||
}
|
||
.di-modal-header h3 { font-size: 16px; font-weight: 700; color: var(--di-text); }
|
||
.di-modal-body { padding: 20px; }
|
||
.di-modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding: 14px 20px;
|
||
border-top: 1px solid var(--di-border);
|
||
}
|
||
.di-field { margin-bottom: 16px; }
|
||
.di-field label {
|
||
display: block;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: var(--di-text);
|
||
margin-bottom: 5px;
|
||
}
|
||
.di-field input[type="text"],
|
||
.di-field textarea,
|
||
.di-field select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--di-border);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
transition: all .15s;
|
||
background: #fff;
|
||
}
|
||
.di-field input:focus, .di-field textarea:focus, .di-field select:focus {
|
||
outline: none;
|
||
border-color: var(--di-blue);
|
||
box-shadow: 0 0 0 3px rgba(59,130,246,.1);
|
||
}
|
||
.di-field textarea { min-height: 80px; resize: vertical; }
|
||
|
||
/* Image Drop Zone */
|
||
.di-drop-zone {
|
||
border: 2px dashed var(--di-border);
|
||
border-radius: 10px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
background: #fafbfc;
|
||
}
|
||
.di-drop-zone:hover, .di-drop-zone.dragover {
|
||
border-color: var(--di-blue);
|
||
background: #eff6ff;
|
||
}
|
||
.di-drop-zone img {
|
||
max-width: 100%;
|
||
max-height: 250px;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Before/After comparison */
|
||
.di-comparison {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
.di-comparison .comp-side {
|
||
border: 2px dashed var(--di-border);
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
background: #fafbfc;
|
||
min-height: 150px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.di-comparison .comp-side:hover { border-color: var(--di-blue); background: #eff6ff; }
|
||
.di-comparison .comp-side img { max-width: 100%; max-height: 200px; object-fit: contain; border-radius: 8px; }
|
||
.di-comparison .comp-label {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 3px 10px;
|
||
border-radius: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.di-comparison .comp-before .comp-label { background: #fef2f2; color: #dc2626; }
|
||
.di-comparison .comp-after .comp-label { background: #ecfdf5; color: #059669; }
|
||
|
||
/* CRAP Checklist */
|
||
.di-crap-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.di-crap-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--di-border);
|
||
cursor: pointer;
|
||
transition: all .12s;
|
||
font-size: 12.5px;
|
||
}
|
||
.di-crap-item:hover { background: #f8fafc; }
|
||
.di-crap-item.pass { background: #ecfdf5; border-color: #a7f3d0; }
|
||
.di-crap-item.fail { background: #fef2f2; border-color: #fecaca; }
|
||
.di-crap-item.warn { background: #fffbeb; border-color: #fde68a; }
|
||
|
||
/* Rating Stars */
|
||
.di-stars {
|
||
display: flex;
|
||
gap: 2px;
|
||
}
|
||
.di-star {
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
color: #e2e8f0;
|
||
transition: color .1s;
|
||
}
|
||
.di-star.filled { color: #f59e0b; }
|
||
.di-star:hover { color: #fbbf24; }
|
||
|
||
/* Help Modal */
|
||
.di-help-modal { width: 740px; }
|
||
.di-help-modal .di-modal-body { padding: 0; }
|
||
.di-help-tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid var(--di-border);
|
||
padding: 0 20px;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
}
|
||
.di-help-tab {
|
||
padding: 12px 16px;
|
||
font-size: 13px;
|
||
color: var(--di-text-secondary);
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
white-space: nowrap;
|
||
background: none;
|
||
border-top: none; border-left: none; border-right: none;
|
||
transition: all .15s;
|
||
}
|
||
.di-help-tab:hover { color: var(--di-text); }
|
||
.di-help-tab.active { color: var(--di-blue); border-bottom-color: var(--di-blue); font-weight: 600; }
|
||
.di-help-content { padding: 24px; max-height: 60vh; overflow-y: auto; }
|
||
.di-help-content h4 { font-size: 15px; font-weight: 700; color: var(--di-text); margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }
|
||
.di-help-content h4 i { font-size: 18px; color: var(--di-blue); }
|
||
.di-help-content p { font-size: 13px; color: var(--di-text-secondary); line-height: 1.7; margin: 0 0 16px; }
|
||
.di-help-section { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #f1f5f9; }
|
||
.di-help-section:last-child { border-bottom: none; margin-bottom: 0; }
|
||
.di-help-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.di-help-item {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
border: 1px solid #f1f5f9;
|
||
}
|
||
.di-help-item .h-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-help-item .h-body { flex: 1; }
|
||
.di-help-item .h-title { font-size: 13px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
|
||
.di-help-item .h-desc { font-size: 11.5px; color: var(--di-text-secondary); line-height: 1.5; }
|
||
.di-help-kbd {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px 8px;
|
||
background: #f1f5f9;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 4px;
|
||
font-size: 11.5px;
|
||
font-family: monospace;
|
||
color: var(--di-text);
|
||
margin: 2px;
|
||
}
|
||
.di-help-step {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
.di-help-step .step-num {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: var(--di-blue);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
.di-help-step .step-body { flex: 1; padding-top: 3px; }
|
||
.di-help-step .step-title { font-size: 13.5px; font-weight: 600; color: var(--di-text); margin-bottom: 2px; }
|
||
.di-help-step .step-desc { font-size: 12.5px; color: var(--di-text-secondary); line-height: 1.6; }
|
||
|
||
/* 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 ? '📌' : '📌'"></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. 탭 구조 → 섹션 접기/펼치기 변경 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
|