6222 lines
440 KiB
PHP
6222 lines
440 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 패턴 100종
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="di-view-tabs">
|
||
<button class="di-view-tab" :class="viewMode === 'board' && 'active'" @click="viewMode = 'board'" title="보드 뷰">
|
||
<i class="ri-layout-grid-line"></i>
|
||
</button>
|
||
<button class="di-view-tab" :class="viewMode === 'gallery' && 'active'" @click="viewMode = 'gallery'" title="갤러리 뷰">
|
||
<i class="ri-image-line"></i>
|
||
</button>
|
||
<button class="di-view-tab" :class="viewMode === 'list' && 'active'" @click="viewMode = 'list'" title="리스트 뷰">
|
||
<i class="ri-list-unordered"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<button class="di-btn sm" @click="showProjectsModal = true" title="프로젝트 관리">
|
||
<i class="ri-folder-line"></i>
|
||
</button>
|
||
<button class="di-btn sm" @click="showHelpModal = true" title="도움말 (?)">
|
||
<i class="ri-question-line"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Category Tabs -->
|
||
<div class="di-cat-tabs">
|
||
<button class="di-cat-tab" :class="categoryFilter === 'all' && 'active'" @click="categoryFilter = 'all'">
|
||
전체 <span x-text="'(' + filteredCards.length + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
|
||
</button>
|
||
<template x-for="type in cardTypes" :key="type.code">
|
||
<button class="di-cat-tab" :class="categoryFilter === type.code && 'active'" @click="categoryFilter = type.code">
|
||
<span x-text="type.icon + ' ' + type.label"></span>
|
||
<span x-text="'(' + getCardCountByType(type.code) + ')'" style="font-size: 11px; color: #94a3b8; margin-left: 2px;"></span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="di-body">
|
||
|
||
<!-- Sidebar -->
|
||
<div class="di-sidebar" :class="!sidebarOpen && 'collapsed'">
|
||
<!-- Search -->
|
||
<div class="di-sidebar-section">
|
||
<div style="position: relative;">
|
||
<i class="ri-search-line" style="position: absolute; left: 10px; top: 8px; font-size: 14px; color: #94a3b8;"></i>
|
||
<input type="text" class="di-search-input" x-model="searchQuery" placeholder="검색..." @keyup.escape="searchQuery = ''">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Categories -->
|
||
<div class="di-sidebar-section">
|
||
<h4>카테고리</h4>
|
||
<div class="di-sidebar-item" :class="screenFilter === 'all' && 'active'" @click="screenFilter = 'all'">
|
||
<span>전체</span>
|
||
<span class="cnt" x-text="currentProject.cards?.length || 0"></span>
|
||
</div>
|
||
<template x-for="cat in categories" :key="cat.code">
|
||
<div class="di-sidebar-item" :class="screenFilter === cat.code && 'active'" @click="screenFilter = cat.code">
|
||
<span x-text="cat.icon + ' ' + cat.label"></span>
|
||
<span class="cnt" x-text="getCardCountByCat(cat.code)"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Tags -->
|
||
<div class="di-sidebar-section">
|
||
<h4>태그</h4>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 2px;">
|
||
<template x-for="tag in allTags" :key="tag">
|
||
<span class="di-tag-chip" :class="selectedTags.includes(tag) && 'active'"
|
||
@click="toggleTag(tag)" x-text="tag"></span>
|
||
</template>
|
||
<template x-if="allTags.length === 0">
|
||
<span style="font-size: 12px; color: #94a3b8;">태그 없음</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sort -->
|
||
<div class="di-sidebar-section">
|
||
<h4>정렬</h4>
|
||
<select style="width: 100%; padding: 6px 8px; border: 1px solid var(--di-border); border-radius: 6px; font-size: 12px;" x-model="sortBy" @change="$nextTick()">
|
||
<option value="newest">최신순</option>
|
||
<option value="oldest">오래된순</option>
|
||
<option value="rating">평점순</option>
|
||
<option value="title">이름순</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="di-main">
|
||
<div class="di-content">
|
||
|
||
<!-- Empty State -->
|
||
<template x-if="filteredCards.length === 0 && !searchQuery && selectedTags.length === 0 && screenFilter === 'all' && categoryFilter === 'all'">
|
||
<div class="di-empty">
|
||
<i class="ri-palette-line"></i>
|
||
<h3>인사이트를 수집해보세요</h3>
|
||
<p><kbd>Ctrl+V</kbd>로 스크린샷을 붙여넣거나, 아래 버튼으로 시작하세요</p>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="di-btn primary" @click="openNewCardModal('reference')">
|
||
<i class="ri-add-line"></i> 첫 번째 카드 추가
|
||
</button>
|
||
<button class="di-btn" @click="loadPresetTemplates()" style="border-color: var(--di-indigo); color: var(--di-indigo);">
|
||
<i class="ri-magic-line"></i> 인기 UI 패턴 100종 불러오기
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- No Results -->
|
||
<template x-if="filteredCards.length === 0 && (searchQuery || selectedTags.length > 0 || screenFilter !== 'all' || categoryFilter !== 'all')">
|
||
<div class="di-empty">
|
||
<i class="ri-search-line"></i>
|
||
<h3>검색 결과 없음</h3>
|
||
<p>필터를 변경하거나 검색어를 수정하세요</p>
|
||
<button class="di-btn" @click="clearFilters()">
|
||
<i class="ri-refresh-line"></i> 필터 초기화
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Board View -->
|
||
<template x-if="viewMode === 'board' && filteredCards.length > 0">
|
||
<div class="di-board">
|
||
<template x-for="card in filteredCards" :key="card.id">
|
||
<div class="di-card" @click="openPreviewModal(card)">
|
||
<!-- Pin -->
|
||
<div class="card-pin" :class="card.pinned && 'pinned'"
|
||
@click.stop="togglePin(card)" x-text="card.pinned ? '📌' : '📌'"></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>`;
|
||
|
||
// ===== 추가 50종 와이어프레임 (51~100) =====
|
||
// 게이지/미터
|
||
if (key.includes('게이지') || key.includes('미터') && key.includes('대시보드')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
||
${[{l:'CPU 사용률',v:'73%',c:'#f59e0b',pct:73},{l:'메모리',v:'45%',c:'#10b981',pct:45},{l:'달성률',v:'92%',c:'#ef4444',pct:92}].map(g => `
|
||
<div style="text-align:center;">
|
||
<svg width="80" height="50" viewBox="0 0 80 50">
|
||
<path d="M10,45 A30,30 0 0,1 70,45" fill="none" stroke="#f1f5f9" stroke-width="6" stroke-linecap="round"/>
|
||
<path d="M10,45 A30,30 0 0,1 70,45" fill="none" stroke="${g.c}" stroke-width="6" stroke-linecap="round" stroke-dasharray="${g.pct*0.94} 100"/>
|
||
</svg>
|
||
<div style="font-size:14px;font-weight:800;color:#1e293b;margin-top:-8px;">${g.v}</div>
|
||
<div style="font-size:8px;color:#94a3b8;margin-top:2px;">${g.l}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 히트맵 캘린더
|
||
if (key.includes('히트맵') || key.includes('잔디') && key.includes('캘린더')) return `
|
||
<div class="wf-wrap" style="padding:12px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">활동 기록</div>
|
||
<div style="display:flex;gap:2px;">
|
||
<div style="display:flex;flex-direction:column;gap:2px;font-size:6px;color:#94a3b8;padding-right:4px;justify-content:space-between;">
|
||
<span>월</span><span>수</span><span>금</span>
|
||
</div>
|
||
<div style="display:flex;gap:2px;flex-wrap:nowrap;overflow:hidden;flex:1;">
|
||
${Array.from({length:26},(_, w) => `<div style="display:flex;flex-direction:column;gap:2px;">${Array.from({length:7},(_, d) => {
|
||
const r = Math.random();
|
||
const colors = ['#f1f5f9','#dcfce7','#86efac','#22c55e','#15803d'];
|
||
const ci = r > 0.7 ? (r > 0.9 ? 4 : 3) : (r > 0.4 ? (r > 0.55 ? 2 : 1) : 0);
|
||
return `<div style="width:8px;height:8px;background:${colors[ci]};border-radius:1px;"></div>`;
|
||
}).join('')}</div>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:4px;justify-content:flex-end;margin-top:6px;">
|
||
<span style="font-size:7px;color:#94a3b8;">적음</span>
|
||
${['#f1f5f9','#dcfce7','#86efac','#22c55e','#15803d'].map(c => `<div style="width:8px;height:8px;background:${c};border-radius:1px;"></div>`).join('')}
|
||
<span style="font-size:7px;color:#94a3b8;">많음</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 퍼널/전환율
|
||
if (key.includes('퍼널') || key.includes('전환율')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">수주 파이프라인</div>
|
||
${[{s:'리드',v:'1,240',w:100,c:'#bfdbfe'},{s:'미팅',v:'680',w:82,c:'#93c5fd',r:'54.8%'},{s:'제안',v:'340',w:64,c:'#60a5fa',r:'50.0%'},{s:'협상',v:'180',w:46,c:'#3b82f6',r:'52.9%'},{s:'수주',v:'95',w:28,c:'#1d4ed8',r:'52.8%'}].map(f => `
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||
<span style="font-size:8px;color:#64748b;width:28px;">${f.s}</span>
|
||
<div style="width:${f.w}%;height:22px;background:${f.c};border-radius:4px;display:flex;align-items:center;padding:0 8px;">
|
||
<span style="font-size:9px;font-weight:600;color:#1e293b;">${f.v}</span>
|
||
</div>
|
||
${f.r?`<span style="font-size:7px;color:#10b981;">→ ${f.r}</span>`:''}
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 비교 대시보드
|
||
if (key.includes('비교') && (key.includes('전월') || key.includes('당월') || key.includes('대시보드'))) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">기간 비교</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:8px;padding:2px 6px;background:#3b82f6;color:#fff;border-radius:3px;">3월</span>
|
||
<span style="font-size:8px;color:#94a3b8;">vs</span>
|
||
<span style="font-size:8px;padding:2px 6px;background:#f1f5f9;color:#64748b;border-radius:3px;">2월</span>
|
||
</div>
|
||
<div style="padding:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
||
${[{l:'매출',a:'₩48.5M',b:'₩42.1M',d:'+15.2%',up:true},{l:'수주',a:'247건',b:'198건',d:'+24.7%',up:true},{l:'미수금',a:'₩3.2M',b:'₩2.8M',d:'+14.3%',up:false},{l:'거래처',a:'52개',b:'49개',d:'+6.1%',up:true}].map(k =>
|
||
`<div class="wf-box" style="padding:8px;">
|
||
<div style="font-size:7px;color:#94a3b8;margin-bottom:4px;">${k.l}</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:baseline;">
|
||
<span style="font-size:12px;font-weight:800;color:#1e293b;">${k.a}</span>
|
||
<span style="font-size:8px;color:#94a3b8;">${k.b}</span>
|
||
</div>
|
||
<div style="font-size:7px;color:${k.up?'#10b981':'#ef4444'};margin-top:2px;">${k.up?'▲':'▲'} ${k.d}</div>
|
||
</div>`
|
||
).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 지도 기반
|
||
if (key.includes('지도') && (key.includes('데이터') || key.includes('시각화') || key.includes('분포'))) return `
|
||
<div class="wf-wrap" style="padding:0;background:#e8f4f8;min-height:200px;position:relative;">
|
||
<div style="position:absolute;top:8px;left:8px;background:#fff;border-radius:6px;padding:6px 8px;box-shadow:0 2px 8px rgba(0,0,0,.1);z-index:2;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">거래처 분포</div>
|
||
${['서울 28','경기 15','부산 8'].map(r => `<div style="font-size:7px;color:#64748b;padding:1px 0;">${r}개</div>`).join('')}
|
||
</div>
|
||
<div style="position:absolute;top:8px;right:8px;display:flex;flex-direction:column;gap:2px;z-index:2;">
|
||
<div style="width:24px;height:24px;background:#fff;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:0 1px 4px rgba(0,0,0,.1);">+</div>
|
||
<div style="width:24px;height:24px;background:#fff;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:0 1px 4px rgba(0,0,0,.1);">−</div>
|
||
</div>
|
||
${[{x:35,y:30,s:20,c:'#3b82f6'},{x:40,y:35,s:14,c:'#3b82f6'},{x:55,y:70,s:10,c:'#3b82f6'},{x:25,y:55,s:8,c:'#3b82f6'},{x:60,y:45,s:6,c:'#3b82f6'}].map(m =>
|
||
`<div style="position:absolute;left:${m.x}%;top:${m.y}%;width:${m.s}px;height:${m.s}px;background:${m.c};opacity:.4;border-radius:50%;border:2px solid ${m.c};"></div>`
|
||
).join('')}
|
||
<div style="position:absolute;left:35%;top:28%;font-size:8px;color:#1e293b;font-weight:600;background:rgba(255,255,255,.8);padding:1px 4px;border-radius:2px;">서울</div>
|
||
</div>`;
|
||
|
||
// 마스터-디테일
|
||
if (key.includes('마스터') && key.includes('디테일')) return `
|
||
<div class="wf-wrap" style="display:flex;padding:0;min-height:220px;">
|
||
<div style="width:160px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;">
|
||
<div style="padding:8px;border-bottom:1px solid #e2e8f0;">
|
||
<div style="height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 8px;font-size:8px;line-height:24px;color:#94a3b8;">🔍 검색...</div>
|
||
</div>
|
||
${['수주 #1024|ABC산업|true','수주 #1023|XYZ전자|false','수주 #1022|한국물산|false','수주 #1021|글로벌|false'].map(r => {
|
||
const [t,c,a] = r.split('|');
|
||
return `<div style="padding:6px 8px;border-bottom:1px solid #f1f5f9;${a==='true'?'background:#eff6ff;border-left:2px solid #3b82f6;':''}">
|
||
<div style="font-size:9px;font-weight:${a==='true'?'600':'400'};color:#1e293b;">${t}</div>
|
||
<div style="font-size:7px;color:#94a3b8;">${c}</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div style="flex:1;padding:12px;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||
<div style="font-size:12px;font-weight:700;color:#1e293b;">수주 #1024</div>
|
||
<div style="display:flex;gap:4px;">
|
||
<span style="font-size:8px;padding:2px 8px;background:#3b82f6;color:#fff;border-radius:4px;">편집</span>
|
||
<span style="font-size:8px;padding:2px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">인쇄</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:8px;">
|
||
${['거래처|ABC산업(주)','금액|₩12,500,000','납기일|2026-04-15','상태|진행 중'].map(f => {
|
||
const [k,v] = f.split('|');
|
||
return `<div class="wf-box" style="padding:6px 8px;"><span style="color:#94a3b8;">${k}</span><div style="color:#1e293b;font-weight:500;margin-top:2px;">${v}</div></div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 타일/썸네일 뷰
|
||
if (key.includes('타일') || key.includes('썸네일') && key.includes('뷰')) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">이미지 라이브러리</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:10px;color:#3b82f6;">▦</span>
|
||
<span style="font-size:10px;color:#94a3b8;">≡</span>
|
||
<span style="font-size:8px;color:#94a3b8;">정렬 ▾</span>
|
||
</div>
|
||
<div style="padding:10px;display:grid;grid-template-columns:repeat(3,1fr);gap:6px;">
|
||
${[{h:70,c:'#bfdbfe',t:'도면 A-101'},{h:90,c:'#bbf7d0',t:'제품 사진'},{h:60,c:'#fde68a',t:'견적서'},{h:80,c:'#ddd6fe',t:'시방서'},{h:65,c:'#fecaca',t:'카탈로그'},{h:75,c:'#e0e7ff',t:'설치 매뉴얼'}].map(i => `
|
||
<div style="border-radius:6px;overflow:hidden;border:1px solid #e2e8f0;">
|
||
<div style="height:${i.h}px;background:${i.c};display:flex;align-items:center;justify-content:center;font-size:16px;opacity:.5;">📸</div>
|
||
<div style="padding:4px 6px;">
|
||
<div style="font-size:8px;font-weight:500;color:#1e293b;">${i.t}</div>
|
||
<div style="font-size:7px;color:#94a3b8;">2026-03-08</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 피벗 테이블
|
||
if (key.includes('피벗') || key.includes('크로스탭')) return `
|
||
<div class="wf-wrap" style="padding:0;font-size:8px;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">매출 분석 (피벗)</span>
|
||
</div>
|
||
<div style="overflow:auto;">
|
||
<table style="width:100%;border-collapse:collapse;">
|
||
<thead><tr style="background:#f8fafc;">
|
||
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:left;">거래처 \\ 월</th>
|
||
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">1월</th>
|
||
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">2월</th>
|
||
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">3월</th>
|
||
<th style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;font-weight:800;">합계</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
${[['ABC산업','12.5','15.2','18.3','46.0'],['XYZ전자','8.1','9.4','11.2','28.7'],['한국물산','5.3','6.8','7.5','19.6']].map(r =>
|
||
`<tr><td style="padding:5px 8px;border:1px solid #e2e8f0;font-weight:500;">${r[0]}</td>${r.slice(1).map((v,i) => `<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;${i===3?'font-weight:800;background:#f8fafc;':''}">${v}M</td>`).join('')}</tr>`
|
||
).join('')}
|
||
<tr style="background:#f8fafc;font-weight:800;">
|
||
<td style="padding:5px 8px;border:1px solid #e2e8f0;">합계</td>
|
||
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">25.9M</td>
|
||
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">31.4M</td>
|
||
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;">37.0M</td>
|
||
<td style="padding:5px 8px;border:1px solid #e2e8f0;text-align:right;color:#3b82f6;">94.3M</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 수평 타임라인
|
||
if (key.includes('수평') && key.includes('타임라인')) return `
|
||
<div class="wf-wrap" style="padding:16px;overflow-x:auto;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:16px;">프로젝트 로드맵</div>
|
||
<div style="position:relative;min-width:400px;height:100px;">
|
||
<div style="position:absolute;top:40px;left:0;right:0;height:2px;background:#e2e8f0;"></div>
|
||
${[{x:5,t:'기획',d:'1월',done:true},{x:25,t:'설계',d:'2월',done:true},{x:45,t:'개발',d:'3월',active:true},{x:65,t:'테스트',d:'4월'},{x:85,t:'배포',d:'5월'}].map(e => `
|
||
<div style="position:absolute;left:${e.x}%;top:${e.done||e.active?28:28}px;transform:translateX(-50%);text-align:center;">
|
||
<div style="width:18px;height:18px;border-radius:50%;margin:0 auto;display:flex;align-items:center;justify-content:center;font-size:8px;
|
||
${e.done?'background:#10b981;color:#fff;':e.active?'background:#3b82f6;color:#fff;box-shadow:0 0 0 4px rgba(59,130,246,.2);':'background:#f1f5f9;color:#94a3b8;border:1px solid #e2e8f0;'}">${e.done?'✓':e.active?'●':'○'}</div>
|
||
<div style="font-size:9px;font-weight:600;color:${e.done||e.active?'#1e293b':'#94a3b8'};margin-top:6px;">${e.t}</div>
|
||
<div style="font-size:7px;color:#94a3b8;">${e.d}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 트리 테이블
|
||
if (key.includes('트리') && key.includes('테이블')) return `
|
||
<div class="wf-wrap" style="padding:0;font-size:8px;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:6px;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">BOM 구조</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:8px;color:#3b82f6;">전체 펼치기</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:0;padding:4px 12px;border-bottom:1px solid #e2e8f0;font-weight:600;color:#94a3b8;">
|
||
<div>품명</div><div style="text-align:center;">수량</div><div style="text-align:right;">단가</div><div style="text-align:right;">금액</div>
|
||
</div>
|
||
${[{n:'▾ 블라인드 50mm',q:'1',p:'',a:'15,000',bold:true,lv:0},{n:'알루미늄 슬랫',q:'25',p:'200',a:'5,000',lv:1},{n:'헤드레일',q:'1',p:'3,000',a:'3,000',lv:1},{n:'▸ 부속품 세트',q:'1',p:'',a:'4,500',lv:1,collapsed:true},{n:'조작 코드',q:'1',p:'2,500',a:'2,500',lv:1}].map(r => `
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:0;padding:5px 12px;border-bottom:1px solid #f1f5f9;${r.bold?'font-weight:600;background:#f8fafc;':''}color:#475569;align-items:center;">
|
||
<div style="padding-left:${r.lv*16}px;${r.collapsed?'color:#94a3b8;':''}">${r.n}</div>
|
||
<div style="text-align:center;">${r.q}</div>
|
||
<div style="text-align:right;">${r.p}</div>
|
||
<div style="text-align:right;">${r.a}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 멀티 스텝 폼
|
||
if (key.includes('멀티') && key.includes('스텝') || key.includes('멀티스텝')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="display:flex;align-items:center;margin-bottom:16px;">
|
||
${[{n:'1',t:'기본 정보',done:true},{n:'2',t:'상세 설정',active:true},{n:'3',t:'확인'}].map((s,i) => `
|
||
<div style="display:flex;align-items:center;${i>0?'flex:1;':''}">
|
||
${i>0?`<div style="flex:1;height:2px;background:${s.done||s.active?'#3b82f6':'#e2e8f0'};margin:0 8px;"></div>`:''}
|
||
<div style="width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;
|
||
${s.done?'background:#3b82f6;color:#fff;':s.active?'border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${s.done?'✓':s.n}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:10px;">상세 설정</div>
|
||
${['납기일','배송 방법','결제 조건'].map(f => `<div style="margin-bottom:8px;"><div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:3px;">${f}</div><div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div></div>`).join('')}
|
||
<div style="display:flex;justify-content:space-between;margin-top:12px;">
|
||
<span style="font-size:9px;color:#3b82f6;padding:5px 12px;">← 이전</span>
|
||
<div style="padding:5px 14px;background:#3b82f6;color:#fff;border-radius:6px;font-size:9px;">다음 →</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 태그/칩 입력
|
||
if (key.includes('태그') && (key.includes('칩') || key.includes('입력'))) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="font-size:9px;font-weight:600;color:#475569;margin-bottom:4px;">태그</div>
|
||
<div style="min-height:36px;background:#f8fafc;border:2px solid #3b82f6;border-radius:6px;padding:4px 6px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
|
||
${['블라인드','50mm','수동','알루미늄'].map(t => `<span style="display:flex;align-items:center;gap:3px;background:#eff6ff;color:#3b82f6;padding:2px 6px;border-radius:4px;font-size:8px;font-weight:500;">${t}<span style="color:#93c5fd;cursor:pointer;font-size:10px;">×</span></span>`).join('')}
|
||
<span style="font-size:9px;color:#94a3b8;">태그 입력...</span>
|
||
</div>
|
||
<div style="margin-top:4px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.08);overflow:hidden;">
|
||
${['전동','롤스크린','커튼'].map((s,i) => `<div style="padding:5px 10px;font-size:9px;color:#475569;${i===0?'background:#f8fafc;':''}cursor:pointer;">${s}</div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 슬라이더/레인지
|
||
if (key.includes('슬라이더') && (key.includes('레인지') || key.includes('조절'))) return `
|
||
<div class="wf-wrap" style="padding:20px;">
|
||
<div style="margin-bottom:20px;">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
||
<span style="font-size:9px;font-weight:600;color:#475569;">가격 범위</span>
|
||
<span style="font-size:9px;color:#3b82f6;font-weight:600;">₩10,000 ~ ₩50,000</span>
|
||
</div>
|
||
<div style="position:relative;height:6px;background:#e2e8f0;border-radius:3px;">
|
||
<div style="position:absolute;left:20%;right:35%;height:100%;background:#3b82f6;border-radius:3px;"></div>
|
||
<div style="position:absolute;left:20%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #3b82f6;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
|
||
<div style="position:absolute;left:65%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #3b82f6;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;margin-top:4px;font-size:7px;color:#94a3b8;"><span>₩0</span><span>₩100,000</span></div>
|
||
</div>
|
||
<div>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
||
<span style="font-size:9px;font-weight:600;color:#475569;">수량</span>
|
||
<span style="font-size:9px;color:#1e293b;font-weight:600;">65</span>
|
||
</div>
|
||
<div style="position:relative;height:6px;background:#e2e8f0;border-radius:3px;">
|
||
<div style="position:absolute;left:0;width:65%;height:100%;background:#10b981;border-radius:3px;"></div>
|
||
<div style="position:absolute;left:65%;top:-5px;width:16px;height:16px;background:#fff;border:2px solid #10b981;border-radius:50%;transform:translateX(-50%);box-shadow:0 1px 4px rgba(0,0,0,.1);"></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 토글 설정
|
||
if (key.includes('토글') && (key.includes('설정') || key.includes('스위치'))) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:11px;font-weight:700;color:#1e293b;">알림 설정</div>
|
||
${[{t:'이메일 알림',d:'수주/견적 상태 변경 시 이메일 발송',on:true},{t:'푸시 알림',d:'실시간 푸시 알림 수신',on:true},{t:'슬랙 연동',d:'슬랙 채널에 알림 전달',on:false},{t:'주간 리포트',d:'매주 월요일 주간 요약 발송',on:true},{t:'마케팅 메일',d:'신규 기능 안내 및 팁',on:false}].map(s => `
|
||
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px;">
|
||
<div style="flex:1;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;">${s.t}</div>
|
||
<div style="font-size:8px;color:#94a3b8;">${s.d}</div>
|
||
</div>
|
||
<div style="width:36px;height:20px;border-radius:10px;background:${s.on?'#3b82f6':'#e2e8f0'};position:relative;cursor:pointer;">
|
||
<div style="position:absolute;top:2px;${s.on?'right:2px;':'left:2px;'}width:16px;height:16px;background:#fff;border-radius:50%;box-shadow:0 1px 2px rgba(0,0,0,.15);"></div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 서명 패드
|
||
if (key.includes('서명') && (key.includes('패드') || key.includes('전자'))) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">전자 서명</div>
|
||
<div style="border:2px dashed #cbd5e1;border-radius:8px;padding:20px;background:repeating-linear-gradient(0deg,transparent,transparent 19px,#f1f5f9 19px,#f1f5f9 20px);min-height:80px;position:relative;display:flex;align-items:center;justify-content:center;">
|
||
<svg width="120" height="40" viewBox="0 0 120 40"><path d="M10,30 Q20,10 40,25 T70,15 T100,28" fill="none" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/></svg>
|
||
<div style="position:absolute;bottom:4px;right:8px;font-size:7px;color:#94a3b8;">마우스/터치로 서명</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;margin-top:8px;">
|
||
<span style="font-size:8px;padding:4px 10px;background:#f1f5f9;color:#64748b;border-radius:4px;">지우기</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:8px;padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;">서명 완료</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 컬러 피커
|
||
if (key.includes('컬러') && key.includes('피커')) return `
|
||
<div style="width:100%;max-width:240px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.1);overflow:hidden;border:1px solid #e2e8f0;padding:10px;">
|
||
<div style="width:100%;height:100px;border-radius:6px;background:linear-gradient(to right,#fff,#3b82f6);position:relative;">
|
||
<div style="position:absolute;inset:0;background:linear-gradient(to bottom,transparent,#000);border-radius:6px;"></div>
|
||
<div style="position:absolute;left:65%;top:35%;width:12px;height:12px;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.3);"></div>
|
||
</div>
|
||
<div style="margin-top:8px;height:10px;border-radius:5px;background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00);position:relative;">
|
||
<div style="position:absolute;left:60%;top:-2px;width:14px;height:14px;border:2px solid #fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.3);background:#3b82f6;"></div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;margin-top:8px;align-items:center;">
|
||
<div style="width:28px;height:28px;background:#3b82f6;border-radius:6px;border:1px solid #e2e8f0;"></div>
|
||
<div style="flex:1;height:24px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:0 6px;font-size:9px;line-height:24px;color:#475569;">#3B82F6</div>
|
||
</div>
|
||
<div style="display:flex;gap:3px;margin-top:6px;">
|
||
${['#ef4444','#f59e0b','#10b981','#3b82f6','#8b5cf6','#ec4899','#64748b','#1e293b'].map(c => `<div style="width:18px;height:18px;background:${c};border-radius:4px;cursor:pointer;"></div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 평점/별점
|
||
if (key.includes('별점') || key.includes('평점') && key.includes('리뷰')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="text-align:center;margin-bottom:16px;">
|
||
<div style="font-size:28px;font-weight:800;color:#1e293b;">4.5</div>
|
||
<div style="font-size:16px;color:#f59e0b;margin:4px 0;">★★★★☆</div>
|
||
<div style="font-size:8px;color:#94a3b8;">128개 리뷰</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:3px;margin-bottom:12px;">
|
||
${[{s:5,w:65},{s:4,w:20},{s:3,w:8},{s:2,w:4},{s:1,w:3}].map(r => `
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<span style="font-size:8px;color:#64748b;width:12px;">${r.s}★</span>
|
||
<div style="flex:1;height:6px;background:#f1f5f9;border-radius:3px;overflow:hidden;"><div style="height:100%;width:${r.w}%;background:#f59e0b;border-radius:3px;"></div></div>
|
||
<span style="font-size:7px;color:#94a3b8;width:20px;">${r.w}%</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div style="border-top:1px solid #e2e8f0;padding-top:10px;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">리뷰 작성</div>
|
||
<div style="font-size:18px;color:#e2e8f0;margin-bottom:4px;">☆☆☆☆☆</div>
|
||
<div style="height:40px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 바텀 시트
|
||
if (key.includes('바텀') && key.includes('시트')) return `
|
||
<div style="width:100%;max-width:280px;background:#f8fafc;border-radius:20px;overflow:hidden;border:6px solid #1e293b;margin:0 auto;height:280px;position:relative;">
|
||
<div style="padding:10px;opacity:.3;">
|
||
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;"></div>
|
||
<div class="wf-bar lg" style="height:6px;"></div>
|
||
</div>
|
||
<div style="position:absolute;bottom:0;left:0;right:0;background:#fff;border-radius:16px 16px 0 0;box-shadow:0 -4px 20px rgba(0,0,0,.1);padding:8px 16px 16px;">
|
||
<div style="width:32px;height:4px;background:#e2e8f0;border-radius:2px;margin:0 auto 10px;"></div>
|
||
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:8px;">필터</div>
|
||
${['상태','날짜 범위','거래처'].map(f => `<div style="padding:8px 0;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center;font-size:9px;"><span style="color:#475569;">${f}</span><span style="color:#94a3b8;">선택 ▸</span></div>`).join('')}
|
||
<div style="display:flex;gap:6px;margin-top:10px;">
|
||
<div style="flex:1;padding:6px;text-align:center;background:#f1f5f9;border-radius:6px;font-size:9px;color:#64748b;">초기화</div>
|
||
<div style="flex:1;padding:6px;text-align:center;background:#3b82f6;border-radius:6px;font-size:9px;color:#fff;">적용</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 컨텍스트 메뉴
|
||
if (key.includes('컨텍스트') && key.includes('우클릭')) return `
|
||
<div style="width:100%;max-width:400px;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:200px;padding:12px;">
|
||
<div class="wf-bar xl" style="height:8px;margin-bottom:6px;opacity:.3;"></div>
|
||
<div class="wf-bar lg" style="height:6px;opacity:.3;"></div>
|
||
<div style="position:absolute;top:60px;left:120px;width:160px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.12);overflow:hidden;z-index:2;">
|
||
${[{t:'복사',k:'⌘C'},{t:'붙여넣기',k:'⌘V'},{t:'잘라내기',k:'⌘X'},{sep:true},{t:'편집',icon:'✏️'},{t:'복제',icon:'📋'},{t:'이동',icon:'📁'},{sep:true},{t:'삭제',danger:true,icon:'🗑️'}].map(i =>
|
||
i.sep?`<div style="height:1px;background:#e2e8f0;margin:2px 0;"></div>`:
|
||
`<div style="display:flex;align-items:center;gap:6px;padding:5px 10px;font-size:9px;color:${i.danger?'#ef4444':'#475569'};cursor:pointer;">
|
||
${i.icon?`<span>${i.icon}</span>`:''}
|
||
<span style="flex:1;">${i.t}</span>
|
||
${i.k?`<span style="font-size:7px;color:#94a3b8;">${i.k}</span>`:''}
|
||
</div>`
|
||
).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 슬라이드오버 패널
|
||
if (key.includes('슬라이드오버') || key.includes('사이드') && key.includes('패널') && key.includes('상세')) return `
|
||
<div style="width:100%;display:flex;min-height:220px;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;position:relative;overflow:hidden;">
|
||
<div style="flex:1;padding:12px;opacity:.4;">
|
||
<div class="wf-bar xl dark" style="height:10px;margin-bottom:8px;"></div>
|
||
<div class="wf-bar lg" style="height:8px;margin-bottom:16px;"></div>
|
||
<div class="wf-box" style="height:80px;"></div>
|
||
</div>
|
||
<div style="position:absolute;top:0;right:0;bottom:0;width:200px;background:#fff;box-shadow:-4px 0 20px rgba(0,0,0,.1);border-left:1px solid #e2e8f0;">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">수주 상세</span>
|
||
<span style="color:#94a3b8;cursor:pointer;font-size:12px;">✕</span>
|
||
</div>
|
||
<div style="padding:10px 12px;">
|
||
${['거래처|ABC산업','금액|₩12.5M','상태|진행 중','담당자|홍길동'].map(f => {
|
||
const [k,v] = f.split('|');
|
||
return `<div style="margin-bottom:8px;"><div style="font-size:7px;color:#94a3b8;">${k}</div><div style="font-size:9px;color:#1e293b;font-weight:500;">${v}</div></div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div style="padding:8px 12px;border-top:1px solid #e2e8f0;display:flex;gap:4px;">
|
||
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;flex:1;text-align:center;">편집</span>
|
||
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">삭제</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 쿠키 동의 배너
|
||
if (key.includes('쿠키') || key.includes('gdpr') || key.includes('동의') && key.includes('배너')) return `
|
||
<div style="width:100%;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:220px;padding:12px;">
|
||
<div style="opacity:.3;"><div class="wf-bar xl" style="height:10px;margin-bottom:8px;"></div><div class="wf-bar lg" style="height:8px;"></div></div>
|
||
<div style="position:absolute;bottom:0;left:0;right:0;background:#1e293b;border-radius:0 0 8px 8px;padding:12px 16px;display:flex;align-items:center;gap:12px;">
|
||
<span style="font-size:16px;">🍪</span>
|
||
<div style="flex:1;">
|
||
<div style="font-size:9px;color:#e2e8f0;margin-bottom:2px;">이 웹사이트는 쿠키를 사용합니다</div>
|
||
<div style="font-size:7px;color:#94a3b8;">더 나은 경험을 위해 쿠키를 사용합니다. <span style="color:#3b82f6;">자세히 보기</span></div>
|
||
</div>
|
||
<div style="display:flex;gap:4px;flex-shrink:0;">
|
||
<span style="font-size:8px;padding:4px 10px;background:#475569;color:#e2e8f0;border-radius:4px;">설정</span>
|
||
<span style="font-size:8px;padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;">모두 동의</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 탑 네비게이션
|
||
if (key.includes('탑바') || key.includes('gnb') || key.includes('탑') && key.includes('네비게이션') && key.includes('검색')) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="background:#fff;border-bottom:1px solid #e2e8f0;padding:0 16px;height:48px;display:flex;align-items:center;gap:12px;">
|
||
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;">
|
||
<div style="width:24px;height:24px;background:#3b82f6;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:800;">S</div>
|
||
<span style="font-size:12px;font-weight:700;color:#1e293b;">SAM</span>
|
||
</div>
|
||
${['대시보드','수주','거래처','품목'].map((m,i) => `<span style="font-size:9px;color:${i===0?'#3b82f6':'#64748b'};font-weight:${i===0?'600':'400'};padding:14px 0;${i===0?'border-bottom:2px solid #3b82f6;':''}">${m}</span>`).join('')}
|
||
<div style="flex:1;max-width:200px;margin:0 auto;">
|
||
<div style="height:28px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:0 10px;display:flex;align-items:center;gap:4px;">
|
||
<span style="font-size:10px;color:#94a3b8;">🔍</span>
|
||
<span style="font-size:8px;color:#94a3b8;">검색... ⌘K</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;">
|
||
<div style="position:relative;"><span style="font-size:14px;">🔔</span><div style="position:absolute;top:-2px;right:-4px;width:10px;height:10px;background:#ef4444;border-radius:50%;font-size:6px;color:#fff;display:flex;align-items:center;justify-content:center;">3</div></div>
|
||
<div class="wf-circle" style="width:24px;height:24px;"></div>
|
||
</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div class="wf-bar xl dark" style="height:10px;margin-bottom:8px;"></div>
|
||
<div class="wf-box" style="height:60px;padding:10px;"><div class="wf-text">콘텐츠</div></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 앵커/스크롤 스파이
|
||
if (key.includes('앵커') || key.includes('스크롤스파이') || key.includes('목차') && key.includes('사이드')) return `
|
||
<div class="wf-wrap" style="display:flex;padding:0;min-height:220px;">
|
||
<div style="flex:1;padding:14px;overflow:hidden;">
|
||
<div style="font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;">가이드 문서</div>
|
||
${['1. 소개','내용이 여기에 표시됩니다...','2. 설치 방법','npm install sam-sdk','3. 기본 사용법'].map((t,i) =>
|
||
i%2===0?`<div style="font-size:11px;font-weight:600;color:#1e293b;margin:12px 0 4px;${i===2?'color:#3b82f6;':''}">${t}</div>`:
|
||
`<div style="font-size:9px;color:#64748b;line-height:1.6;">${t}</div>`
|
||
).join('')}
|
||
</div>
|
||
<div style="width:120px;border-left:1px solid #e2e8f0;padding:14px 8px;position:sticky;top:0;">
|
||
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:6px;">목차</div>
|
||
${['소개','설치 방법','기본 사용법','API 레퍼런스','FAQ'].map((t,i) => `
|
||
<div style="font-size:8px;padding:3px 8px;color:${i===1?'#3b82f6':'#64748b'};font-weight:${i===1?'600':'400'};border-left:2px solid ${i===1?'#3b82f6':'transparent'};margin-bottom:2px;">${t}</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 페이지네이션
|
||
if (key.includes('페이지네이션') && key.includes('패턴')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">페이지네이션 유형</div>
|
||
<div style="margin-bottom:12px;">
|
||
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">기본형</div>
|
||
<div style="display:flex;align-items:center;gap:3px;">
|
||
${['◀','1','2','3','...','28','29','30','▶'].map(p => `<span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;font-size:8px;cursor:pointer;${p==='3'?'background:#3b82f6;color:#fff;':'background:#f8fafc;color:#64748b;border:1px solid #e2e8f0;'}">${p}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:12px;">
|
||
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">미니형</div>
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<span style="font-size:9px;color:#3b82f6;">← 이전</span>
|
||
<span style="font-size:9px;color:#475569;">3 / 30</span>
|
||
<span style="font-size:9px;color:#3b82f6;">다음 →</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:8px;color:#94a3b8;margin-bottom:4px;">상세형</div>
|
||
<div style="display:flex;align-items:center;gap:8px;">
|
||
<span style="font-size:8px;color:#64748b;">총 842건</span>
|
||
<span style="font-size:8px;color:#64748b;">|</span>
|
||
<span style="font-size:8px;color:#64748b;">페이지당 <span style="padding:1px 4px;background:#f1f5f9;border-radius:2px;">20 ▾</span></span>
|
||
<div style="flex:1;"></div>
|
||
<div style="display:flex;gap:2px;">
|
||
${['◀','1','2','3','▶'].map(p => `<span style="padding:2px 6px;border-radius:3px;font-size:8px;${p==='2'?'background:#3b82f6;color:#fff;':'background:#f1f5f9;color:#64748b;'}">${p}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// FAB
|
||
if (key.includes('fab') || key.includes('플로팅') && key.includes('액션')) return `
|
||
<div style="width:100%;position:relative;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;min-height:240px;padding:12px;">
|
||
<div style="opacity:.3;"><div class="wf-bar xl" style="height:10px;margin-bottom:8px;"></div><div class="wf-bar lg" style="height:8px;margin-bottom:12px;"></div><div class="wf-box" style="height:80px;"></div></div>
|
||
<div style="position:absolute;bottom:16px;right:16px;display:flex;flex-direction:column;align-items:center;gap:6px;">
|
||
${[{icon:'📋',label:'수주 등록'},{icon:'🏢',label:'거래처 등록'},{icon:'📦',label:'품목 등록'}].map(a => `
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<span style="font-size:8px;color:#475569;background:#fff;padding:2px 6px;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.1);">${a.label}</span>
|
||
<div style="width:36px;height:36px;background:#fff;border-radius:50%;box-shadow:0 2px 8px rgba(0,0,0,.1);display:flex;align-items:center;justify-content:center;font-size:14px;">${a.icon}</div>
|
||
</div>
|
||
`).join('')}
|
||
<div style="width:48px;height:48px;background:#3b82f6;border-radius:50%;box-shadow:0 4px 12px rgba(59,130,246,.3);display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;transform:rotate(45deg);">+</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 사이드 탭
|
||
if (key.includes('세로') && key.includes('탭') || key.includes('사이드') && key.includes('탭')) return `
|
||
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
|
||
<div style="width:48px;background:#f8fafc;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;align-items:center;padding:8px 0;gap:2px;">
|
||
${[{icon:'📊',active:true},{icon:'📋',active:false},{icon:'🏢',active:false},{icon:'⚙️',active:false}].map(t => `
|
||
<div style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:6px;font-size:14px;${t.active?'background:#eff6ff;border-left:2px solid #3b82f6;':''}">${t.icon}</div>
|
||
`).join('')}
|
||
</div>
|
||
<div style="flex:1;padding:12px;">
|
||
<div style="font-size:12px;font-weight:700;color:#1e293b;margin-bottom:8px;">📊 대시보드</div>
|
||
<div class="wf-bar lg" style="height:8px;margin-bottom:6px;"></div>
|
||
<div class="wf-bar md" style="height:8px;margin-bottom:12px;"></div>
|
||
<div class="wf-box" style="height:80px;padding:8px;"><div class="wf-text">콘텐츠 영역</div></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// RBAC 권한 관리
|
||
if (key.includes('권한') && (key.includes('rbac') || key.includes('역할') || key.includes('접근'))) return `
|
||
<div class="wf-wrap" style="padding:0;font-size:8px;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;font-size:10px;font-weight:700;color:#1e293b;">역할/권한 관리</div>
|
||
<div style="overflow:auto;">
|
||
<table style="width:100%;border-collapse:collapse;">
|
||
<thead><tr style="background:#f8fafc;">
|
||
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:left;">기능</th>
|
||
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#ef4444;">관리자</th>
|
||
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#3b82f6;">매니저</th>
|
||
<th style="padding:5px 10px;border:1px solid #e2e8f0;text-align:center;color:#10b981;">일반</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
${[['수주 조회','✅','✅','✅'],['수주 등록','✅','✅','❌'],['수주 삭제','✅','❌','❌'],['설정 변경','✅','❌','❌'],['사용자 관리','✅','❌','❌']].map(r =>
|
||
`<tr><td style="padding:4px 10px;border:1px solid #e2e8f0;">${r[0]}</td>${r.slice(1).map(v => `<td style="padding:4px 10px;border:1px solid #e2e8f0;text-align:center;">${v}</td>`).join('')}</tr>`
|
||
).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 세션/디바이스 관리
|
||
if (key.includes('세션') && (key.includes('디바이스') || key.includes('관리'))) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">활성 세션</span>
|
||
<span style="font-size:8px;color:#ef4444;">모두 로그아웃</span>
|
||
</div>
|
||
${[{d:'Chrome · Windows',ip:'192.168.1.100',time:'현재 활동 중',current:true},{d:'Safari · macOS',ip:'10.0.0.55',time:'2시간 전'},{d:'Samsung Internet · Android',ip:'172.16.0.12',time:'어제'}].map(s => `
|
||
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px;">
|
||
<span style="font-size:18px;">${s.d.includes('Windows')?'💻':s.d.includes('mac')?'🖥️':'📱'}</span>
|
||
<div style="flex:1;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;">${s.d} ${s.current?'<span style="font-size:7px;background:#dcfce7;color:#16a34a;padding:1px 4px;border-radius:3px;">이 기기</span>':''}</div>
|
||
<div style="font-size:7px;color:#94a3b8;">${s.ip} · ${s.time}</div>
|
||
</div>
|
||
${!s.current?'<span style="font-size:8px;color:#ef4444;cursor:pointer;">로그아웃</span>':''}
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// API 키 관리
|
||
if (key.includes('api') && key.includes('키') || key.includes('토큰') && key.includes('관리')) return `
|
||
<div class="wf-wrap" style="padding:0;">
|
||
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">🔑 API 키</span>
|
||
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">+ 새 키 생성</span>
|
||
</div>
|
||
${[{name:'Production Key',key:'sk-prod...x8f2',created:'2026-01-15',last:'2분 전'},{name:'Development Key',key:'sk-dev...m4k9',created:'2026-02-20',last:'1시간 전'},{name:'Test Key',key:'sk-test...p2j7',created:'2026-03-01',last:'3일 전'}].map(k => `
|
||
<div style="padding:10px 14px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:8px;">
|
||
<div style="flex:1;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;">${k.name}</div>
|
||
<div style="display:flex;align-items:center;gap:4px;margin-top:2px;">
|
||
<code style="font-size:8px;background:#f8fafc;padding:1px 4px;border-radius:2px;color:#64748b;font-family:monospace;">${k.key}</code>
|
||
<span style="font-size:7px;color:#3b82f6;cursor:pointer;">📋</span>
|
||
</div>
|
||
<div style="font-size:7px;color:#94a3b8;margin-top:1px;">생성: ${k.created} · 마지막 사용: ${k.last}</div>
|
||
</div>
|
||
<span style="font-size:8px;color:#ef4444;">폐기</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 간트 차트
|
||
if (key.includes('간트') || key.includes('gantt')) return `
|
||
<div class="wf-wrap" style="padding:0;font-size:8px;">
|
||
<div style="padding:8px 12px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px;">
|
||
<span style="font-size:10px;font-weight:700;color:#1e293b;">프로젝트 일정</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="color:#94a3b8;">◀</span><span style="font-weight:600;color:#1e293b;">2026년 3월</span><span style="color:#94a3b8;">▶</span>
|
||
</div>
|
||
<div style="display:flex;">
|
||
<div style="width:100px;flex-shrink:0;border-right:1px solid #e2e8f0;">
|
||
<div style="padding:4px 8px;border-bottom:1px solid #e2e8f0;font-weight:600;color:#94a3b8;height:24px;">작업</div>
|
||
${['기획','UI 설계','개발','테스트','배포'].map(t => `<div style="padding:4px 8px;border-bottom:1px solid #f1f5f9;color:#475569;height:20px;">${t}</div>`).join('')}
|
||
</div>
|
||
<div style="flex:1;overflow:hidden;">
|
||
<div style="display:flex;border-bottom:1px solid #e2e8f0;height:24px;">
|
||
${Array.from({length:10},(_, i) => `<div style="flex:1;padding:4px;text-align:center;color:#94a3b8;border-right:1px solid #f1f5f9;">${i+1}</div>`).join('')}
|
||
</div>
|
||
${[{s:0,w:3,c:'#10b981',p:100},{s:2,w:3,c:'#3b82f6',p:80},{s:3,w:5,c:'#f59e0b',p:40},{s:6,w:3,c:'#8b5cf6',p:0},{s:8,w:2,c:'#ec4899',p:0}].map(b => `
|
||
<div style="height:20px;position:relative;border-bottom:1px solid #f1f5f9;">
|
||
<div style="position:absolute;left:${b.s*10}%;width:${b.w*10}%;top:3px;height:14px;background:${b.c};opacity:.2;border-radius:3px;"></div>
|
||
<div style="position:absolute;left:${b.s*10}%;width:${b.w*10*b.p/100}%;top:3px;height:14px;background:${b.c};border-radius:3px;"></div>
|
||
</div>
|
||
`).join('')}
|
||
<div style="position:absolute;left:calc(100px + 50%);top:24px;bottom:0;width:2px;background:#ef4444;opacity:.5;"></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 조직도
|
||
if (key.includes('조직도') && key.includes('트리')) return `
|
||
<div class="wf-wrap" style="padding:16px;text-align:center;">
|
||
<div style="display:inline-block;padding:8px 16px;background:#3b82f6;color:#fff;border-radius:8px;font-size:10px;font-weight:700;">대표이사<div style="font-size:8px;font-weight:400;opacity:.8;">홍길동</div></div>
|
||
<div style="width:2px;height:12px;background:#e2e8f0;margin:0 auto;"></div>
|
||
<div style="display:flex;justify-content:center;gap:0;">
|
||
<div style="flex:1;border-top:2px solid #e2e8f0;"></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:center;gap:20px;margin-top:0;">
|
||
${['영업팀|김영업','개발팀|이개발','생산팀|박생산'].map(d => {
|
||
const [dept,lead] = d.split('|');
|
||
return `<div style="text-align:center;">
|
||
<div style="width:2px;height:12px;background:#e2e8f0;margin:0 auto;"></div>
|
||
<div style="display:inline-block;padding:6px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;font-size:9px;font-weight:600;color:#1e293b;">${dept}<div style="font-size:7px;color:#64748b;font-weight:400;">${lead}</div></div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 워터폴 차트
|
||
if (key.includes('워터폴') && key.includes('재무')) return `
|
||
<div class="wf-wrap" style="padding:12px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">손익 분석</div>
|
||
<div style="display:flex;align-items:flex-end;gap:4px;height:120px;padding-bottom:20px;position:relative;">
|
||
${[{l:'매출',v:100,h:100,c:'#3b82f6',base:0},{l:'원가',v:-40,h:40,c:'#ef4444',base:60},{l:'판관비',v:-25,h:25,c:'#ef4444',base:35},{l:'기타수익',v:10,h:10,c:'#10b981',base:35},{l:'영업이익',v:45,h:45,c:'#8b5cf6',base:0}].map(b => `
|
||
<div style="flex:1;display:flex;flex-direction:column;align-items:center;">
|
||
<div style="font-size:7px;color:#475569;font-weight:600;margin-bottom:2px;">${b.v>0?'+':''}${b.v}M</div>
|
||
<div style="width:100%;height:${b.h}%;margin-bottom:${b.base}%;background:${b.c};border-radius:3px 3px 0 0;opacity:.8;min-height:2px;"></div>
|
||
<div style="font-size:6px;color:#94a3b8;margin-top:2px;white-space:nowrap;">${b.l}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 리포트 빌더
|
||
if (key.includes('리포트') && key.includes('빌더') || key.includes('커스텀') && key.includes('보고서')) return `
|
||
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
|
||
<div style="width:120px;border-right:1px solid #e2e8f0;padding:8px;">
|
||
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:4px;">필드 선택</div>
|
||
${['📋 거래처명','💰 수주금액','📅 수주일','👤 담당자','📊 상태'].map((f,i) => `<div style="padding:3px 6px;font-size:8px;color:${i<3?'#3b82f6':'#64748b'};background:${i<3?'#eff6ff':'transparent'};border-radius:3px;margin-bottom:2px;cursor:grab;">${f} ${i<3?'✓':''}</div>`).join('')}
|
||
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin:6px 0 4px;">차트</div>
|
||
${['📊 바','📈 라인','🍩 도넛'].map((c,i) => `<div style="padding:3px 6px;font-size:8px;color:${i===0?'#3b82f6':'#64748b'};background:${i===0?'#eff6ff':'transparent'};border-radius:3px;margin-bottom:2px;">${c}</div>`).join('')}
|
||
</div>
|
||
<div style="flex:1;padding:10px;">
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:6px;">미리보기</div>
|
||
<div class="wf-box" style="padding:8px;height:60px;">
|
||
<div style="display:flex;align-items:flex-end;gap:4px;height:40px;">
|
||
${[60,40,80,50,70,90,45].map(h => `<div style="flex:1;height:${h}%;background:#3b82f6;border-radius:2px 2px 0 0;opacity:.6;"></div>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:4px;margin-top:6px;">
|
||
<span style="font-size:8px;padding:3px 8px;background:#3b82f6;color:#fff;border-radius:4px;">실행</span>
|
||
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">저장</span>
|
||
<span style="font-size:8px;padding:3px 8px;background:#f1f5f9;color:#64748b;border-radius:4px;">📥 PDF</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 댓글/스레드
|
||
if (key.includes('댓글') || key.includes('스레드') && key.includes('답글')) return `
|
||
<div class="wf-wrap" style="padding:12px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:10px;">💬 댓글 3</div>
|
||
${[{u:'김영업',t:'견적 금액이 확정되면 알려주세요.',time:'2시간 전',replies:[{u:'이관리',t:'네, 오늘 중으로 확인하겠습니다.',time:'1시간 전'}]},{u:'박생산',t:'납기일 조정이 필요합니다.',time:'30분 전',replies:[]}].map(c => `
|
||
<div style="margin-bottom:10px;">
|
||
<div style="display:flex;gap:6px;">
|
||
<div class="wf-circle" style="width:24px;height:24px;flex-shrink:0;"></div>
|
||
<div style="flex:1;">
|
||
<div style="font-size:9px;"><span style="font-weight:600;color:#1e293b;">${c.u}</span> <span style="color:#94a3b8;font-size:7px;">${c.time}</span></div>
|
||
<div style="font-size:9px;color:#475569;margin:3px 0;">${c.t}</div>
|
||
<div style="display:flex;gap:8px;font-size:7px;color:#94a3b8;"><span>👍 2</span><span>답글</span></div>
|
||
${c.replies.map(r => `
|
||
<div style="display:flex;gap:6px;margin-top:6px;padding-left:8px;border-left:2px solid #e2e8f0;">
|
||
<div class="wf-circle" style="width:20px;height:20px;flex-shrink:0;"></div>
|
||
<div><div style="font-size:8px;"><span style="font-weight:600;color:#1e293b;">${r.u}</span> <span style="color:#94a3b8;font-size:7px;">${r.time}</span></div><div style="font-size:8px;color:#475569;margin-top:2px;">${r.t}</div></div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 에러 페이지
|
||
if (key.includes('에러') && (key.includes('404') || key.includes('500') || key.includes('오류'))) return `
|
||
<div class="wf-wrap" style="padding:30px;text-align:center;">
|
||
<div style="font-size:48px;font-weight:900;color:#e2e8f0;line-height:1;">404</div>
|
||
<div style="font-size:13px;font-weight:700;color:#1e293b;margin:8px 0 4px;">페이지를 찾을 수 없습니다</div>
|
||
<div style="font-size:9px;color:#94a3b8;margin-bottom:16px;line-height:1.6;">요청하신 페이지가 존재하지 않거나<br>이동되었을 수 있습니다</div>
|
||
<div style="display:flex;justify-content:center;gap:6px;">
|
||
<span style="font-size:9px;padding:5px 14px;background:#3b82f6;color:#fff;border-radius:6px;">홈으로 가기</span>
|
||
<span style="font-size:9px;padding:5px 14px;background:#f1f5f9;color:#64748b;border-radius:6px;">← 이전 페이지</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 비교 테이블
|
||
if (key.includes('비교') && (key.includes('기능') || key.includes('제품') || key.includes('체크'))) return `
|
||
<div class="wf-wrap" style="padding:0;font-size:8px;">
|
||
<div style="display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:0;">
|
||
<div style="padding:8px;border:1px solid #e2e8f0;"></div>
|
||
${['Basic','Pro','Enterprise'].map((p,i) => `<div style="padding:8px;border:1px solid ${i===1?'#3b82f6':'#e2e8f0'};text-align:center;${i===1?'background:#eff6ff;':''}"><div style="font-weight:700;color:${i===1?'#3b82f6':'#1e293b'};">${p}</div>${i===1?'<div style="font-size:6px;background:#3b82f6;color:#fff;padding:1px 4px;border-radius:6px;display:inline-block;margin-top:2px;">추천</div>':''}</div>`).join('')}
|
||
${[['사용자 수','5명','50명','무제한'],['저장 공간','1GB','10GB','무제한'],['API 호출','1,000/일','10,000/일','무제한'],['우선 지원','❌','✅','✅'],['커스텀 도메인','❌','❌','✅']].map(r =>
|
||
`<div style="padding:5px 8px;border:1px solid #e2e8f0;color:#475569;">${r[0]}</div>${r.slice(1).map((v,i) => `<div style="padding:5px 8px;border:1px solid ${i===1?'#3b82f6':'#e2e8f0'};text-align:center;${i===1?'background:#f8fbff;':''}">${v}</div>`).join('')}`
|
||
).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 캐러셀/슬라이더
|
||
if (key.includes('캐러셀') || key.includes('슬라이더') && key.includes('스와이프')) return `
|
||
<div class="wf-wrap" style="padding:0;position:relative;overflow:hidden;">
|
||
<div style="display:flex;">
|
||
<div style="width:100%;flex-shrink:0;height:140px;background:linear-gradient(135deg,#667eea,#764ba2);display:flex;align-items:center;justify-content:center;color:#fff;">
|
||
<div style="text-align:center;"><div style="font-size:12px;font-weight:700;">슬라이드 2 / 5</div><div style="font-size:9px;opacity:.8;">콘텐츠가 여기에 표시됩니다</div></div>
|
||
</div>
|
||
</div>
|
||
<div style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;background:rgba(255,255,255,.8);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 2px 4px rgba(0,0,0,.1);">◀</div>
|
||
<div style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;background:rgba(255,255,255,.8);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;box-shadow:0 2px 4px rgba(0,0,0,.1);">▶</div>
|
||
<div style="position:absolute;bottom:8px;left:50%;transform:translateX(-50%);display:flex;gap:4px;">
|
||
${[false,true,false,false,false].map(a => `<div style="width:${a?'16px':'6px'};height:6px;background:${a?'#fff':'rgba(255,255,255,.5)'};border-radius:3px;"></div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 아코디언/FAQ
|
||
if (key.includes('아코디언') || key.includes('faq')) return `
|
||
<div class="wf-wrap" style="padding:12px;">
|
||
<div style="font-size:11px;font-weight:700;color:#1e293b;margin-bottom:10px;">자주 묻는 질문</div>
|
||
${[{q:'SAM은 어떤 서비스인가요?',a:'SAM은 블라인드/스크린 제조업체를 위한 ERP/MES 통합 솔루션입니다.',open:true},{q:'무료 체험이 가능한가요?',a:'',open:false},{q:'데이터 마이그레이션을 지원하나요?',a:'',open:false},{q:'커스터마이징이 가능한가요?',a:'',open:false}].map(f => `
|
||
<div style="border:1px solid ${f.open?'#3b82f6':'#e2e8f0'};border-radius:6px;margin-bottom:4px;overflow:hidden;">
|
||
<div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;${f.open?'background:#eff6ff;':''}">
|
||
<span style="font-size:9px;font-weight:600;color:${f.open?'#3b82f6':'#1e293b'};">${f.q}</span>
|
||
<span style="font-size:10px;color:${f.open?'#3b82f6':'#94a3b8'};transition:transform .2s;${f.open?'transform:rotate(180deg);':''}">▾</span>
|
||
</div>
|
||
${f.open?`<div style="padding:0 12px 8px;font-size:9px;color:#64748b;line-height:1.6;">${f.a}</div>`:''}
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 툴팁/팝오버
|
||
if (key.includes('툴팁') || key.includes('팝오버')) return `
|
||
<div class="wf-wrap" style="padding:24px;display:flex;flex-direction:column;align-items:center;gap:24px;">
|
||
<div style="position:relative;display:inline-block;">
|
||
<div style="padding:4px 10px;background:#f1f5f9;border-radius:4px;font-size:9px;color:#64748b;">hover 요소</div>
|
||
<div style="position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#1e293b;color:#fff;padding:4px 8px;border-radius:4px;font-size:8px;white-space:nowrap;">
|
||
이것은 툴팁입니다
|
||
<div style="position:absolute;top:100%;left:50%;transform:translateX(-50%);border:4px solid transparent;border-top-color:#1e293b;"></div>
|
||
</div>
|
||
</div>
|
||
<div style="position:relative;display:inline-block;">
|
||
<div style="padding:4px 10px;background:#3b82f6;color:#fff;border-radius:4px;font-size:9px;">클릭 요소</div>
|
||
<div style="position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:10px;box-shadow:0 4px 12px rgba(0,0,0,.1);width:160px;">
|
||
<div style="position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#fff;"></div>
|
||
<div style="font-size:9px;font-weight:600;color:#1e293b;margin-bottom:4px;">팝오버 제목</div>
|
||
<div style="font-size:8px;color:#64748b;">추가 정보를 여기에 표시합니다.</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 프로그레스 트래커
|
||
if (key.includes('프로그레스') && (key.includes('배송') || key.includes('처리') || key.includes('추적'))) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:12px;">수주 진행 상태</div>
|
||
<div style="display:flex;flex-direction:column;gap:0;padding-left:12px;">
|
||
${[{s:'수주 접수',t:'2026-03-01 09:00',done:true},{s:'설계 검토',t:'2026-03-03 14:00',done:true},{s:'생산 중',t:'2026-03-05 ~',active:true},{s:'품질 검사',t:'',wait:true},{s:'출하/배송',t:'',wait:true}].map(e => `
|
||
<div style="display:flex;gap:10px;padding-bottom:${e.wait&&!e.active?'0':'12'}px;position:relative;">
|
||
<div style="display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:16px;">
|
||
<div style="width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:8px;
|
||
${e.done?'background:#10b981;color:#fff;':e.active?'background:#3b82f6;color:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.2);':'background:#f1f5f9;color:#94a3b8;border:1px solid #e2e8f0;'}">${e.done?'✓':e.active?'●':'○'}</div>
|
||
${!e.wait?`<div style="width:2px;flex:1;background:${e.done?'#10b981':'#e2e8f0'};margin-top:2px;"></div>`:''}
|
||
</div>
|
||
<div style="padding-bottom:4px;">
|
||
<div style="font-size:9px;font-weight:600;color:${e.done||e.active?'#1e293b':'#94a3b8'};">${e.s}</div>
|
||
${e.t?`<div style="font-size:7px;color:#94a3b8;">${e.t}</div>`:''}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 데이터 임포트
|
||
if (key.includes('임포트') || key.includes('가져오기') && key.includes('csv')) return `
|
||
<div class="wf-wrap" style="padding:16px;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
${['1 업로드','2 매핑','3 확인'].map((s,i) => `
|
||
<div style="display:flex;align-items:center;gap:4px;${i>0?'flex:1;':''}">
|
||
${i>0?`<div style="flex:1;height:2px;background:${i<=1?'#3b82f6':'#e2e8f0'};"></div>`:''}
|
||
<div style="width:20px;height:20px;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center;
|
||
${i===0?'background:#3b82f6;color:#fff;':i===1?'border:2px solid #3b82f6;color:#3b82f6;':'background:#f1f5f9;color:#94a3b8;'}">${i+1}</div>
|
||
<span style="font-size:8px;color:${i<=1?'#1e293b':'#94a3b8'};">${s.slice(2)}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div style="font-size:10px;font-weight:700;color:#1e293b;margin-bottom:8px;">필드 매핑</div>
|
||
${['거래처명 → company_name','연락처 → phone','주소 → address'].map(m => `
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||
<span style="font-size:8px;color:#64748b;width:60px;text-align:right;">${m.split(' → ')[0]}</span>
|
||
<span style="font-size:10px;color:#94a3b8;">→</span>
|
||
<div style="flex:1;height:24px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;padding:0 6px;font-size:8px;line-height:24px;color:#3b82f6;">${m.split(' → ')[1]}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>`;
|
||
|
||
// 랜딩 히어로
|
||
if (key.includes('랜딩') || key.includes('히어로') && key.includes('cta')) return `
|
||
<div class="wf-wrap" style="padding:0;background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);min-height:200px;display:flex;align-items:center;justify-content:center;">
|
||
<div style="text-align:center;padding:24px;">
|
||
<div style="font-size:16px;font-weight:800;color:#fff;line-height:1.3;margin-bottom:8px;">스마트한 제조 관리,<br>SAM으로 시작하세요</div>
|
||
<div style="font-size:9px;color:#94a3b8;margin-bottom:16px;line-height:1.6;">수주부터 출하까지, 하나의 플랫폼에서<br>제조 전 공정을 관리하세요</div>
|
||
<div style="display:flex;gap:6px;justify-content:center;">
|
||
<span style="font-size:9px;padding:6px 16px;background:#3b82f6;color:#fff;border-radius:6px;font-weight:600;">무료로 시작하기</span>
|
||
<span style="font-size:9px;padding:6px 16px;background:transparent;color:#fff;border-radius:6px;border:1px solid rgba(255,255,255,.3);">데모 보기</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:center;gap:12px;margin-top:12px;">
|
||
${['제조사 120+','도입 6개월','만족도 98%'].map(s => `<span style="font-size:7px;color:#64748b;">${s}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 코드 에디터
|
||
if (key.includes('코드') && (key.includes('에디터') || key.includes('구문'))) return `
|
||
<div class="wf-wrap" style="padding:0;background:#1e1e1e;border-radius:8px;overflow:hidden;">
|
||
<div style="padding:6px 12px;background:#2d2d2d;display:flex;align-items:center;gap:6px;">
|
||
<div style="display:flex;gap:4px;"><div style="width:8px;height:8px;background:#ff5f56;border-radius:50%;"></div><div style="width:8px;height:8px;background:#ffbd2e;border-radius:50%;"></div><div style="width:8px;height:8px;background:#27ca40;border-radius:50%;"></div></div>
|
||
<span style="font-size:8px;color:#808080;flex:1;text-align:center;">api.php</span>
|
||
<span style="font-size:8px;color:#808080;cursor:pointer;">📋</span>
|
||
</div>
|
||
<div style="padding:10px 12px;font-family:monospace;font-size:9px;line-height:1.8;">
|
||
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">1</span><span style="color:#c586c0;">use</span> <span style="color:#4ec9b0;">App\\Http\\Controllers</span>;</div>
|
||
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">2</span></div>
|
||
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">3</span><span style="color:#569cd6;">Route</span>::<span style="color:#dcdcaa;">get</span>(<span style="color:#ce9178;">'/orders'</span>, <span style="color:#569cd6;">function</span> () {</div>
|
||
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">4</span> <span style="color:#c586c0;">return</span> <span style="color:#4ec9b0;">Order</span>::<span style="color:#dcdcaa;">all</span>();</div>
|
||
<div><span style="color:#808080;width:20px;display:inline-block;text-align:right;margin-right:12px;">5</span>});</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 키보드 단축키
|
||
if (key.includes('단축키') && (key.includes('키보드') || key.includes('도움말'))) return `
|
||
<div style="width:100%;max-width:360px;background:#fff;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);overflow:hidden;border:1px solid #e2e8f0;">
|
||
<div style="padding:10px 14px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
|
||
<span style="font-size:11px;font-weight:700;color:#1e293b;">⌨️ 키보드 단축키</span>
|
||
<span style="color:#94a3b8;font-size:12px;">✕</span>
|
||
</div>
|
||
<div style="padding:8px 14px;">
|
||
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin-bottom:4px;">일반</div>
|
||
${[['⌘ K','검색 열기'],['⌘ N','새로 만들기'],['⌘ S','저장']].map(([k,d]) => `<div style="display:flex;justify-content:space-between;padding:3px 0;"><span style="font-size:9px;color:#475569;">${d}</span><kbd style="font-size:8px;background:#f8fafc;border:1px solid #e2e8f0;padding:1px 6px;border-radius:3px;color:#64748b;font-family:monospace;">${k}</kbd></div>`).join('')}
|
||
<div style="font-size:8px;font-weight:600;color:#94a3b8;margin:6px 0 4px;">네비게이션</div>
|
||
${[['G → D','대시보드'],['G → O','수주 목록'],['?','단축키 도움말']].map(([k,d]) => `<div style="display:flex;justify-content:space-between;padding:3px 0;"><span style="font-size:9px;color:#475569;">${d}</span><kbd style="font-size:8px;background:#f8fafc;border:1px solid #e2e8f0;padding:1px 6px;border-radius:3px;color:#64748b;font-family:monospace;">${k}</kbd></div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// 다크 모드
|
||
if (key.includes('다크') && key.includes('모드') || key.includes('다크모드')) return `
|
||
<div class="wf-wrap" style="padding:0;display:grid;grid-template-columns:1fr 1fr;min-height:160px;">
|
||
<div style="padding:12px;background:#fff;">
|
||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
|
||
<span style="font-size:14px;">☀️</span>
|
||
<span style="font-size:9px;font-weight:600;color:#1e293b;">라이트</span>
|
||
</div>
|
||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px;">
|
||
<div class="wf-bar md dark" style="height:8px;margin-bottom:4px;"></div>
|
||
<div class="wf-bar sm" style="height:6px;margin-bottom:6px;"></div>
|
||
<div style="height:30px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;"></div>
|
||
</div>
|
||
</div>
|
||
<div style="padding:12px;background:#0f172a;">
|
||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:8px;">
|
||
<span style="font-size:14px;">🌙</span>
|
||
<span style="font-size:9px;font-weight:600;color:#e2e8f0;">다크</span>
|
||
</div>
|
||
<div style="background:#1e293b;border:1px solid #334155;border-radius:6px;padding:8px;">
|
||
<div style="height:8px;width:60%;background:#475569;border-radius:4px;margin-bottom:4px;"></div>
|
||
<div style="height:6px;width:40%;background:#334155;border-radius:4px;margin-bottom:6px;"></div>
|
||
<div style="height:30px;background:#0f172a;border:1px solid #334155;border-radius:4px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 파일 매니저
|
||
if (key.includes('파일') && (key.includes('매니저') || key.includes('탐색기'))) return `
|
||
<div class="wf-wrap" style="display:flex;padding:0;min-height:200px;">
|
||
<div style="width:110px;border-right:1px solid #e2e8f0;padding:8px 4px;font-size:8px;">
|
||
<div style="padding:3px 6px;font-weight:600;color:#1e293b;">📁 전체 파일</div>
|
||
${['📂 견적서','📂 도면','📂 계약서','📂 사진'].map((f,i) => `<div style="padding:3px 6px 3px 14px;color:${i===1?'#3b82f6':'#64748b'};${i===1?'background:#eff6ff;border-radius:3px;font-weight:600;':''}">${f}</div>`).join('')}
|
||
</div>
|
||
<div style="flex:1;padding:8px;">
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||
<span style="font-size:8px;color:#3b82f6;">전체 파일</span><span style="font-size:8px;color:#94a3b8;">/</span><span style="font-size:8px;font-weight:600;color:#1e293b;">도면</span>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:8px;padding:2px 6px;background:#3b82f6;color:#fff;border-radius:3px;">업로드</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;">
|
||
${['A-101.dwg|📐','A-102.dwg|📐','제품사진.png|🖼️','시방서.pdf|📄','매뉴얼.pdf|📄','+ 새 폴더|📁'].map(f => {
|
||
const [n,i] = f.split('|');
|
||
return `<div style="border:1px solid ${n.includes('+')?'#cbd5e1':'#e2e8f0'};border-radius:6px;padding:8px;text-align:center;${n.includes('+')?'border-style:dashed;':''}">
|
||
<div style="font-size:18px;margin-bottom:2px;${n.includes('+')?'opacity:.4;':''}">${i}</div>
|
||
<div style="font-size:7px;color:${n.includes('+')?'#94a3b8':'#475569'};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${n}</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// 기본 와이어프레임 (매칭 안 됨)
|
||
return `
|
||
<div class="wf-wrap" style="padding: 24px; text-align: center;">
|
||
<div style="font-size: 32px; margin-bottom: 12px; opacity: .5;"><i class="${this.getTypeIcon(card.type)}" style="font-size:32px;color:#94a3b8;"></i></div>
|
||
<div style="font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 4px;">${card.title || ''}</div>
|
||
<div style="font-size: 10px; color: #94a3b8;">이미지를 추가하면 여기에 표시됩니다</div>
|
||
</div>`;
|
||
},
|
||
|
||
openEditCardModal(card) {
|
||
this.editingCard = {
|
||
...card,
|
||
tagsText: (card.tags || []).join(', '),
|
||
usedInText: (card.usedIn || []).join(', '),
|
||
changesText: (card.changes || []).join('\n'),
|
||
components: card.components ? [...card.components.map(c => ({...c}))] : [{ name: '', required: true }],
|
||
principles: card.principles ? { ...card.principles } : {},
|
||
};
|
||
this.showCardModal = true;
|
||
},
|
||
|
||
closeCardModal() {
|
||
this.showCardModal = false;
|
||
this.editingCard = {};
|
||
},
|
||
|
||
saveCard() {
|
||
const card = this.editingCard;
|
||
|
||
// Parse text fields
|
||
card.tags = card.tagsText ? card.tagsText.split(',').map(t => t.trim()).filter(Boolean) : [];
|
||
card.usedIn = card.usedInText ? card.usedInText.split(',').map(t => t.trim()).filter(Boolean) : [];
|
||
card.changes = card.changesText ? card.changesText.split('\n').map(t => t.trim()).filter(Boolean) : [];
|
||
|
||
// Clean temp fields
|
||
const { tagsText, usedInText, changesText, ...cleanCard } = card;
|
||
|
||
// Remove empty components
|
||
if (cleanCard.components) {
|
||
cleanCard.components = cleanCard.components.filter(c => c.name.trim());
|
||
}
|
||
|
||
if (!cleanCard.id) {
|
||
// New card
|
||
cleanCard.id = 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4);
|
||
cleanCard.createdAt = new Date().toISOString();
|
||
cleanCard.updatedAt = new Date().toISOString();
|
||
if (!this.currentProject.cards) this.currentProject.cards = [];
|
||
this.currentProject.cards.unshift(cleanCard);
|
||
this.toast('카드 추가됨');
|
||
} else {
|
||
// Update card
|
||
cleanCard.updatedAt = new Date().toISOString();
|
||
const idx = this.currentProject.cards.findIndex(c => c.id === cleanCard.id);
|
||
if (idx >= 0) this.currentProject.cards[idx] = cleanCard;
|
||
this.toast('카드 수정됨');
|
||
}
|
||
|
||
this.saveProject();
|
||
this.closeCardModal();
|
||
},
|
||
|
||
deleteCard(id) {
|
||
if (!confirm('이 카드를 삭제하시겠습니까?')) return;
|
||
this.currentProject.cards = this.currentProject.cards.filter(c => c.id !== id);
|
||
this.saveProject();
|
||
this.closeCardModal();
|
||
this.toast('카드 삭제됨');
|
||
},
|
||
|
||
togglePin(card) {
|
||
card.pinned = !card.pinned;
|
||
this.saveProject();
|
||
},
|
||
|
||
// ===== Images =====
|
||
handlePaste(e) {
|
||
if (this.showCardModal || this.showProjectsModal) return;
|
||
const items = e.clipboardData?.items;
|
||
if (!items) return;
|
||
for (const item of items) {
|
||
if (item.type.startsWith('image/')) {
|
||
e.preventDefault();
|
||
const file = item.getAsFile();
|
||
this.readImageFile(file, (dataUrl) => {
|
||
this.openNewCardModal('reference');
|
||
this.$nextTick(() => { this.editingCard.image = dataUrl; });
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
handleDrop(e) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
const file = e.dataTransfer?.files?.[0];
|
||
if (file && file.type.startsWith('image/')) {
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
|
||
}
|
||
},
|
||
|
||
handleFileSelect(e) {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard.image = dataUrl; });
|
||
}
|
||
e.target.value = '';
|
||
},
|
||
|
||
handleCompDrop(e, side) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
const file = e.dataTransfer?.files?.[0];
|
||
if (file && file.type.startsWith('image/')) {
|
||
const key = side === 'before' ? 'beforeImage' : 'afterImage';
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
|
||
}
|
||
},
|
||
|
||
handleCompFileSelect(e, side) {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
const key = side === 'before' ? 'beforeImage' : 'afterImage';
|
||
this.readImageFile(file, (dataUrl) => { this.editingCard[key] = dataUrl; });
|
||
}
|
||
e.target.value = '';
|
||
},
|
||
|
||
readImageFile(file, callback) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => callback(e.target.result);
|
||
reader.readAsDataURL(file);
|
||
},
|
||
|
||
// ===== CRAP Principles =====
|
||
cyclePrinciple(key) {
|
||
if (!this.editingCard.principles) this.editingCard.principles = {};
|
||
const current = this.editingCard.principles[key] || '';
|
||
const cycle = { '': 'pass', 'pass': 'warn', 'warn': 'fail', 'fail': '' };
|
||
this.editingCard.principles[key] = cycle[current] || '';
|
||
},
|
||
|
||
getPrincipleStatus(key) {
|
||
return (this.editingCard.principles || {})[key] || '';
|
||
},
|
||
|
||
getPrincipleIcon(key) {
|
||
const status = this.getPrincipleStatus(key);
|
||
return { pass: '✅', warn: '⚠️', fail: '❌', '': '—' }[status] || '—';
|
||
},
|
||
|
||
// ===== Filters =====
|
||
get filteredCards() {
|
||
let cards = this.currentProject.cards || [];
|
||
|
||
// Card type filter (tabs)
|
||
if (this.categoryFilter !== 'all') {
|
||
cards = cards.filter(c => c.type === this.categoryFilter);
|
||
}
|
||
|
||
// Screen category filter (sidebar)
|
||
if (this.screenFilter !== 'all') {
|
||
cards = cards.filter(c => c.category === this.screenFilter);
|
||
}
|
||
|
||
// Tag filter
|
||
if (this.selectedTags.length > 0) {
|
||
cards = cards.filter(c => {
|
||
const cardTags = c.tags || [];
|
||
return this.selectedTags.some(t => cardTags.includes(t));
|
||
});
|
||
}
|
||
|
||
// Search
|
||
if (this.searchQuery.trim()) {
|
||
const q = this.searchQuery.toLowerCase();
|
||
cards = cards.filter(c =>
|
||
(c.title || '').toLowerCase().includes(q) ||
|
||
(c.memo || '').toLowerCase().includes(q) ||
|
||
(c.suggestion || '').toLowerCase().includes(q) ||
|
||
(c.effect || '').toLowerCase().includes(q) ||
|
||
(c.source || '').toLowerCase().includes(q) ||
|
||
(c.tags || []).some(t => t.toLowerCase().includes(q))
|
||
);
|
||
}
|
||
|
||
// Sort — pinned first
|
||
cards = [...cards].sort((a, b) => {
|
||
if (a.pinned && !b.pinned) return -1;
|
||
if (!a.pinned && b.pinned) return 1;
|
||
return 0;
|
||
});
|
||
|
||
// Sort by
|
||
cards.sort((a, b) => {
|
||
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
||
switch (this.sortBy) {
|
||
case 'newest': return new Date(b.createdAt) - new Date(a.createdAt);
|
||
case 'oldest': return new Date(a.createdAt) - new Date(b.createdAt);
|
||
case 'rating': return (b.rating || 0) - (a.rating || 0);
|
||
case 'title': return (a.title || '').localeCompare(b.title || '');
|
||
default: return 0;
|
||
}
|
||
});
|
||
|
||
return cards;
|
||
},
|
||
|
||
get allTags() {
|
||
const tags = new Set();
|
||
(this.currentProject.cards || []).forEach(c => {
|
||
(c.tags || []).forEach(t => tags.add(t));
|
||
});
|
||
return [...tags].sort();
|
||
},
|
||
|
||
toggleTag(tag) {
|
||
const idx = this.selectedTags.indexOf(tag);
|
||
if (idx >= 0) this.selectedTags.splice(idx, 1);
|
||
else this.selectedTags.push(tag);
|
||
},
|
||
|
||
clearFilters() {
|
||
this.categoryFilter = 'all';
|
||
this.screenFilter = 'all';
|
||
this.searchQuery = '';
|
||
this.selectedTags = [];
|
||
this.sortBy = 'newest';
|
||
},
|
||
|
||
getCardCountByType(type) {
|
||
return (this.currentProject.cards || []).filter(c => c.type === type).length;
|
||
},
|
||
|
||
getCardCountByCat(cat) {
|
||
return (this.currentProject.cards || []).filter(c => c.category === cat).length;
|
||
},
|
||
|
||
// ===== Export / Import =====
|
||
exportJSON() {
|
||
const data = JSON.stringify(this.currentProject, null, 2);
|
||
const blob = new Blob([data], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = (this.currentProject.title || 'design-insight') + '.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
this.toast('JSON 내보내기 완료');
|
||
},
|
||
|
||
importJSON() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.json';
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
try {
|
||
const data = JSON.parse(ev.target.result);
|
||
if (data.id && data.cards) {
|
||
// Check if project already exists
|
||
const existIdx = this.projects.findIndex(p => p.id === data.id);
|
||
if (existIdx >= 0) {
|
||
if (confirm('동일 ID 프로젝트가 있습니다. 덮어쓰시겠습니까?')) {
|
||
this.projects[existIdx] = data;
|
||
} else return;
|
||
} else {
|
||
this.projects.push(data);
|
||
}
|
||
this.saveProjects();
|
||
this.switchProject(data.id);
|
||
this.toast('프로젝트 가져오기 완료');
|
||
}
|
||
} catch { this.toast('JSON 파일 오류'); }
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
input.click();
|
||
},
|
||
|
||
// ===== Keyboard =====
|
||
handleKeydown(e) {
|
||
if (this.showCardModal || this.showProjectsModal) return;
|
||
|
||
// Ctrl+S — Save
|
||
if (e.ctrlKey && e.key === 's') {
|
||
e.preventDefault();
|
||
this.saveProject();
|
||
this.toast('저장됨');
|
||
}
|
||
// Ctrl+N — New card
|
||
if (e.ctrlKey && e.key === 'n') {
|
||
e.preventDefault();
|
||
this.openNewCardModal('reference');
|
||
}
|
||
// Ctrl+F — Focus search
|
||
if (e.ctrlKey && e.key === 'f') {
|
||
e.preventDefault();
|
||
document.querySelector('.di-search-input')?.focus();
|
||
}
|
||
},
|
||
|
||
// ===== Helpers =====
|
||
getTypeLabel(type) {
|
||
const t = this.cardTypes.find(ct => ct.code === type);
|
||
return t ? t.icon + ' ' + t.label : type;
|
||
},
|
||
|
||
getTypeIcon(type) {
|
||
const map = {
|
||
reference: 'ri-camera-line',
|
||
analysis: 'ri-search-eye-line',
|
||
pattern: 'ri-layout-masonry-line',
|
||
comparison: 'ri-arrow-left-right-line',
|
||
};
|
||
return map[type] || 'ri-file-line';
|
||
},
|
||
|
||
getRatingStars(rating) {
|
||
return '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
|
||
},
|
||
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const d = new Date(dateStr);
|
||
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
|
||
},
|
||
|
||
formatTime(d) {
|
||
return String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
|
||
},
|
||
|
||
toast(msg) {
|
||
this.toastMsg = msg;
|
||
setTimeout(() => { this.toastMsg = ''; }, 2500);
|
||
},
|
||
|
||
// ===== Preset Templates =====
|
||
loadPresetTemplates() {
|
||
const existing = (this.currentProject.cards || []).length;
|
||
if (existing > 0 && !confirm('현재 프로젝트에 인기 UI 패턴 100종을 추가합니다. 계속하시겠습니까?')) return;
|
||
|
||
const now = new Date().toISOString();
|
||
const mkId = () => 'di_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
|
||
const presets = [
|
||
{
|
||
type: 'pattern', title: 'KPI 대시보드', category: 'dashboard', rating: 5,
|
||
tags: ['대시보드', 'KPI', '통계', '차트'],
|
||
memo: 'Stripe, Shopify, Vercel 등 SaaS 서비스에서 사용하는 핵심 패턴. 로그인 직후 전체 현황을 3초 안에 파악할 수 있어야 한다.',
|
||
components: [
|
||
{ name: 'KPI 요약 카드 (4~6개, 상단 고정)', required: true },
|
||
{ name: '추이 차트 (라인/바 차트, 기간 선택)', required: true },
|
||
{ name: '최근 활동 피드 / 알림', required: false },
|
||
{ name: '빠른 액션 버튼', required: false },
|
||
{ name: '기간 필터 (오늘/주/월/커스텀)', required: true },
|
||
],
|
||
usedIn: ['Stripe Dashboard', 'Shopify Admin', 'Vercel Dashboard', 'SAM 메인 대시보드'],
|
||
guidelines: 'KPI 카드는 숫자 + 변화율(▲▼) + 미니 스파크라인 조합. 가장 중요한 지표를 좌상단에 배치. 색상은 긍정(초록)/부정(빨강)으로 즉시 인지 가능하게.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '데이터 테이블 + 검색/필터', category: 'list', rating: 5,
|
||
tags: ['테이블', '검색', '필터', '정렬', '페이지네이션'],
|
||
memo: 'Airtable, Notion Database, GitHub Issues 등 데이터 중심 서비스의 핵심 패턴. CRUD 목록 화면의 표준.',
|
||
components: [
|
||
{ name: '검색바 (상단 고정, 플레이스홀더 힌트)', required: true },
|
||
{ name: '필터 칩/드롭다운 (상태, 날짜, 카테고리)', required: true },
|
||
{ name: '데이터 테이블 (컬럼 정렬, 행 선택)', required: true },
|
||
{ name: '페이지네이션 / 무한 스크롤', required: true },
|
||
{ name: '벌크 액션 바 (선택 시 나타남)', required: false },
|
||
{ name: '컬럼 커스터마이징 (표시/숨기기)', required: false },
|
||
],
|
||
usedIn: ['Airtable', 'Notion', 'GitHub Issues', 'SAM 수주목록, 거래처목록, 품목목록'],
|
||
guidelines: '검색은 debounce 300ms 적용. 필터 상태는 URL 파라미터로 유지 (뒤로가기 대응). 빈 상태 시 "결과 없음" + 필터 초기화 버튼 제공.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '칸반 보드 (Kanban)', category: 'dashboard', rating: 5,
|
||
tags: ['칸반', '드래그앤드롭', '워크플로우', '상태관리'],
|
||
memo: 'Trello, Jira, Linear, Notion Board 등 프로젝트 관리 도구의 핵심. 작업 상태를 시각적으로 한눈에 파악.',
|
||
components: [
|
||
{ name: '컬럼 헤더 (상태명 + 카드 수)', required: true },
|
||
{ name: '드래그 가능 카드 (제목 + 라벨 + 담당자)', required: true },
|
||
{ name: '컬럼 간 드래그 앤 드롭', required: true },
|
||
{ name: '카드 추가 버튼 (각 컬럼 하단)', required: true },
|
||
{ name: '필터 (담당자, 라벨, 우선순위)', required: false },
|
||
{ name: 'WIP 제한 표시', required: false },
|
||
],
|
||
usedIn: ['Trello', 'Jira', 'Linear', 'Notion Board', 'GitHub Projects'],
|
||
guidelines: '컬럼은 3~6개 권장 (너무 많으면 가독성 저하). 카드에는 핵심 정보만 표시 (제목 + 라벨 + 아바타). 드래그 시 시각적 피드백 필수.',
|
||
},
|
||
{
|
||
type: 'pattern', title: 'Command Palette (Cmd+K)', category: 'navigation', rating: 5,
|
||
tags: ['커맨드팔레트', '검색', '네비게이션', '키보드'],
|
||
memo: 'Linear, Vercel, GitHub, VS Code, Figma 등 파워유저 대상 서비스의 필수 패턴. 키보드만으로 모든 기능에 접근.',
|
||
components: [
|
||
{ name: '오버레이 모달 (화면 중앙 상단)', required: true },
|
||
{ name: '검색 입력란 (자동 포커스)', required: true },
|
||
{ name: '결과 목록 (아이콘 + 이름 + 단축키)', required: true },
|
||
{ name: '카테고리 그룹핑 (페이지, 액션, 최근)', required: true },
|
||
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
|
||
{ name: '최근 사용 기록', required: false },
|
||
],
|
||
usedIn: ['Linear', 'Vercel', 'GitHub', 'VS Code', 'Figma', 'Raycast'],
|
||
guidelines: 'Cmd+K 또는 Ctrl+K로 열기. 타이핑 즉시 퍼지 검색. 결과는 최대 8~10개 표시. ESC로 닫기. 최근 사용 항목 상단 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '사이드바 네비게이션', category: 'navigation', rating: 5,
|
||
tags: ['사이드바', '메뉴', '네비게이션', '트리'],
|
||
memo: 'Slack, Discord, Notion, Linear, VS Code 등 거의 모든 SaaS 앱의 기본 네비게이션. 접기/펼치기 + 트리 구조.',
|
||
components: [
|
||
{ name: '로고/앱 이름 (상단)', required: true },
|
||
{ name: '메인 메뉴 그룹 (아이콘 + 라벨)', required: true },
|
||
{ name: '접기/펼치기 토글', required: true },
|
||
{ name: '현재 위치 하이라이트', required: true },
|
||
{ name: '트리 구조 (하위 메뉴 들여쓰기)', required: false },
|
||
{ name: '즐겨찾기/고정 섹션', required: false },
|
||
{ name: '사용자 프로필 (하단)', required: false },
|
||
],
|
||
usedIn: ['Slack', 'Discord', 'Notion', 'Linear', 'VS Code', 'SAM MNG 사이드바'],
|
||
guidelines: '너비 240~280px 권장. 접힌 상태에서는 아이콘만 표시 (56px). 메뉴 그룹 간 구분선. 활성 항목은 배경색 + 좌측 인디케이터.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '모달 폼 (생성/편집)', category: 'modal', rating: 4,
|
||
tags: ['모달', '폼', 'CRUD', '입력'],
|
||
memo: '대부분의 SaaS에서 레코드 생성/편집에 사용. 페이지 이동 없이 빠르게 데이터 입력. Notion, Linear, Jira 등.',
|
||
components: [
|
||
{ name: '오버레이 배경 (클릭 시 닫기)', required: true },
|
||
{ name: '모달 헤더 (제목 + 닫기 버튼)', required: true },
|
||
{ name: '폼 필드 (라벨 + 입력 + 검증 메시지)', required: true },
|
||
{ name: '액션 버튼 (저장/취소, 우하단)', required: true },
|
||
{ name: '키보드 지원 (ESC 닫기, Enter 저장)', required: true },
|
||
{ name: '로딩 상태 표시', required: false },
|
||
],
|
||
usedIn: ['Linear', 'Notion', 'Jira', 'SAM 등록/수정 팝업'],
|
||
guidelines: '너비 480~640px 권장. 필드 5개 이하면 모달, 그 이상이면 전체 페이지 고려. 필수 필드 * 표시. Tab 순서 정확히 설정.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '설정 페이지 (그룹 섹션)', category: 'form', rating: 4,
|
||
tags: ['설정', '프로필', '섹션', '그룹'],
|
||
memo: 'GitHub Settings, Vercel Settings, Notion Settings 등. 좌측 탭 메뉴 + 우측 섹션별 설정 카드.',
|
||
components: [
|
||
{ name: '좌측 탭 메뉴 (세로 목록)', required: true },
|
||
{ name: '섹션 카드 (제목 + 설명 + 입력 필드)', required: true },
|
||
{ name: '개별 저장 버튼 (섹션마다)', required: true },
|
||
{ name: '위험 영역 (빨간 테두리, 하단 배치)', required: false },
|
||
{ name: '변경사항 감지 (저장 안 된 변경 알림)', required: false },
|
||
],
|
||
usedIn: ['GitHub Settings', 'Vercel Settings', 'Notion Settings', 'SAM 시스템 설정'],
|
||
guidelines: '좌측 탭은 고정, 우측은 스크롤. 섹션 간 명확한 구분선. 위험 작업(삭제, 비활성화)은 페이지 최하단 빨간 영역에 배치.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '타임라인/활동 피드', category: 'list', rating: 4,
|
||
tags: ['타임라인', '피드', '활동로그', '히스토리'],
|
||
memo: 'GitHub Activity, Twitter/X Feed, Slack Messages 등. 시간순 이벤트 흐름을 표시하는 패턴.',
|
||
components: [
|
||
{ name: '타임라인 세로선 (좌측)', required: true },
|
||
{ name: '이벤트 노드 (아이콘 + 시간)', required: true },
|
||
{ name: '이벤트 카드 (내용 + 작성자 + 시간)', required: true },
|
||
{ name: '날짜 구분선', required: false },
|
||
{ name: '더 보기 / 무한 스크롤', required: false },
|
||
{ name: '이벤트 타입별 아이콘/색상', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Twitter/X', 'Slack', 'Jira', 'SAM 변경이력'],
|
||
guidelines: '최신 이벤트 상단. 아이콘과 색상으로 이벤트 타입 즉시 구분. 시간 표시는 상대 시간(3분 전) + hover 시 절대 시간.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '트리 + 상세 분할 뷰', category: 'form', rating: 4,
|
||
tags: ['트리', '분할뷰', '마스터-디테일', '패널'],
|
||
memo: 'VS Code 파일 탐색기, Figma 레이어 패널, macOS Finder 컬럼 뷰. 계층 구조를 탐색하며 상세를 확인.',
|
||
components: [
|
||
{ name: '좌측 트리 패널 (접기/펼치기 노드)', required: true },
|
||
{ name: '우측 상세 패널 (선택 항목 정보)', required: true },
|
||
{ name: '분할선 (드래그 리사이즈)', required: false },
|
||
{ name: '빈 상태 (선택 안 했을 때)', required: true },
|
||
{ name: '트리 검색/필터', required: false },
|
||
{ name: '드래그 앤 드롭 재정렬', required: false },
|
||
],
|
||
usedIn: ['VS Code', 'Figma', 'macOS Finder', 'SAM 메뉴관리, 조직도'],
|
||
guidelines: '좌측 패널 240~320px, 최소 너비 제한. 선택 항목 시각적 하이라이트. 트리 깊이 4단계 이하 권장. 키보드 화살표 네비게이션 지원.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '온보딩 스테퍼/위자드', category: 'form', rating: 4,
|
||
tags: ['온보딩', '스테퍼', '위자드', '단계별'],
|
||
memo: 'Notion 초기 설정, Stripe 계정 생성, Linear 워크스페이스 설정 등. 복잡한 프로세스를 단계별로 안내.',
|
||
components: [
|
||
{ name: '진행 표시줄 (상단, 현재 단계 강조)', required: true },
|
||
{ name: '단계별 카드 (제목 + 설명 + 입력)', required: true },
|
||
{ name: '이전/다음 버튼', required: true },
|
||
{ name: '단계 건너뛰기 옵션', required: false },
|
||
{ name: '완료 축하 화면 (마지막 단계)', required: false },
|
||
{ name: '현재 진행률 (예: 3/5)', required: true },
|
||
],
|
||
usedIn: ['Notion', 'Stripe', 'Linear', 'Vercel', 'SAM 초기 설정'],
|
||
guidelines: '단계는 3~5개 권장. 각 단계는 하나의 주제에 집중. 이전 단계 데이터 유지. 진행률 시각적 표시 필수. 마지막 단계에서 전체 요약.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '토스트 알림 시스템', category: 'etc', rating: 4,
|
||
tags: ['토스트', '알림', '피드백', '노티피케이션'],
|
||
memo: 'Vercel, Linear, Notion 등 거의 모든 SaaS 앱. 사용자 액션에 대한 즉각적인 피드백을 비침습적으로 제공.',
|
||
components: [
|
||
{ name: '토스트 컨테이너 (우하단 고정)', required: true },
|
||
{ name: '타입별 아이콘+색상 (성공/오류/경고/정보)', required: true },
|
||
{ name: '메시지 텍스트 + 선택적 액션 버튼', required: true },
|
||
{ name: '자동 사라짐 (3~5초)', required: true },
|
||
{ name: '수동 닫기 (X 버튼)', required: true },
|
||
{ name: '다중 토스트 스택', required: false },
|
||
],
|
||
usedIn: ['Vercel', 'Linear', 'Notion', 'Stripe', 'SAM 전역'],
|
||
guidelines: '성공=초록, 오류=빨강, 경고=노랑, 정보=파랑. 텍스트는 1줄 이내. 되돌리기(Undo) 액션 제공 시 사용자 신뢰도 향상. 오류는 자동 사라짐 비활성화.',
|
||
},
|
||
{
|
||
type: 'pattern', title: 'Empty State (빈 상태)', category: 'etc', rating: 4,
|
||
tags: ['빈상태', '온보딩', '가이드', 'CTA'],
|
||
memo: 'Dropbox, Mailchimp, Notion, Linear 등. 데이터가 없을 때 사용자를 안내하는 핵심 패턴. 이탈을 방지하고 첫 행동을 유도.',
|
||
components: [
|
||
{ name: '일러스트/아이콘 (시각적 안내)', required: true },
|
||
{ name: '제목 (상황 설명)', required: true },
|
||
{ name: '설명 텍스트 (다음 단계 안내)', required: true },
|
||
{ name: 'CTA 버튼 (첫 행동 유도)', required: true },
|
||
{ name: '대안 링크 (도움말, 템플릿 등)', required: false },
|
||
],
|
||
usedIn: ['Dropbox', 'Mailchimp', 'Notion', 'Linear', 'SAM 각 목록 화면'],
|
||
guidelines: '일러스트는 브랜드 톤 유지. CTA 버튼은 1개만 (선택 피로 방지). "데이터 없음" 대신 긍정적 표현 사용 ("첫 프로젝트를 시작하세요!").',
|
||
},
|
||
{
|
||
type: 'pattern', title: '검색 + 자동완성 드롭다운', category: 'navigation', rating: 4,
|
||
tags: ['검색', '자동완성', '드롭다운', '서제스트'],
|
||
memo: 'Google, Algolia, GitHub Search, Amazon 등. 타이핑과 동시에 결과를 보여주어 탐색 속도를 극대화.',
|
||
components: [
|
||
{ name: '검색 입력란 (돋보기 아이콘 + placeholder)', required: true },
|
||
{ name: '자동완성 드롭다운 (입력 시 표시)', required: true },
|
||
{ name: '결과 하이라이팅 (매칭 텍스트 볼드)', required: true },
|
||
{ name: '카테고리별 그룹핑', required: false },
|
||
{ name: '최근 검색 기록', required: false },
|
||
{ name: '키보드 네비게이션 (↑↓ + Enter)', required: true },
|
||
],
|
||
usedIn: ['Google', 'Algolia', 'GitHub', 'Amazon', 'SAM 품목 검색'],
|
||
guidelines: 'Debounce 200~300ms. 최소 2글자부터 검색. 결과 최대 8개. 키워드 하이라이팅 필수. ESC로 닫기, 외부 클릭 시 닫기.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '탭 레이아웃', category: 'navigation', rating: 4,
|
||
tags: ['탭', '네비게이션', '섹션', '컨텐츠전환'],
|
||
memo: 'Google Analytics, Stripe Dashboard, GitHub Repo 등. 같은 페이지 내에서 컨텐츠 섹션을 전환.',
|
||
components: [
|
||
{ name: '탭 바 (가로 목록, 활성 탭 하이라이트)', required: true },
|
||
{ name: '탭 콘텐츠 영역', required: true },
|
||
{ name: '탭 카운트 배지', required: false },
|
||
{ name: '탭 아이콘 (선택적)', required: false },
|
||
{ name: '탭 스크롤 (많을 때 화살표)', required: false },
|
||
],
|
||
usedIn: ['Google Analytics', 'Stripe', 'GitHub', 'SAM 품목기준관리, 설정'],
|
||
guidelines: '탭 5개 이하 권장 (7개 초과 시 드롭다운 또는 더보기). 활성 탭은 하단 인디케이터 + 볼드. 탭 전환 시 URL 해시 변경 (북마크 가능).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '카드 그리드 레이아웃', category: 'dashboard', rating: 4,
|
||
tags: ['카드', '그리드', '갤러리', '레이아웃'],
|
||
memo: 'Pinterest, Dribbble, Notion Gallery, YouTube 등. 시각적 콘텐츠를 격자로 배열. 정보 밀도와 시각적 매력 균형.',
|
||
components: [
|
||
{ name: '반응형 그리드 컨테이너', required: true },
|
||
{ name: '카드 (이미지 + 제목 + 메타정보)', required: true },
|
||
{ name: '호버 오버레이 (액션 버튼)', required: false },
|
||
{ name: '무한 스크롤 / 더 보기', required: false },
|
||
{ name: '필터/정렬 툴바', required: false },
|
||
],
|
||
usedIn: ['Pinterest', 'Dribbble', 'YouTube', 'Notion Gallery', 'SAM 대시보드 카드'],
|
||
guidelines: '카드 최소 너비 280px, 간격 16px. 이미지 비율 고정 (16:9 또는 4:3). 호버 시 그림자 + 미세 상승 효과. Skeleton 로딩 적용.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '가격표/플랜 비교', category: 'etc', rating: 4,
|
||
tags: ['가격', '플랜', '비교', '테이블', 'CTA'],
|
||
memo: 'Stripe, Vercel, Notion, Slack 등 SaaS 가격 페이지. 플랜 간 차이를 한눈에 비교하여 구매 결정을 유도.',
|
||
components: [
|
||
{ name: '플랜 카드 (이름 + 가격 + 주요 기능)', required: true },
|
||
{ name: '추천 플랜 강조 (배지, 테두리)', required: true },
|
||
{ name: 'CTA 버튼 (각 플랜마다)', required: true },
|
||
{ name: '상세 기능 비교 테이블 (하단)', required: false },
|
||
{ name: '월간/연간 토글', required: false },
|
||
],
|
||
usedIn: ['Stripe', 'Vercel', 'Notion', 'Slack', 'SAM 요금 안내'],
|
||
guidelines: '3~4개 플랜 권장. 가장 인기 플랜을 시각적으로 강조. 가격은 크고 볼드하게. 무료 체험 CTA 제공. 기능 비교는 ✓/✗ 아이콘.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '캘린더 뷰', category: 'dashboard', rating: 4,
|
||
tags: ['캘린더', '일정', '날짜', '이벤트'],
|
||
memo: 'Google Calendar, Calendly, Notion Calendar 등. 시간 기반 데이터를 날짜 격자에 시각화.',
|
||
components: [
|
||
{ name: '월/주/일 뷰 전환 탭', required: true },
|
||
{ name: '날짜 격자 (이벤트 표시)', required: true },
|
||
{ name: '이벤트 카드 (색상 코딩)', required: true },
|
||
{ name: '이전/다음 네비게이션', required: true },
|
||
{ name: '오늘 버튼 (빠른 이동)', required: true },
|
||
{ name: '이벤트 생성 (날짜 클릭)', required: false },
|
||
],
|
||
usedIn: ['Google Calendar', 'Calendly', 'Notion Calendar', 'SAM 일정관리, 근태'],
|
||
guidelines: '오늘 날짜 강조 (원형 배경). 이벤트 색상은 카테고리별 구분. 주말 배경색 다르게. 이벤트 3개 초과 시 "+N more" 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '채팅/메시징 인터페이스', category: 'etc', rating: 4,
|
||
tags: ['채팅', '메시징', '실시간', '대화'],
|
||
memo: 'Slack, Discord, Intercom, WhatsApp Web 등. 실시간 대화를 위한 인터페이스. 고객 지원 위젯에도 활용.',
|
||
components: [
|
||
{ name: '채널/대화 목록 (좌측)', required: true },
|
||
{ name: '메시지 영역 (시간순 스크롤)', required: true },
|
||
{ name: '메시지 입력란 (하단 고정)', required: true },
|
||
{ name: '메시지 버블 (내 메시지 우측, 상대 좌측)', required: true },
|
||
{ name: '첨부파일/이모지 버튼', required: false },
|
||
{ name: '읽음 표시 / 타이핑 인디케이터', required: false },
|
||
],
|
||
usedIn: ['Slack', 'Discord', 'Intercom', 'WhatsApp Web'],
|
||
guidelines: '새 메시지 자동 스크롤. 날짜 구분선. 연속 메시지는 아바타 한 번만 표시. 링크 미리보기. 이미지 인라인 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '파일 업로드 + 진행률', category: 'modal', rating: 3,
|
||
tags: ['업로드', '파일', '진행률', '드래그앤드롭'],
|
||
memo: 'Dropbox, Google Drive, WeTransfer, Figma 등. 파일 업로드 과정을 시각적으로 안내하여 사용자 불안 해소.',
|
||
components: [
|
||
{ name: '드롭존 (점선 테두리, 드래그 안내)', required: true },
|
||
{ name: '파일 선택 버튼', required: true },
|
||
{ name: '업로드 진행률 바', required: true },
|
||
{ name: '파일 목록 (이름 + 크기 + 상태)', required: true },
|
||
{ name: '개별 취소/삭제 버튼', required: true },
|
||
{ name: '미리보기 (이미지 썸네일)', required: false },
|
||
],
|
||
usedIn: ['Dropbox', 'Google Drive', 'WeTransfer', 'SAM 파일 첨부'],
|
||
guidelines: '드래그 시 드롭존 하이라이트. 파일 크기/형식 제한 사전 안내. 실패 시 재시도 버튼. 다중 파일 동시 업로드 지원.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '브레드크럼 네비게이션', category: 'navigation', rating: 3,
|
||
tags: ['브레드크럼', '경로', '네비게이션', '계층'],
|
||
memo: 'AWS Console, Shopify Admin, Jira, SAM 등. 현재 위치를 계층적으로 표시하여 깊은 네비게이션에서 길을 잃지 않게.',
|
||
components: [
|
||
{ name: '경로 항목 (클릭 가능 링크)', required: true },
|
||
{ name: '구분자 (/ 또는 > 아이콘)', required: true },
|
||
{ name: '현재 페이지 (비링크, 볼드)', required: true },
|
||
{ name: '긴 경로 말줄임 (중간 생략)', required: false },
|
||
{ name: '드롭다운 (형제 페이지 선택)', required: false },
|
||
],
|
||
usedIn: ['AWS Console', 'Shopify', 'Jira', 'SAM 상세 페이지'],
|
||
guidelines: '페이지 상단 좌측 배치. 글꼴 크기 12~13px. 현재 페이지는 클릭 불가 (시각적 구분). 3단계 이상일 때 가장 유용.',
|
||
},
|
||
// ===== 로그인/인증 (auth) =====
|
||
{
|
||
type: 'pattern', title: '로그인 폼 (클래식)', category: 'auth', rating: 5,
|
||
tags: ['로그인', '인증', '폼', '보안'],
|
||
memo: 'GitHub, Google, Notion 등 거의 모든 서비스의 진입점. 심플하고 명확한 로그인 경험이 핵심.',
|
||
components: [
|
||
{ name: '로고 + 서비스명 (상단 중앙)', required: true },
|
||
{ name: '이메일/아이디 입력란', required: true },
|
||
{ name: '비밀번호 입력란 (보기 토글)', required: true },
|
||
{ name: '로그인 버튼 (풀 너비)', required: true },
|
||
{ name: '비밀번호 찾기 링크', required: true },
|
||
{ name: '자동 로그인 체크박스', required: false },
|
||
{ name: '회원가입 링크', required: false },
|
||
],
|
||
usedIn: ['GitHub', 'Google', 'Notion', 'SAM 로그인'],
|
||
guidelines: '폼은 화면 중앙 정렬, 너비 360~400px. 비밀번호 표시/숨기기 아이콘. Enter 키로 로그인 가능. 에러 메시지는 필드 하단에 빨간색.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '소셜 로그인 / SSO', category: 'auth', rating: 4,
|
||
tags: ['소셜로그인', 'SSO', 'OAuth', '간편로그인'],
|
||
memo: 'Vercel, Figma, Notion 등 최신 SaaS. 소셜 계정으로 원클릭 로그인. 가입 허들을 극적으로 낮춤.',
|
||
components: [
|
||
{ name: '소셜 로그인 버튼 (Google, GitHub 등)', required: true },
|
||
{ name: '구분선 ("또는")', required: true },
|
||
{ name: '이메일 로그인 폼 (하단)', required: false },
|
||
{ name: '서비스 로고 + 환영 메시지', required: true },
|
||
{ name: '개인정보 동의 링크', required: true },
|
||
],
|
||
usedIn: ['Vercel', 'Figma', 'Notion', 'Linear'],
|
||
guidelines: 'Google 로그인 최상단 배치 (가장 많이 사용). 버튼에 각 서비스 로고 + 이름 표시. "Continue with Google" 패턴이 표준.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '2단계 인증 (2FA)', category: 'auth', rating: 4,
|
||
tags: ['2FA', 'OTP', '보안', '인증코드'],
|
||
memo: 'GitHub, AWS, Stripe 등 보안이 중요한 서비스. 6자리 코드 입력으로 추가 인증.',
|
||
components: [
|
||
{ name: '안내 텍스트 (인증 방법 설명)', required: true },
|
||
{ name: '6자리 코드 입력란 (각 칸 분리)', required: true },
|
||
{ name: '확인 버튼', required: true },
|
||
{ name: '재전송 버튼 (카운트다운)', required: true },
|
||
{ name: '백업 코드로 인증 링크', required: false },
|
||
{ name: '자동 포커스 이동 (입력 시)', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'AWS', 'Stripe', 'Google'],
|
||
guidelines: '각 숫자 칸은 40x48px, 중앙 정렬. 입력 시 자동으로 다음 칸 포커스. 붙여넣기(Ctrl+V) 시 자동 분배. 타이머 60초 후 재전송 가능.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '비밀번호 재설정 플로우', category: 'auth', rating: 3,
|
||
tags: ['비밀번호', '재설정', '이메일', '복구'],
|
||
memo: 'GitHub, Google, Slack 등. 3단계 플로우: 이메일 입력 → 인증 메일 확인 → 새 비밀번호 설정.',
|
||
components: [
|
||
{ name: '이메일 입력 화면', required: true },
|
||
{ name: '이메일 발송 완료 안내 화면', required: true },
|
||
{ name: '새 비밀번호 설정 화면', required: true },
|
||
{ name: '비밀번호 강도 표시', required: false },
|
||
{ name: '완료 + 로그인 유도 화면', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Google', 'Slack', 'SAM 비밀번호 찾기'],
|
||
guidelines: '각 단계 명확한 안내 텍스트. 이메일 발송 후 "메일함 열기" 버튼 제공. 비밀번호 규칙 사전 안내. 토큰 만료 시 재시도 안내.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '회원가입 폼', category: 'auth', rating: 4,
|
||
tags: ['회원가입', '가입', '온보딩', '폼'],
|
||
memo: 'Notion, Linear, Vercel 등. 최소 필드로 빠른 가입 후 프로필 완성은 나중에. 전환율이 핵심.',
|
||
components: [
|
||
{ name: '이름 입력란', required: true },
|
||
{ name: '이메일 입력란', required: true },
|
||
{ name: '비밀번호 입력란 (강도 표시)', required: true },
|
||
{ name: '가입 버튼 (풀 너비)', required: true },
|
||
{ name: '약관 동의 체크박스', required: true },
|
||
{ name: '소셜 가입 옵션', required: false },
|
||
{ name: '이미 계정이 있나요? 로그인 링크', required: true },
|
||
],
|
||
usedIn: ['Notion', 'Linear', 'Vercel', 'SAM 회원가입'],
|
||
guidelines: '필드 3~4개 이하 (이름, 이메일, 비밀번호). 비밀번호 강도 실시간 표시. 가입 버튼은 눈에 띄는 색상. 소셜 가입을 상단에 배치하면 전환율 향상.',
|
||
},
|
||
// ===== 보고서/인쇄 (report) =====
|
||
{
|
||
type: 'pattern', title: '인쇄용 보고서 레이아웃', category: 'report', rating: 4,
|
||
tags: ['인쇄', '보고서', 'A4', '프린트'],
|
||
memo: 'SAM, 전자세금계산서, 관공서 서식 등. A4 기준 인쇄 최적화. @media print 스타일 핵심.',
|
||
components: [
|
||
{ name: '회사 로고 + 문서 제목 (상단)', required: true },
|
||
{ name: '문서 번호 + 날짜 (우상단)', required: true },
|
||
{ name: '데이터 테이블 (테두리 인쇄)', required: true },
|
||
{ name: '합계/요약 영역 (하단)', required: true },
|
||
{ name: '서명란/도장 영역', required: false },
|
||
{ name: '페이지 번호 (푸터)', required: true },
|
||
{ name: '인쇄 버튼 (화면에서만 표시)', required: true },
|
||
],
|
||
usedIn: ['전자세금계산서', '관공서 서식', 'SAM 견적서/거래명세서'],
|
||
guidelines: 'A4 기준 210×297mm. @media print로 불필요 요소 숨기기. 테이블 테두리는 인쇄 시 선명하게. 페이지 나눔은 page-break-before/after.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '인보이스/견적서 문서', category: 'report', rating: 4,
|
||
tags: ['인보이스', '견적서', '문서', '금액'],
|
||
memo: 'Stripe Invoice, FreshBooks, SAM 견적서 등. 발행자/수신자 정보 + 품목 테이블 + 합계의 정형화된 구조.',
|
||
components: [
|
||
{ name: '발행자 정보 (좌상단: 로고+주소)', required: true },
|
||
{ name: '수신자 정보 (우상단: 업체명+주소)', required: true },
|
||
{ name: '문서 번호/날짜/유효기간', required: true },
|
||
{ name: '품목 테이블 (품명, 수량, 단가, 금액)', required: true },
|
||
{ name: '소계/세금/합계 (우하단)', required: true },
|
||
{ name: '비고/조건 (하단)', required: false },
|
||
{ name: '인감/서명란', required: false },
|
||
],
|
||
usedIn: ['Stripe Invoice', 'FreshBooks', 'Wave', 'SAM 견적서/거래명세서'],
|
||
guidelines: '금액은 우측 정렬 + 천단위 콤마. 합계는 볼드 + 크게. 품목 테이블은 줄무늬 배경(zebra striping). PDF 다운로드 버튼 제공.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '데이터 분석 리포트', category: 'report', rating: 4,
|
||
tags: ['분석', '리포트', '차트', 'KPI'],
|
||
memo: 'Google Analytics, Mixpanel, Amplitude 등. 다양한 차트와 데이터를 조합한 종합 분석 보고서.',
|
||
components: [
|
||
{ name: '기간 선택기 (상단)', required: true },
|
||
{ name: 'KPI 요약 카드 (핵심 지표 4~6개)', required: true },
|
||
{ name: '라인/바/파이 차트 (메인)', required: true },
|
||
{ name: '데이터 테이블 (상세)', required: true },
|
||
{ name: 'PDF/Excel 내보내기 버튼', required: true },
|
||
{ name: '비교 기간 토글 (전월/전년)', required: false },
|
||
],
|
||
usedIn: ['Google Analytics', 'Mixpanel', 'Amplitude', 'SAM 매출 분석'],
|
||
guidelines: '차트는 목적에 맞는 유형 선택 (추이→라인, 비교→바, 비율→파이). KPI 카드에 전기 대비 증감률 표시. 데이터 로딩 시 Skeleton UI.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '일일/주간 업무 보고서', category: 'report', rating: 3,
|
||
tags: ['일일보고', '주간보고', '업무보고', '보고서'],
|
||
memo: '사내 업무 보고서 양식. 완료/진행/예정 업무를 구조화하여 상사에게 보고하는 형식.',
|
||
components: [
|
||
{ name: '보고 기간/작성일/작성자', required: true },
|
||
{ name: '완료 업무 섹션 (체크리스트)', required: true },
|
||
{ name: '진행 중 업무 섹션 (진행률)', required: true },
|
||
{ name: '예정 업무 섹션', required: true },
|
||
{ name: '이슈/건의 사항', required: false },
|
||
{ name: '첨부파일', required: false },
|
||
],
|
||
usedIn: ['사내 보고 시스템', 'Notion 일일보고', 'SAM 일일 스크럼'],
|
||
guidelines: '완료 항목은 취소선 또는 체크 표시. 진행 중은 퍼센트 바. 중요 이슈는 빨간 강조. 인쇄 시 A4 1~2페이지 이내.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '대시보드 PDF 리포트', category: 'report', rating: 3,
|
||
tags: ['PDF', '대시보드', '리포트', '내보내기'],
|
||
memo: 'Stripe Report, HubSpot Report 등. 대시보드 데이터를 PDF로 내보내기 위한 인쇄 최적화 레이아웃.',
|
||
components: [
|
||
{ name: '리포트 표지 (로고+제목+기간)', required: true },
|
||
{ name: '요약 페이지 (핵심 KPI)', required: true },
|
||
{ name: '차트 페이지 (차트+설명)', required: true },
|
||
{ name: '상세 데이터 테이블', required: true },
|
||
{ name: '페이지 번호/날짜 (푸터)', required: true },
|
||
{ name: '목차 (3페이지 이상 시)', required: false },
|
||
],
|
||
usedIn: ['Stripe Report', 'HubSpot', 'Google Data Studio', 'SAM 월간 보고서'],
|
||
guidelines: 'A4 세로 또는 가로. 차트는 벡터(SVG) 또는 고해상도. 페이지 당 정보량 제한 (여백 충분히). 컬러 인쇄/흑백 인쇄 모두 고려.',
|
||
},
|
||
// ===== 대시보드 추가 =====
|
||
{
|
||
type: 'pattern', title: '위젯 대시보드 (커스터마이징)', category: 'dashboard', rating: 5,
|
||
tags: ['위젯', '대시보드', '드래그', '커스텀'],
|
||
memo: 'Notion Dashboard, Grafana, Datadog 등. 사용자가 위젯을 자유롭게 배치하고 크기를 조절하는 맞춤형 대시보드.',
|
||
components: [
|
||
{ name: '위젯 그리드 (드래그 이동)', required: true },
|
||
{ name: '위젯 리사이즈 (코너 드래그)', required: true },
|
||
{ name: '위젯 추가 버튼/패널', required: true },
|
||
{ name: '위젯 삭제/설정 (hover 메뉴)', required: true },
|
||
{ name: '레이아웃 저장/불러오기', required: true },
|
||
{ name: '위젯 유형: 차트, 숫자, 테이블, 목록', required: true },
|
||
],
|
||
usedIn: ['Grafana', 'Datadog', 'Notion', 'macOS 위젯'],
|
||
guidelines: '그리드 기반 배치 (12컬럼). 위젯 최소 크기 제한. 저장 시 레이아웃 JSON 직렬화. 기본 레이아웃 프리셋 제공.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '실시간 모니터링 대시보드', category: 'dashboard', rating: 4,
|
||
tags: ['실시간', '모니터링', '라이브', '상태'],
|
||
memo: 'Datadog, New Relic, AWS CloudWatch 등. 서버/시스템 상태를 실시간으로 모니터링하는 대시보드.',
|
||
components: [
|
||
{ name: '실시간 차트 (자동 업데이트)', required: true },
|
||
{ name: '상태 인디케이터 (정상/경고/장애)', required: true },
|
||
{ name: '알림 피드 (최근 이벤트)', required: true },
|
||
{ name: '서비스별 상태 카드', required: true },
|
||
{ name: '업타임 퍼센트 표시', required: false },
|
||
{ name: '자동 새로고침 간격 설정', required: false },
|
||
],
|
||
usedIn: ['Datadog', 'New Relic', 'AWS CloudWatch', 'SAM 서버 모니터링'],
|
||
guidelines: '정상=초록, 경고=노랑, 장애=빨강 (신호등 패턴). 차트 30초~1분 자동 갱신. 장애 시 화면 상단 빨간 배너. 소리 알림 선택적.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '멀티 차트 분석 대시보드', category: 'dashboard', rating: 4,
|
||
tags: ['차트', '분석', '시각화', '그래프'],
|
||
memo: 'Google Analytics, Tableau, Power BI 등. 다양한 차트 유형을 조합한 데이터 시각화 중심 대시보드.',
|
||
components: [
|
||
{ name: '라인 차트 (추이 분석)', required: true },
|
||
{ name: '바 차트 (비교 분석)', required: true },
|
||
{ name: '파이/도넛 차트 (비율 분석)', required: true },
|
||
{ name: '히트맵 (밀도 분석)', required: false },
|
||
{ name: '필터 바 (기간, 세그먼트)', required: true },
|
||
{ name: '차트 전환 옵션', required: false },
|
||
],
|
||
usedIn: ['Google Analytics', 'Tableau', 'Power BI', 'SAM 분석 대시보드'],
|
||
guidelines: '차트 간 일관된 색상 팔레트. 각 차트에 제목+범례. 데이터 포인트 hover 시 툴팁. 빈 데이터 시 안내 메시지.',
|
||
},
|
||
// ===== 목록 추가 =====
|
||
{
|
||
type: 'pattern', title: '무한 스크롤 피드', category: 'list', rating: 4,
|
||
tags: ['무한스크롤', '피드', '소셜', '스크롤'],
|
||
memo: 'Twitter/X, Instagram, LinkedIn 등 소셜 미디어. 스크롤하면 자동으로 다음 데이터를 로드하는 패턴.',
|
||
components: [
|
||
{ name: '피드 카드 (콘텐츠 + 메타정보)', required: true },
|
||
{ name: '자동 로딩 트리거 (스크롤 감지)', required: true },
|
||
{ name: '로딩 스피너/스켈레톤', required: true },
|
||
{ name: '새 글 알림 배너 ("새 글 N개")', required: false },
|
||
{ name: '맨 위로 가기 버튼', required: true },
|
||
{ name: '끝 도달 표시', required: true },
|
||
],
|
||
usedIn: ['Twitter/X', 'Instagram', 'LinkedIn', 'SAM 활동 로그'],
|
||
guidelines: 'IntersectionObserver로 스크롤 감지. 한 번에 20~30개 로드. 로딩 중 스켈레톤 3~4개 표시. 에러 시 재시도 버튼.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '그룹/섹션 목록', category: 'list', rating: 3,
|
||
tags: ['그룹', '섹션', '목록', '분류'],
|
||
memo: 'iOS 설정, macOS Finder, Notion 등. 항목을 논리적 그룹으로 묶어 표시하는 목록 패턴.',
|
||
components: [
|
||
{ name: '그룹 헤더 (섹션 제목 + 카운트)', required: true },
|
||
{ name: '그룹 내 항목 목록', required: true },
|
||
{ name: '접기/펼치기 토글', required: true },
|
||
{ name: 'Sticky 그룹 헤더 (스크롤 시 고정)', required: false },
|
||
{ name: '전체 접기/펼치기 버튼', required: false },
|
||
],
|
||
usedIn: ['iOS 설정', 'macOS Finder', 'Notion', 'SAM 품목 분류 목록'],
|
||
guidelines: '그룹 헤더는 배경색으로 구분. 접힌 상태에서도 항목 수 표시. 스크롤 시 현재 그룹 헤더 sticky. 초기 상태: 첫 그룹만 펼침.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '벌크 액션 바', category: 'list', rating: 4,
|
||
tags: ['벌크', '다중선택', '일괄처리', '액션바'],
|
||
memo: 'Gmail, GitHub, Jira 등. 여러 항목을 선택한 후 일괄 작업(삭제, 이동, 상태 변경)을 수행하는 패턴.',
|
||
components: [
|
||
{ name: '전체 선택 체크박스', required: true },
|
||
{ name: '선택 수 표시', required: true },
|
||
{ name: '벌크 액션 버튼 (삭제, 이동, 상태변경)', required: true },
|
||
{ name: '선택 해제 버튼', required: true },
|
||
{ name: '플로팅 액션 바 (하단 고정)', required: false },
|
||
{ name: '확인 다이얼로그 (위험 작업)', required: true },
|
||
],
|
||
usedIn: ['Gmail', 'GitHub', 'Jira', 'SAM 목록 화면'],
|
||
guidelines: '액션 바는 선택 시에만 표시 (애니메이션 슬라이드). 삭제 등 위험 작업은 확인 다이얼로그. 최대 선택 수 제한 안내.',
|
||
},
|
||
// ===== 상세/폼 추가 =====
|
||
{
|
||
type: 'pattern', title: '인라인 편집 테이블', category: 'form', rating: 4,
|
||
tags: ['인라인편집', '테이블', '스프레드시트', 'CRUD'],
|
||
memo: 'Airtable, Notion Table, Google Sheets 등. 행 클릭/더블클릭 시 셀을 직접 편집하는 스프레드시트형 패턴.',
|
||
components: [
|
||
{ name: '셀 클릭 → 편집 모드 전환', required: true },
|
||
{ name: '셀 유형별 에디터 (텍스트, 선택, 날짜)', required: true },
|
||
{ name: 'ESC 취소 / Enter 저장', required: true },
|
||
{ name: '행 추가 (하단 + 버튼)', required: true },
|
||
{ name: '행 삭제 (hover 메뉴)', required: true },
|
||
{ name: 'Tab 키로 다음 셀 이동', required: false },
|
||
],
|
||
usedIn: ['Airtable', 'Notion', 'Google Sheets', 'SAM BOM 테이블'],
|
||
guidelines: '편집 중인 셀은 파란 테두리 하이라이트. 변경 즉시 자동저장 (debounce 500ms). 셀 유형에 맞는 입력 UI (드롭다운, 날짜 피커).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '리치 텍스트 에디터', category: 'form', rating: 4,
|
||
tags: ['에디터', '리치텍스트', 'WYSIWYG', '마크다운'],
|
||
memo: 'Notion, Google Docs, Medium 등. 서식 있는 텍스트를 편집하는 WYSIWYG 에디터.',
|
||
components: [
|
||
{ name: '서식 도구 모음 (볼드, 기울임, 밑줄)', required: true },
|
||
{ name: '제목 레벨 (H1~H3)', required: true },
|
||
{ name: '목록 (순서/비순서)', required: true },
|
||
{ name: '이미지/파일 삽입', required: false },
|
||
{ name: '코드 블록', required: false },
|
||
{ name: '링크 삽입', required: true },
|
||
{ name: '플로팅 툴바 (선택 시 표시)', required: false },
|
||
],
|
||
usedIn: ['Notion', 'Google Docs', 'Medium', 'SAM 게시판 에디터'],
|
||
guidelines: '플로팅 툴바 (텍스트 선택 시 나타남) 또는 상단 고정 툴바. Markdown 단축키 지원 (**볼드**, # 제목). 이미지 드래그 앤 드롭.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '상세 정보 카드 (프로필)', category: 'form', rating: 3,
|
||
tags: ['상세', '프로필', '정보카드', '요약'],
|
||
memo: 'LinkedIn 프로필, GitHub 프로필, Salesforce Contact 등. 엔티티의 핵심 정보를 카드형으로 요약 표시.',
|
||
components: [
|
||
{ name: '프로필 이미지/아바타', required: true },
|
||
{ name: '이름 + 직함/역할', required: true },
|
||
{ name: '연락처 정보 (이메일, 전화)', required: true },
|
||
{ name: '액션 버튼 (편집, 메시지, 삭제)', required: true },
|
||
{ name: '태그/배지 (부서, 상태)', required: false },
|
||
{ name: '통계 (거래건수, 매출액 등)', required: false },
|
||
],
|
||
usedIn: ['LinkedIn', 'GitHub', 'Salesforce', 'SAM 거래처 상세'],
|
||
guidelines: '이미지 좌측 또는 상단 배치. 핵심 정보는 한눈에 파악 가능하게. 액션 버튼은 우상단. 통계는 숫자 + 라벨 조합.',
|
||
},
|
||
// ===== 모달/팝업 추가 =====
|
||
{
|
||
type: 'pattern', title: '확인/경고 다이얼로그', category: 'modal', rating: 4,
|
||
tags: ['확인', '경고', '다이얼로그', '삭제확인'],
|
||
memo: 'GitHub "Delete repository", Slack "Leave channel" 등. 위험한 작업 전 사용자에게 재확인을 요청하는 패턴.',
|
||
components: [
|
||
{ name: '경고 아이콘 (⚠️ 또는 빨간 방패)', required: true },
|
||
{ name: '제목 (명확한 액션 설명)', required: true },
|
||
{ name: '설명 텍스트 (영향 범위)', required: true },
|
||
{ name: '확인 입력 (리소스명 타이핑)', required: false },
|
||
{ name: '취소/확인 버튼 (확인은 빨간색)', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Slack', 'AWS', 'SAM 삭제 확인'],
|
||
guidelines: '삭제 버튼은 빨간색 (위험 인지). GitHub 패턴: 리소스명을 직접 타이핑해야 확인 가능. 취소가 기본 포커스 (실수 방지).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '이미지 라이트박스/갤러리', category: 'modal', rating: 3,
|
||
tags: ['라이트박스', '갤러리', '이미지', '확대'],
|
||
memo: 'Medium, Dribbble, Instagram 등. 이미지 클릭 시 전체 화면으로 확대하여 상세히 볼 수 있는 패턴.',
|
||
components: [
|
||
{ name: '오버레이 배경 (어둡게)', required: true },
|
||
{ name: '이미지 (최대 크기, 중앙)', required: true },
|
||
{ name: '좌우 네비게이션 화살표', required: true },
|
||
{ name: '닫기 버튼 (우상단 또는 ESC)', required: true },
|
||
{ name: '이미지 카운터 (3/12)', required: false },
|
||
{ name: '확대/축소 컨트롤', required: false },
|
||
{ name: '썸네일 스트립 (하단)', required: false },
|
||
],
|
||
usedIn: ['Medium', 'Dribbble', 'Instagram', 'SAM 첨부 이미지 뷰어'],
|
||
guidelines: '배경 클릭 또는 ESC로 닫기. 좌우 화살표 키 네비게이션. 이미지 로딩 시 placeholder. 모바일: 스와이프 네비게이션.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '알림 센터 (슬라이드 패널)', category: 'modal', rating: 4,
|
||
tags: ['알림', '패널', '슬라이드', '노티피케이션'],
|
||
memo: 'GitHub Notifications, Slack Activity, Linear Inbox 등. 화면 우측에서 슬라이드하여 알림 목록을 보여주는 패턴.',
|
||
components: [
|
||
{ name: '알림 아이콘 + 배지 (트리거)', required: true },
|
||
{ name: '슬라이드 패널 (우측)', required: true },
|
||
{ name: '알림 목록 (시간순)', required: true },
|
||
{ name: '읽음/안읽음 구분', required: true },
|
||
{ name: '모두 읽음 처리 버튼', required: true },
|
||
{ name: '알림 유형별 아이콘/색상', required: false },
|
||
{ name: '알림 클릭 → 해당 페이지 이동', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Slack', 'Linear', 'SAM 알림'],
|
||
guidelines: '패널 너비 360~400px. 안읽은 알림은 배경색 구분. 알림 그룹핑 (오늘/어제/이전). 빈 상태: "새 알림이 없습니다".',
|
||
},
|
||
// ===== 네비게이션 추가 =====
|
||
{
|
||
type: 'pattern', title: '메가 메뉴', category: 'navigation', rating: 3,
|
||
tags: ['메가메뉴', '드롭다운', '대형메뉴', 'GNB'],
|
||
memo: 'Amazon, Microsoft, Shopify 등 대규모 서비스. 메뉴 hover 시 전체 너비 드롭다운으로 하위 메뉴를 한눈에 표시.',
|
||
components: [
|
||
{ name: '상단 메뉴 바 (1차 메뉴)', required: true },
|
||
{ name: '메가 드롭다운 (전체 너비)', required: true },
|
||
{ name: '카테고리 컬럼 (2~4열)', required: true },
|
||
{ name: '아이콘/이미지 포함 항목', required: false },
|
||
{ name: '추천/하이라이트 영역', required: false },
|
||
],
|
||
usedIn: ['Amazon', 'Microsoft', 'Shopify', 'SAM 대형 메뉴 (검토용)'],
|
||
guidelines: '메뉴 진입 지연 200ms (실수 방지). 컬럼 간 명확한 구분. 현재 호버 메뉴 시각적 강조. 모바일에서는 아코디언으로 변환.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '모바일 하단 네비게이션', category: 'navigation', rating: 4,
|
||
tags: ['모바일', '하단바', '탭바', '네비게이션'],
|
||
memo: 'Instagram, Twitter/X, YouTube 등 모바일 앱. 엄지 손가락이 닿기 쉬운 하단에 핵심 메뉴 배치.',
|
||
components: [
|
||
{ name: '하단 고정 바', required: true },
|
||
{ name: '메뉴 아이콘 + 라벨 (4~5개)', required: true },
|
||
{ name: '활성 탭 하이라이트', required: true },
|
||
{ name: '알림 배지 (숫자)', required: false },
|
||
{ name: '중앙 액션 버튼 (FAB)', required: false },
|
||
],
|
||
usedIn: ['Instagram', 'Twitter/X', 'YouTube', 'SAM 모바일 뷰'],
|
||
guidelines: '항목 4~5개 권장 (초과 시 "더보기"). 아이콘 24px + 라벨 10px. 활성 탭은 색상 + 아이콘 변화. Safe area 하단 여백 (iOS 노치 대응).',
|
||
},
|
||
// ===== 기타 추가 =====
|
||
{
|
||
type: 'pattern', title: '드래그 앤 드롭 정렬', category: 'etc', rating: 4,
|
||
tags: ['드래그', '정렬', '순서변경', '드래그앤드롭'],
|
||
memo: 'Trello, Todoist, Notion, macOS Reminders 등. 마우스로 항목을 끌어서 순서를 변경하는 인터랙션.',
|
||
components: [
|
||
{ name: '드래그 핸들 (⠿ 아이콘)', required: true },
|
||
{ name: '드래그 미리보기 (반투명 복제)', required: true },
|
||
{ name: '드롭 위치 인디케이터 (파란 선)', required: true },
|
||
{ name: '드래그 중 원래 위치 표시', required: true },
|
||
{ name: '키보드 접근성 (Alt+↑↓)', required: false },
|
||
],
|
||
usedIn: ['Trello', 'Todoist', 'Notion', 'SAM 메뉴 순서, BOM 순서'],
|
||
guidelines: '드래그 시작 지연 150ms (클릭과 구분). 드래그 중 오토 스크롤 (목록 끝 근처). 드롭 후 애니메이션. 모바일: 길게 누르기.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '스켈레톤 로딩', category: 'etc', rating: 4,
|
||
tags: ['스켈레톤', '로딩', '플레이스홀더', 'UX'],
|
||
memo: 'Facebook, LinkedIn, YouTube, Notion 등. 데이터 로딩 중 콘텐츠 구조를 미리 보여주어 체감 속도를 높이는 패턴.',
|
||
components: [
|
||
{ name: '콘텐츠 형태의 회색 플레이스홀더', required: true },
|
||
{ name: '펄스/웨이브 애니메이션', required: true },
|
||
{ name: '실제 레이아웃과 동일한 구조', required: true },
|
||
{ name: '텍스트 줄 (다양한 너비)', required: true },
|
||
{ name: '이미지 영역 (회색 사각형)', required: true },
|
||
{ name: '아바타 (회색 원형)', required: false },
|
||
],
|
||
usedIn: ['Facebook', 'LinkedIn', 'YouTube', 'SAM 목록/상세 로딩'],
|
||
guidelines: '실제 UI와 동일한 구조/크기. 텍스트 줄은 60~80% 너비 랜덤. 애니메이션은 좌→우 웨이브. 로딩 1초 미만이면 스켈레톤 불필요.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '알림 배지 시스템', category: 'etc', rating: 3,
|
||
tags: ['배지', '알림', '카운터', '인디케이터'],
|
||
memo: 'Gmail, Slack, iOS 앱 아이콘 등. 읽지 않은 항목 수나 새 알림을 숫자/점으로 표시하는 패턴.',
|
||
components: [
|
||
{ name: '숫자 배지 (빨간 원 + 숫자)', required: true },
|
||
{ name: '점 배지 (숫자 없이 존재만 표시)', required: true },
|
||
{ name: '아이콘 위 배치 (우상단)', required: true },
|
||
{ name: '99+ 처리 (큰 숫자)', required: true },
|
||
{ name: '애니메이션 (새 알림 시 바운스)', required: false },
|
||
],
|
||
usedIn: ['Gmail', 'Slack', 'iOS', 'SAM 사이드바 메뉴'],
|
||
guidelines: '배지 최소 크기 18px. 숫자 1자리: 원형, 2자리 이상: pill 형태. 99+로 표기 (3자리 이상). 색상: 빨강(긴급), 파랑(정보), 회색(비활성).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '데이터 시각화 차트 컴포넌트', category: 'dashboard', rating: 4,
|
||
tags: ['차트', '그래프', '시각화', '데이터'],
|
||
memo: 'Chart.js, D3.js, Recharts 등 차트 라이브러리를 활용한 데이터 시각화 컴포넌트 패턴.',
|
||
components: [
|
||
{ name: '차트 제목 + 범례', required: true },
|
||
{ name: '차트 영역 (라인/바/파이/도넛)', required: true },
|
||
{ name: '툴팁 (데이터 포인트 hover)', required: true },
|
||
{ name: '축 라벨 (X/Y축)', required: true },
|
||
{ name: '기간 선택 토글', required: false },
|
||
{ name: '데이터 없음 표시', required: true },
|
||
],
|
||
usedIn: ['Stripe', 'Vercel', 'GitHub Insights', 'SAM 차트 컴포넌트'],
|
||
guidelines: '색상 6~8개 팔레트 고정. 접근성: 색맹 대응 패턴 구분. 반응형: 작은 화면에서 범례 하단 이동. 로딩 시 차트 영역 스켈레톤.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '다단계 드롭다운 메뉴', category: 'navigation', rating: 3,
|
||
tags: ['드롭다운', '계층', '메뉴', '셀렉트'],
|
||
memo: 'macOS 메뉴, VS Code 우클릭, Figma 메뉴 등. 메뉴 항목에 서브메뉴가 있는 계층형 드롭다운.',
|
||
components: [
|
||
{ name: '1단계 메뉴 (클릭/hover 트리거)', required: true },
|
||
{ name: '2단계 서브메뉴 (우측 확장)', required: true },
|
||
{ name: '구분선 (그룹 분리)', required: true },
|
||
{ name: '단축키 표시 (우측)', required: false },
|
||
{ name: '체크 마크 (선택 상태)', required: false },
|
||
{ name: '비활성 항목 (회색)', required: true },
|
||
],
|
||
usedIn: ['macOS', 'VS Code', 'Figma', 'SAM 우클릭 메뉴'],
|
||
guidelines: '서브메뉴 전환 지연 200ms. 현재 활성 항목 배경색 하이라이트. 메뉴 외부 클릭 시 닫기. 키보드: 화살표 키 네비게이션.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '날짜/기간 선택기 (Date Picker)', category: 'modal', rating: 4,
|
||
tags: ['날짜', '기간', '달력', '피커'],
|
||
memo: 'Airbnb, Booking.com, Google Flights 등. 단일 날짜 또는 시작~종료 기간을 선택하는 UI.',
|
||
components: [
|
||
{ name: '달력 그리드 (월간)', required: true },
|
||
{ name: '이전/다음 월 네비게이션', required: true },
|
||
{ name: '기간 선택 (시작~종료 하이라이트)', required: false },
|
||
{ name: '오늘 버튼 (빠른 이동)', required: true },
|
||
{ name: '프리셋 (오늘, 이번 주, 이번 달)', required: false },
|
||
{ name: '시간 선택 (선택적)', required: false },
|
||
],
|
||
usedIn: ['Airbnb', 'Booking.com', 'Google Flights', 'SAM 기간 필터'],
|
||
guidelines: '오늘 날짜 강조. 선택 불가 날짜 회색 처리. 기간 선택 시 범위 배경색. 모바일: 전체 화면 달력. 키보드: 화살표 키 이동.',
|
||
},
|
||
// ===== 추가 50종 (51~100) =====
|
||
// ===== 대시보드 추가 (5) =====
|
||
{
|
||
type: 'pattern', title: '게이지/미터 대시보드', category: 'dashboard', rating: 4,
|
||
tags: ['게이지', '미터', '진행률', '속도계'],
|
||
memo: 'Grafana, Datadog, 자동차 계기판 UI. 원형 게이지로 목표 달성률이나 시스템 사용률을 직관적으로 표시.',
|
||
components: [
|
||
{ name: '원형 게이지 (반원 또는 전원)', required: true },
|
||
{ name: '현재 값 (중앙 큰 텍스트)', required: true },
|
||
{ name: '목표/한계 값 표시', required: true },
|
||
{ name: '색상 구간 (초록/노랑/빨강)', required: true },
|
||
{ name: '라벨 (측정 항목명)', required: true },
|
||
],
|
||
usedIn: ['Grafana', 'Datadog', 'Google PageSpeed', 'SAM 생산 달성률'],
|
||
guidelines: '0~100% 범위. 초록(0~60%), 노랑(60~80%), 빨강(80~100%). 애니메이션 전환. 숫자는 게이지 중앙 크게 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '히트맵 캘린더 (활동 기록)', category: 'dashboard', rating: 4,
|
||
tags: ['히트맵', '캘린더', '활동', '잔디'],
|
||
memo: 'GitHub Contribution, Wakatime 등. 일별 활동량을 색상 농도로 표현하여 장기 패턴을 한눈에 파악.',
|
||
components: [
|
||
{ name: '일별 셀 그리드 (52주 × 7일)', required: true },
|
||
{ name: '색상 농도 스케일 (연→진)', required: true },
|
||
{ name: '요일 라벨 (좌측)', required: true },
|
||
{ name: '월 라벨 (상단)', required: true },
|
||
{ name: '셀 hover 시 상세 툴팁', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Wakatime', 'Habitica', 'SAM 출근 기록'],
|
||
guidelines: '5단계 색상 (없음/연/중/진/최대). 셀 크기 10~12px. 오늘 날짜 테두리 강조. 빈 날짜는 가장 연한 색.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '퍼널/전환율 차트', category: 'dashboard', rating: 4,
|
||
tags: ['퍼널', '전환율', '세일즈', '단계'],
|
||
memo: 'HubSpot, Salesforce, Mixpanel 등. 단계별 전환율을 시각화하여 이탈 구간을 파악하는 세일즈/마케팅 핵심 차트.',
|
||
components: [
|
||
{ name: '단계별 사다리꼴 바', required: true },
|
||
{ name: '각 단계 수치 + 전환율 (%)', required: true },
|
||
{ name: '단계 간 이탈률 표시', required: true },
|
||
{ name: '단계 라벨', required: true },
|
||
{ name: '기간 필터', required: false },
|
||
],
|
||
usedIn: ['HubSpot', 'Salesforce', 'Mixpanel', 'SAM 수주 파이프라인'],
|
||
guidelines: '최상단이 가장 넓고 아래로 좁아지는 형태. 전환율은 단계 사이에 표시. 이탈률이 큰 구간 빨간 강조.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '비교 대시보드 (전월 vs 당월)', category: 'dashboard', rating: 3,
|
||
tags: ['비교', '전월', '당월', '증감'],
|
||
memo: 'Google Analytics 비교 뷰, Stripe 비교 등. 두 기간의 데이터를 나란히 비교하여 성과 변화를 분석.',
|
||
components: [
|
||
{ name: '기간 A / 기간 B 선택기', required: true },
|
||
{ name: '병렬 KPI 카드 (A vs B)', required: true },
|
||
{ name: '증감률 화살표 + 색상', required: true },
|
||
{ name: '겹친 라인 차트 (비교)', required: true },
|
||
{ name: '차이 요약 테이블', required: false },
|
||
],
|
||
usedIn: ['Google Analytics', 'Stripe', 'Shopify', 'SAM 월간 비교'],
|
||
guidelines: '기간 A는 실선, B는 점선. 증가=초록, 감소=빨강. 카드에서 양쪽 숫자 나란히 표시. 차이 절대값 + 퍼센트 동시 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '지도 기반 데이터 시각화', category: 'dashboard', rating: 3,
|
||
tags: ['지도', '위치', '지리', '분포'],
|
||
memo: 'Google Maps, Mapbox, Tableau 지도 등. 지역별 데이터 분포를 지도 위에 시각화.',
|
||
components: [
|
||
{ name: '지도 배경 (벡터/래스터)', required: true },
|
||
{ name: '데이터 마커/핀', required: true },
|
||
{ name: '클러스터링 (밀집 지역)', required: false },
|
||
{ name: '범례 (색상/크기 의미)', required: true },
|
||
{ name: '지역 hover 상세 정보', required: true },
|
||
],
|
||
usedIn: ['Google Maps', 'Tableau', 'Uber', 'SAM 거래처 분포'],
|
||
guidelines: '마커는 데이터 크기에 비례. 밀집 시 클러스터 자동 그룹핑. 지역 클릭 시 드릴다운. 줌 레벨별 상세도 조절.',
|
||
},
|
||
// ===== 목록 추가 (5) =====
|
||
{
|
||
type: 'pattern', title: '마스터-디테일 레이아웃', category: 'list', rating: 5,
|
||
tags: ['마스터', '디테일', '분할', '목록상세'],
|
||
memo: 'Apple Mail, Outlook, Notion 등. 좌측 목록 + 우측 상세의 2패널 구조. ERP에서 가장 빈번히 사용.',
|
||
components: [
|
||
{ name: '좌측 목록 패널 (검색 + 스크롤)', required: true },
|
||
{ name: '우측 상세 패널', required: true },
|
||
{ name: '목록 항목 선택 하이라이트', required: true },
|
||
{ name: '리사이즈 핸들 (패널 너비 조절)', required: false },
|
||
{ name: '상세 패널 탭 (정보/이력/첨부)', required: false },
|
||
{ name: '빈 상태 (선택 안 됨)', required: true },
|
||
],
|
||
usedIn: ['Apple Mail', 'Outlook', 'Notion', 'SAM 수주/거래처 상세'],
|
||
guidelines: '좌측 300~400px 고정, 우측 flex:1. 목록에서 현재 선택은 파란 배경. 모바일: 스택 레이아웃 (목록→상세 전환).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '타일/썸네일 뷰', category: 'list', rating: 3,
|
||
tags: ['타일', '썸네일', '갤러리', '그리드뷰'],
|
||
memo: 'Pinterest, Dribbble, macOS Finder 등. 이미지 중심 데이터를 그리드 타일로 표시하는 시각적 목록.',
|
||
components: [
|
||
{ name: '이미지 썸네일 (메인)', required: true },
|
||
{ name: '타이틀 + 메타정보', required: true },
|
||
{ name: 'Masonry/균등 그리드 레이아웃', required: true },
|
||
{ name: '뷰 모드 전환 (타일/목록)', required: true },
|
||
{ name: '정렬 옵션', required: false },
|
||
],
|
||
usedIn: ['Pinterest', 'Dribbble', 'macOS Finder', 'SAM 도면/이미지 관리'],
|
||
guidelines: '타일 최소 200px, 최대 4열. 이미지 비율 유지 (object-fit: cover). hover 시 오버레이 액션. lazy loading 적용.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '피벗 테이블', category: 'list', rating: 4,
|
||
tags: ['피벗', '크로스탭', '집계', '엑셀'],
|
||
memo: 'Excel 피벗, Google Sheets, Tableau 등. 행/열 교차 집계로 다차원 데이터를 분석하는 테이블.',
|
||
components: [
|
||
{ name: '행 헤더 (그룹핑 기준)', required: true },
|
||
{ name: '열 헤더 (분석 차원)', required: true },
|
||
{ name: '데이터 셀 (집계 값)', required: true },
|
||
{ name: '행/열 합계', required: true },
|
||
{ name: '필드 드래그 앤 드롭 (행↔열)', required: false },
|
||
{ name: '집계 함수 선택 (합계/평균/카운트)', required: false },
|
||
],
|
||
usedIn: ['Excel', 'Google Sheets', 'Tableau', 'SAM 매출 분석'],
|
||
guidelines: '행 그룹 접기/펼치기. 합계 행은 볼드 + 배경색. 금액은 천단위 콤마 우측 정렬. 큰 데이터는 가상 스크롤.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '수평 타임라인', category: 'list', rating: 3,
|
||
tags: ['타임라인', '수평', '이력', '연대기'],
|
||
memo: 'LinkedIn Experience, 프로젝트 로드맵, 제품 릴리즈 히스토리 등. 시간순 이벤트를 가로로 나열.',
|
||
components: [
|
||
{ name: '수평 축 (시간선)', required: true },
|
||
{ name: '이벤트 노드 (점/아이콘)', required: true },
|
||
{ name: '이벤트 카드 (상/하 교대 배치)', required: true },
|
||
{ name: '스크롤/드래그 네비게이션', required: true },
|
||
{ name: '현재 시점 표시', required: false },
|
||
],
|
||
usedIn: ['LinkedIn', 'Jira Roadmap', 'Product Hunt', 'SAM 프로젝트 이력'],
|
||
guidelines: '카드를 상/하 교대로 배치하여 가독성 확보. 현재 시점 빨간 선. 이벤트 노드 클릭 시 상세 팝업. 좌우 스크롤 또는 드래그.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '트리 테이블 (계층형 데이터)', category: 'list', rating: 4,
|
||
tags: ['트리', '계층', '테이블', '접기펼치기'],
|
||
memo: 'Jira 이슈 트리, BOM 구조, 파일 탐색기 등. 테이블 행 안에서 부모-자식 관계를 트리로 표현.',
|
||
components: [
|
||
{ name: '접기/펼치기 화살표 (행 좌측)', required: true },
|
||
{ name: '들여쓰기 레벨 (depth 표현)', required: true },
|
||
{ name: '테이블 헤더 (일반 컬럼)', required: true },
|
||
{ name: '부모 행 볼드/배경색 구분', required: false },
|
||
{ name: '전체 펼치기/접기 버튼', required: true },
|
||
],
|
||
usedIn: ['Jira', 'SAM BOM 트리', 'Windows Explorer', 'macOS Finder'],
|
||
guidelines: '레벨당 들여쓰기 16~20px. 리프 노드는 화살표 없음. 접힌 상태에서 자식 수 표시. Shift+클릭으로 재귀 펼치기.',
|
||
},
|
||
// ===== 폼 추가 (8) =====
|
||
{
|
||
type: 'pattern', title: '멀티 스텝 폼 (위자드)', category: 'form', rating: 5,
|
||
tags: ['멀티스텝', '위자드', '단계', '폼'],
|
||
memo: 'Typeform, Stripe Checkout, HubSpot 등. 복잡한 폼을 여러 단계로 나눠 사용자 부담을 줄이는 패턴.',
|
||
components: [
|
||
{ name: '진행 바/스텝 인디케이터', required: true },
|
||
{ name: '현재 단계 폼 필드', required: true },
|
||
{ name: '이전/다음 버튼', required: true },
|
||
{ name: '단계별 유효성 검사', required: true },
|
||
{ name: '요약/확인 단계 (마지막)', required: true },
|
||
{ name: '임시 저장 (중간 이탈 방지)', required: false },
|
||
],
|
||
usedIn: ['Typeform', 'Stripe Checkout', 'HubSpot', 'SAM 견적 등록'],
|
||
guidelines: '3~5단계 권장. 각 단계 필드 3~5개 이내. 완료 단계에 확인 아이콘. 진행 바에 퍼센트 또는 단계 번호 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '주소 검색/입력 폼', category: 'form', rating: 3,
|
||
tags: ['주소', '검색', '우편번호', 'API'],
|
||
memo: '다음 주소 API, Google Places, 네이버 지도 등. 우편번호 검색 후 상세주소를 입력하는 한국형 주소 입력.',
|
||
components: [
|
||
{ name: '우편번호 검색 버튼', required: true },
|
||
{ name: '주소 검색 모달/팝업', required: true },
|
||
{ name: '기본 주소 (자동 채워짐)', required: true },
|
||
{ name: '상세 주소 입력란', required: true },
|
||
{ name: '검색 결과 목록', required: true },
|
||
],
|
||
usedIn: ['카카오 주소 API', '네이버 주소', 'SAM 거래처 주소'],
|
||
guidelines: '검색창에 동/도로명/건물명 입력. 결과 클릭 시 기본주소 자동 채움 + 상세주소 포커스. 모바일: 전체 화면 검색.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '태그/칩 입력', category: 'form', rating: 4,
|
||
tags: ['태그', '칩', '다중선택', '입력'],
|
||
memo: 'Gmail 수신자, Notion 태그, Jira 라벨 등. 여러 항목을 태그 형태로 추가/삭제하는 입력 UI.',
|
||
components: [
|
||
{ name: '입력 필드 (텍스트 입력)', required: true },
|
||
{ name: '태그 칩 (X 삭제 버튼)', required: true },
|
||
{ name: '자동완성 드롭다운', required: true },
|
||
{ name: 'Enter/콤마로 추가', required: true },
|
||
{ name: 'Backspace로 마지막 태그 삭제', required: true },
|
||
{ name: '드래그로 순서 변경', required: false },
|
||
],
|
||
usedIn: ['Gmail', 'Notion', 'Jira', 'SAM 품목 태그'],
|
||
guidelines: '태그 칩 높이 24px, pill 형태. 입력 필드와 태그가 같은 줄에 배치. 중복 태그 방지 (이미 있으면 깜빡임). 최대 개수 제한 안내.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '슬라이더/레인지 입력', category: 'form', rating: 3,
|
||
tags: ['슬라이더', '레인지', '범위', '조절'],
|
||
memo: 'Airbnb 가격 필터, YouTube 볼륨, Figma 투명도 등. 드래그로 수치를 조절하는 입력 컨트롤.',
|
||
components: [
|
||
{ name: '트랙 바 (가로 선)', required: true },
|
||
{ name: '핸들/썸 (드래그 포인트)', required: true },
|
||
{ name: '현재 값 표시 (핸들 위 또는 옆)', required: true },
|
||
{ name: '최소/최대 라벨', required: true },
|
||
{ name: '범위 선택 (핸들 2개)', required: false },
|
||
{ name: '수치 직접 입력 (옆 필드)', required: false },
|
||
],
|
||
usedIn: ['Airbnb', 'YouTube', 'Figma', 'SAM 가격 필터'],
|
||
guidelines: '핸들 크기 최소 20px (터치 영역 44px). 드래그 중 값 실시간 업데이트. 스텝 단위 설정 가능. 키보드: 좌우 화살표로 미세 조정.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '토글 설정 패널', category: 'form', rating: 3,
|
||
tags: ['토글', '스위치', '설정', '온오프'],
|
||
memo: 'iOS 설정, Android 설정, Notion 설정 등. 각 옵션을 토글 스위치로 켜고 끄는 설정 패널.',
|
||
components: [
|
||
{ name: '토글 스위치 (ON/OFF)', required: true },
|
||
{ name: '옵션 라벨 + 설명', required: true },
|
||
{ name: '섹션 그룹핑', required: true },
|
||
{ name: '위험한 설정 빨간 강조', required: false },
|
||
{ name: '변경 시 즉시 저장 / 저장 버튼', required: true },
|
||
],
|
||
usedIn: ['iOS 설정', 'Android 설정', 'Notion', 'SAM 알림 설정'],
|
||
guidelines: '토글 너비 44px, 높이 24px. ON=파랑/초록, OFF=회색. 라벨 좌측, 토글 우측 정렬. 변경 사항 즉시 저장 또는 하단 저장 버튼.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '서명 패드 (전자 서명)', category: 'form', rating: 3,
|
||
tags: ['서명', '전자서명', '캔버스', '필기'],
|
||
memo: 'DocuSign, Adobe Sign, SAM 전자결재 등. 마우스/터치로 직접 서명을 작성하는 UI.',
|
||
components: [
|
||
{ name: '캔버스 영역 (서명 작성)', required: true },
|
||
{ name: '지우기 버튼', required: true },
|
||
{ name: '펜 굵기/색상 옵션', required: false },
|
||
{ name: '서명 완료/확인 버튼', required: true },
|
||
{ name: '서명 이미지 미리보기', required: false },
|
||
{ name: '텍스트 서명 옵션 (타이핑)', required: false },
|
||
],
|
||
usedIn: ['DocuSign', 'Adobe Sign', 'HelloSign', 'SAM 전자결재 서명'],
|
||
guidelines: '캔버스 배경 연한 줄무늬. 펜 압력 감지 (가능 시). 서명 이미지 PNG/SVG로 저장. 모바일: 가로 모드 권장.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '컬러 피커', category: 'form', rating: 3,
|
||
tags: ['컬러', '색상', '피커', '팔레트'],
|
||
memo: 'Figma, Photoshop, Notion 등 디자인 도구. 색상을 시각적으로 선택하는 UI 컴포넌트.',
|
||
components: [
|
||
{ name: '색상 영역 (Saturation/Brightness)', required: true },
|
||
{ name: '색상 바 (Hue 슬라이더)', required: true },
|
||
{ name: '투명도 슬라이더 (Alpha)', required: false },
|
||
{ name: 'HEX/RGB/HSL 입력', required: true },
|
||
{ name: '최근 사용 색상', required: false },
|
||
{ name: '프리셋 팔레트', required: false },
|
||
],
|
||
usedIn: ['Figma', 'Photoshop', 'VS Code', 'SAM 테마 설정'],
|
||
guidelines: '미리보기 원형 또는 사각형. HEX 6자리 입력. 스포이드 도구 (브라우저 지원 시). 자주 사용 색상 저장.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '평점/별점 입력', category: 'form', rating: 3,
|
||
tags: ['별점', '평점', '리뷰', '평가'],
|
||
memo: 'Amazon, Google Maps, App Store 등. 별 아이콘을 클릭하여 점수를 매기는 UI.',
|
||
components: [
|
||
{ name: '별 아이콘 5개 (클릭 가능)', required: true },
|
||
{ name: 'hover 시 미리보기', required: true },
|
||
{ name: '반별 (0.5점 단위) 지원', required: false },
|
||
{ name: '숫자 표시 (4.5/5.0)', required: true },
|
||
{ name: '리뷰 텍스트 영역', required: false },
|
||
],
|
||
usedIn: ['Amazon', 'Google Maps', 'App Store', 'SAM 공급업체 평가'],
|
||
guidelines: '별 크기 24~32px. 채워진 별=노랑, 빈 별=회색. hover 시 해당 별까지 채워짐. 읽기 전용 모드: 평균 + 리뷰 수.',
|
||
},
|
||
// ===== 모달/팝업 추가 (5) =====
|
||
{
|
||
type: 'pattern', title: '바텀 시트 (모바일)', category: 'modal', rating: 4,
|
||
tags: ['바텀시트', '모바일', '시트', '슬라이드업'],
|
||
memo: 'Google Maps, Apple Maps, Uber 등 모바일 앱. 화면 하단에서 올라오는 시트형 모달.',
|
||
components: [
|
||
{ name: '드래그 핸들 (상단 바)', required: true },
|
||
{ name: '시트 콘텐츠', required: true },
|
||
{ name: '반쯤 열린 / 완전히 열린 상태', required: true },
|
||
{ name: '배경 딤 (오버레이)', required: true },
|
||
{ name: '아래로 스와이프 닫기', required: true },
|
||
],
|
||
usedIn: ['Google Maps', 'Apple Maps', 'Uber', 'SAM 모바일 필터'],
|
||
guidelines: '핸들 바 너비 40px, 높이 4px, 중앙 배치. 3단계 높이 (peek/half/full). 스와이프 속도에 따라 닫기 판단. 배경 탭 시 닫기.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '컨텍스트 메뉴 (우클릭)', category: 'modal', rating: 3,
|
||
tags: ['컨텍스트', '우클릭', '메뉴', '팝업'],
|
||
memo: 'VS Code, Figma, macOS 등. 마우스 우클릭 위치에 나타나는 상황별 액션 메뉴.',
|
||
components: [
|
||
{ name: '메뉴 항목 목록', required: true },
|
||
{ name: '키보드 단축키 표시', required: true },
|
||
{ name: '구분선 (그룹 분리)', required: true },
|
||
{ name: '아이콘 (항목 좌측)', required: false },
|
||
{ name: '비활성 항목 (회색)', required: true },
|
||
{ name: '서브메뉴 (우측 화살표)', required: false },
|
||
],
|
||
usedIn: ['VS Code', 'Figma', 'macOS', 'SAM 테이블 우클릭'],
|
||
guidelines: '마우스 위치에 생성 (화면 밖으로 나가지 않게 조정). 외부 클릭 시 닫기. 메뉴 항목 높이 28~32px. 위험 항목 빨간색.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '슬라이드오버 상세 패널', category: 'modal', rating: 4,
|
||
tags: ['슬라이드오버', '패널', '사이드', '상세'],
|
||
memo: 'AWS Console, Jira 상세, Salesforce Record 등. 우측에서 슬라이드하여 상세 정보를 보여주는 패널.',
|
||
components: [
|
||
{ name: '우측 슬라이드 패널 (320~480px)', required: true },
|
||
{ name: '패널 헤더 (제목 + 닫기)', required: true },
|
||
{ name: '콘텐츠 영역 (스크롤)', required: true },
|
||
{ name: '배경 딤 (반투명)', required: true },
|
||
{ name: '하단 액션 버튼 (고정)', required: false },
|
||
],
|
||
usedIn: ['AWS Console', 'Jira', 'Salesforce', 'SAM 수주 상세'],
|
||
guidelines: '패널 너비 400~500px. 뒤 페이지는 어둡게 + 클릭 가능(닫기). 슬라이드 애니메이션 200~300ms. ESC로 닫기.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '쿠키/개인정보 동의 배너', category: 'modal', rating: 3,
|
||
tags: ['쿠키', '동의', 'GDPR', '배너'],
|
||
memo: 'EU GDPR 대응. 거의 모든 웹사이트에 필수. 쿠키 사용 동의를 받는 하단 배너 또는 모달.',
|
||
components: [
|
||
{ name: '안내 텍스트 (쿠키 사용 목적)', required: true },
|
||
{ name: '동의/거부 버튼', required: true },
|
||
{ name: '상세 설정 링크', required: true },
|
||
{ name: '개인정보처리방침 링크', required: true },
|
||
{ name: '카테고리별 토글 (필수/분석/마케팅)', required: false },
|
||
],
|
||
usedIn: ['EU 웹사이트 전체', 'Vercel', 'Notion', 'SAM (향후 적용)'],
|
||
guidelines: '하단 고정 또는 중앙 모달. "모두 동의" 버튼 강조. 필수 쿠키는 비활성화 불가 (회색 토글). 닫기 버튼 없이 선택 강제.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '풀스크린 모달 (이머시브)', category: 'modal', rating: 3,
|
||
tags: ['풀스크린', '전체화면', '모달', '이머시브'],
|
||
memo: 'Medium 글쓰기, Notion 전체화면, Google Docs 등. 전체 화면을 차지하는 몰입형 모달.',
|
||
components: [
|
||
{ name: '전체 화면 오버레이', required: true },
|
||
{ name: '상단 툴바 (닫기 + 액션)', required: true },
|
||
{ name: '중앙 콘텐츠 영역 (최대 너비 제한)', required: true },
|
||
{ name: '닫기 버튼 (좌상단 또는 우상단)', required: true },
|
||
{ name: '저장/발행 버튼', required: false },
|
||
],
|
||
usedIn: ['Medium', 'Notion', 'Google Docs', 'SAM 문서 편집'],
|
||
guidelines: '배경 완전 흰색 또는 단색. 콘텐츠 최대 너비 720~800px (가독성). ESC로 닫기 + 저장 안 됨 확인. 애니메이션: fade-in.',
|
||
},
|
||
// ===== 네비게이션 추가 (5) =====
|
||
{
|
||
type: 'pattern', title: '탑 네비게이션 바 + 검색', category: 'navigation', rating: 5,
|
||
tags: ['탑바', 'GNB', '검색', '헤더'],
|
||
memo: 'GitHub, Notion, AWS Console 등. 상단 고정 네비게이션 바. 로고 + 메뉴 + 검색 + 프로필의 표준 구조.',
|
||
components: [
|
||
{ name: '로고 (좌측)', required: true },
|
||
{ name: '메뉴 항목 (중앙 또는 좌측)', required: true },
|
||
{ name: '검색 바 (중앙)', required: true },
|
||
{ name: '알림 아이콘 + 배지', required: false },
|
||
{ name: '프로필 아바타 + 드롭다운', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Notion', 'AWS', 'SAM 상단 헤더'],
|
||
guidelines: '높이 48~64px. position: sticky. 스크롤 시 그림자 추가. 검색: Ctrl+K 단축키. 프로필 클릭 시 드롭다운 메뉴.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '앵커/스크롤 스파이 네비게이션', category: 'navigation', rating: 3,
|
||
tags: ['앵커', '스크롤스파이', '목차', '사이드'],
|
||
memo: 'Bootstrap Docs, MDN, Tailwind Docs 등. 긴 문서에서 현재 섹션을 추적하는 사이드 목차.',
|
||
components: [
|
||
{ name: '섹션 링크 목록 (사이드)', required: true },
|
||
{ name: '현재 섹션 하이라이트', required: true },
|
||
{ name: '스크롤 연동 (자동 추적)', required: true },
|
||
{ name: '클릭 시 스무스 스크롤', required: true },
|
||
{ name: '들여쓰기 (서브섹션)', required: false },
|
||
],
|
||
usedIn: ['Bootstrap Docs', 'MDN', 'Tailwind CSS', 'SAM 가이드 페이지'],
|
||
guidelines: '사이드 고정 (position: sticky). IntersectionObserver로 현재 섹션 감지. 현재 섹션 파란색 + 좌측 바. 모바일: 상단 드롭다운.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '페이지네이션 패턴', category: 'navigation', rating: 4,
|
||
tags: ['페이지네이션', '페이지', '이동', '넘기기'],
|
||
memo: 'Google 검색, GitHub Issues 등. 대량 데이터를 페이지 단위로 나누어 탐색하는 UI.',
|
||
components: [
|
||
{ name: '이전/다음 버튼', required: true },
|
||
{ name: '페이지 번호 (1, 2, 3...)', required: true },
|
||
{ name: '말줄임 (1 ... 5 6 7 ... 20)', required: true },
|
||
{ name: '첫 페이지/마지막 페이지', required: true },
|
||
{ name: '페이지당 표시 수 선택', required: false },
|
||
{ name: '총 결과 수 표시', required: true },
|
||
],
|
||
usedIn: ['Google 검색', 'GitHub', 'AWS Console', 'SAM 목록 페이지'],
|
||
guidelines: '현재 페이지 파란 배경. 표시 페이지 5~7개. 양 끝에 말줄임(...). 첫/마지막 페이지는 항상 표시. 모바일: 이전/다음만.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '플로팅 액션 버튼 (FAB)', category: 'navigation', rating: 3,
|
||
tags: ['FAB', '플로팅', '액션', '버튼'],
|
||
memo: 'Material Design, Gmail, Google Docs 등. 화면 하단에 떠있는 주요 액션 버튼.',
|
||
components: [
|
||
{ name: '원형 버튼 (고정 위치)', required: true },
|
||
{ name: '아이콘 (+ 기호)', required: true },
|
||
{ name: '확장 메뉴 (클릭 시 다중 액션)', required: false },
|
||
{ name: '스크롤 시 축소/숨기기', required: false },
|
||
{ name: '툴팁 라벨', required: false },
|
||
],
|
||
usedIn: ['Gmail', 'Google Docs', 'Material Design', 'SAM 모바일 빠른 등록'],
|
||
guidelines: '우하단 고정 (right: 24px, bottom: 24px). 크기 56px (mini: 40px). 그림자 필수. 확장 시 위로 액션 아이콘 나열.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '사이드 탭 (세로 탭)', category: 'navigation', rating: 3,
|
||
tags: ['세로탭', '사이드탭', '수직탭', '패널'],
|
||
memo: 'VS Code 사이드바, Slack 좌측, Notion 좌측 등. 좌측에 세로로 배치된 탭으로 콘텐츠를 전환.',
|
||
components: [
|
||
{ name: '세로 탭 목록 (좌측)', required: true },
|
||
{ name: '활성 탭 하이라이트', required: true },
|
||
{ name: '아이콘 + 라벨 (선택적)', required: true },
|
||
{ name: '콘텐츠 패널 (우측)', required: true },
|
||
{ name: '아이콘만 모드 (축소)', required: false },
|
||
],
|
||
usedIn: ['VS Code', 'Slack', 'Notion', 'SAM 설정 페이지'],
|
||
guidelines: '탭 너비: 확장 200px, 축소 48px. 활성 탭: 좌측 파란 바 + 배경색. 아이콘: 24px, 라벨: 12~14px. 키보드: ↑↓ 탭 전환.',
|
||
},
|
||
// ===== 인증 추가 (3) =====
|
||
{
|
||
type: 'pattern', title: '역할/권한 관리 (RBAC)', category: 'auth', rating: 4,
|
||
tags: ['권한', 'RBAC', '역할', '접근제어'],
|
||
memo: 'AWS IAM, GitHub Org Settings, Notion Permissions 등. 역할 기반 접근 제어를 관리하는 UI.',
|
||
components: [
|
||
{ name: '역할 목록 (관리자, 편집자, 뷰어)', required: true },
|
||
{ name: '권한 매트릭스 (역할 × 기능)', required: true },
|
||
{ name: '체크박스/토글 (권한 부여)', required: true },
|
||
{ name: '역할 생성/편집/삭제', required: true },
|
||
{ name: '사용자-역할 배정', required: true },
|
||
],
|
||
usedIn: ['AWS IAM', 'GitHub', 'Notion', 'SAM 부서 권한 관리'],
|
||
guidelines: '매트릭스: 행=권한, 열=역할. 체크/언체크로 즉시 변경. 관리자 역할은 수정 불가 (잠금 아이콘). 변경 시 확인 다이얼로그.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '세션/디바이스 관리', category: 'auth', rating: 3,
|
||
tags: ['세션', '디바이스', '보안', '로그아웃'],
|
||
memo: 'Google 계정, GitHub Sessions, Notion 등. 로그인된 디바이스/세션을 관리하고 원격 로그아웃.',
|
||
components: [
|
||
{ name: '활성 세션 목록', required: true },
|
||
{ name: '디바이스 정보 (OS, 브라우저, IP)', required: true },
|
||
{ name: '마지막 활동 시간', required: true },
|
||
{ name: '현재 세션 표시 (이 기기)', required: true },
|
||
{ name: '개별/전체 로그아웃 버튼', required: true },
|
||
],
|
||
usedIn: ['Google 계정', 'GitHub', 'Notion', 'SAM 보안 설정'],
|
||
guidelines: '현재 세션은 "이 기기" 초록 배지. 디바이스 아이콘 (데스크톱/모바일). 로그아웃 시 확인 다이얼로그. 마지막 활동 상대 시간 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: 'API 키 관리', category: 'auth', rating: 3,
|
||
tags: ['API키', '토큰', '인증', '개발자'],
|
||
memo: 'Stripe API Keys, OpenAI API, GitHub Token 등. API 키를 생성/관리/폐기하는 개발자 설정.',
|
||
components: [
|
||
{ name: 'API 키 목록 (마스킹)', required: true },
|
||
{ name: '새 키 생성 버튼', required: true },
|
||
{ name: '키 복사 버튼', required: true },
|
||
{ name: '키 폐기/삭제', required: true },
|
||
{ name: '만료일/사용 통계', required: false },
|
||
{ name: '생성 시 한 번만 표시 경고', required: true },
|
||
],
|
||
usedIn: ['Stripe', 'OpenAI', 'GitHub', 'SAM API 키 관리'],
|
||
guidelines: '키 마스킹: sk-...xxxx (앞 4자 + 뒤 4자만). 복사 시 "복사됨" 토스트. 새 키 생성 시 전체 표시 (한 번만). 테스트/프로덕션 키 구분.',
|
||
},
|
||
// ===== 보고서 추가 (4) =====
|
||
{
|
||
type: 'pattern', title: '간트 차트 (프로젝트 일정)', category: 'report', rating: 5,
|
||
tags: ['간트', '일정', '프로젝트', '타임라인'],
|
||
memo: 'Jira Roadmap, Monday.com, Microsoft Project 등. 작업 일정을 가로 막대로 시각화하는 프로젝트 관리 핵심 도구.',
|
||
components: [
|
||
{ name: '작업 목록 (좌측 고정)', required: true },
|
||
{ name: '시간축 (상단 헤더, 일/주/월)', required: true },
|
||
{ name: '작업 바 (시작~종료)', required: true },
|
||
{ name: '의존관계 화살표', required: false },
|
||
{ name: '진행률 표시 (바 내부)', required: true },
|
||
{ name: '마일스톤 (다이아몬드 아이콘)', required: false },
|
||
],
|
||
usedIn: ['Jira Roadmap', 'Monday.com', 'MS Project', 'SAM 프로젝트 관리'],
|
||
guidelines: '작업 바는 드래그로 이동/리사이즈. 오늘 날짜 빨간 세로선. 주말 배경 회색. 크리티컬 패스 빨간 강조.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '조직도 (트리 구조)', category: 'report', rating: 4,
|
||
tags: ['조직도', '트리', '계층', '구조'],
|
||
memo: 'SAM 조직도, Lucidchart, OrgChart 등. 부서/직원의 계층 구조를 시각적으로 표현.',
|
||
components: [
|
||
{ name: '노드 카드 (이름 + 직책)', required: true },
|
||
{ name: '연결선 (부모→자식)', required: true },
|
||
{ name: '확대/축소 컨트롤', required: true },
|
||
{ name: '접기/펼치기 (부서 단위)', required: true },
|
||
{ name: '드래그 팬 (이동)', required: true },
|
||
],
|
||
usedIn: ['SAM 조직도', 'Lucidchart', 'OrgChart', 'HR 시스템'],
|
||
guidelines: '노드 카드: 이름, 직책, 부서, 아바타. 연결선: 직선 또는 곡선. 수평/수직 레이아웃 전환. 검색 시 해당 노드 하이라이트.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '워터폴 차트 (재무 분석)', category: 'report', rating: 3,
|
||
tags: ['워터폴', '재무', '증감', '누적'],
|
||
memo: 'McKinsey, BCG 재무 보고서, Excel 등. 초기 값에서 증가/감소를 순차적으로 보여주는 재무 분석 차트.',
|
||
components: [
|
||
{ name: '시작 막대 (전체 높이)', required: true },
|
||
{ name: '증가 막대 (초록)', required: true },
|
||
{ name: '감소 막대 (빨강)', required: true },
|
||
{ name: '연결선 (이전 값→현재 시작)', required: true },
|
||
{ name: '최종 합계 막대', required: true },
|
||
{ name: '각 막대 위 수치 라벨', required: true },
|
||
],
|
||
usedIn: ['McKinsey 보고서', 'Excel', 'Power BI', 'SAM 재무 분석'],
|
||
guidelines: '증가=초록, 감소=빨강, 합계=파랑. 연결선으로 이전 값과 현재 시작점 연결. 막대 위에 절대 값 + 변화량 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '리포트 빌더 (커스텀 보고서)', category: 'report', rating: 4,
|
||
tags: ['리포트빌더', '커스텀', '보고서', '생성기'],
|
||
memo: 'Salesforce Reports, HubSpot Reports, Metabase 등. 사용자가 직접 필드/필터/차트를 조합하여 보고서를 생성.',
|
||
components: [
|
||
{ name: '데이터 소스 선택', required: true },
|
||
{ name: '필드 선택 (드래그 또는 체크)', required: true },
|
||
{ name: '필터 조건 빌더', required: true },
|
||
{ name: '차트 유형 선택', required: true },
|
||
{ name: '미리보기', required: true },
|
||
{ name: '저장/내보내기', required: true },
|
||
],
|
||
usedIn: ['Salesforce', 'HubSpot', 'Metabase', 'SAM 리포트 빌더 (계획)'],
|
||
guidelines: '좌측: 필드 목록 (드래그). 우측: 미리보기 영역. 필터: AND/OR 조합 UI. 차트 유형 아이콘 선택. 저장된 보고서 목록.',
|
||
},
|
||
// ===== 기타 추가 (15) =====
|
||
{
|
||
type: 'pattern', title: '댓글/스레드', category: 'etc', rating: 4,
|
||
tags: ['댓글', '스레드', '답글', '토론'],
|
||
memo: 'GitHub PR Comments, Notion Comments, Figma Comments 등. 특정 항목에 대한 토론을 스레드로 관리.',
|
||
components: [
|
||
{ name: '댓글 목록 (시간순)', required: true },
|
||
{ name: '아바타 + 작성자 + 시간', required: true },
|
||
{ name: '답글 (들여쓰기 스레드)', required: true },
|
||
{ name: '편집/삭제 (본인 댓글)', required: true },
|
||
{ name: '이모지 리액션', required: false },
|
||
{ name: '멘션 (@사용자)', required: false },
|
||
],
|
||
usedIn: ['GitHub', 'Notion', 'Figma', 'SAM 게시판/수주 댓글'],
|
||
guidelines: '답글 들여쓰기 24px. 시간은 상대 표시 (3분 전). 본인 댓글만 편집/삭제 가능. 새 댓글 자동 스크롤.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '에러 페이지 (404/500)', category: 'etc', rating: 3,
|
||
tags: ['에러', '404', '500', '오류페이지'],
|
||
memo: 'GitHub 404, Notion 404, Stripe 등. 페이지를 찾을 수 없거나 서버 오류 시 표시되는 안내 페이지.',
|
||
components: [
|
||
{ name: '에러 코드 (큰 텍스트)', required: true },
|
||
{ name: '안내 메시지', required: true },
|
||
{ name: '일러스트/아이콘', required: false },
|
||
{ name: '홈으로 가기 버튼', required: true },
|
||
{ name: '검색 바 (선택적)', required: false },
|
||
{ name: '이전 페이지로 가기', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Notion', 'Stripe', 'SAM 에러 페이지'],
|
||
guidelines: '화면 중앙 배치. 에러 코드 크게 (72~96px). 친근한 메시지. 홈/뒤로 가기 버튼 명확히. 404는 유머 가능, 500은 진지하게.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '비교 테이블 (기능/제품)', category: 'etc', rating: 4,
|
||
tags: ['비교', '기능비교', '제품', '체크마크'],
|
||
memo: 'SaaS 가격 페이지, Product Hunt, AWS 서비스 비교 등. 여러 옵션을 기능별로 나란히 비교하는 테이블.',
|
||
components: [
|
||
{ name: '기능 목록 (좌측 고정)', required: true },
|
||
{ name: '옵션 컬럼 (2~4개)', required: true },
|
||
{ name: '체크/X 아이콘', required: true },
|
||
{ name: '추천 옵션 하이라이트', required: false },
|
||
{ name: '가격 행 (상단 또는 하단)', required: true },
|
||
{ name: '선택/CTA 버튼', required: true },
|
||
],
|
||
usedIn: ['SaaS 가격 페이지', 'Product Hunt', 'AWS', 'SAM 요금제 비교'],
|
||
guidelines: '기능 행 zebra striping. 추천 열 파란 테두리 + 배지. 체크=초록, X=빨강, 하이픈=회색. 모바일: 좌우 스크롤.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '캐러셀/슬라이더', category: 'etc', rating: 3,
|
||
tags: ['캐러셀', '슬라이더', '갤러리', '스와이프'],
|
||
memo: 'Amazon 상품 이미지, Netflix 콘텐츠 행, Instagram 다중 이미지 등. 좌우로 넘기는 콘텐츠 슬라이더.',
|
||
components: [
|
||
{ name: '슬라이드 컨테이너', required: true },
|
||
{ name: '좌/우 네비게이션 화살표', required: true },
|
||
{ name: '페이지 인디케이터 (점)', required: true },
|
||
{ name: '자동 재생 (선택적)', required: false },
|
||
{ name: '스와이프 제스처 (모바일)', required: true },
|
||
],
|
||
usedIn: ['Amazon', 'Netflix', 'Instagram', 'SAM 제품 이미지'],
|
||
guidelines: '한 번에 1~3개 표시. 인디케이터 5개 이하. 자동 재생 시 정지 버튼. 마지막 슬라이드 후 첫 번째로 순환. 스와이프 threshold 50px.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '아코디언/FAQ', category: 'etc', rating: 4,
|
||
tags: ['아코디언', 'FAQ', '접기', '펼치기'],
|
||
memo: 'Apple FAQ, Stripe Help, Bootstrap Accordion 등. 질문/답변을 접기/펼치기로 관리하는 UI.',
|
||
components: [
|
||
{ name: '질문/제목 (클릭 영역)', required: true },
|
||
{ name: '펼치기/접기 아이콘 (+ 또는 ▾)', required: true },
|
||
{ name: '답변/내용 (슬라이드 펼침)', required: true },
|
||
{ name: '하나만 열기 모드 (선택적)', required: false },
|
||
{ name: '전체 펼치기/접기 버튼', required: false },
|
||
],
|
||
usedIn: ['Apple FAQ', 'Stripe', 'Bootstrap', 'SAM FAQ/도움말'],
|
||
guidelines: '기본 모두 접힌 상태. 클릭 영역은 전체 행. 아이콘 회전 애니메이션 (180도). 펼침 시 슬라이드 다운 200ms.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '툴팁/팝오버', category: 'etc', rating: 3,
|
||
tags: ['툴팁', '팝오버', '도움말', 'hover'],
|
||
memo: 'GitHub 아이콘 hover, Notion 블록 메뉴, AWS 설명 등. 요소에 마우스를 올리면 추가 정보를 표시.',
|
||
components: [
|
||
{ name: '트리거 요소 (hover/click)', required: true },
|
||
{ name: '팝업 컨테이너 (말풍선)', required: true },
|
||
{ name: '화살표 (연결 포인터)', required: true },
|
||
{ name: '텍스트 내용', required: true },
|
||
{ name: '닫기 버튼 (팝오버)', required: false },
|
||
],
|
||
usedIn: ['GitHub', 'Notion', 'AWS', 'SAM 아이콘 도움말'],
|
||
guidelines: '툴팁: hover 후 300ms 지연 표시. 팝오버: 클릭 트리거 + 닫기. 위치: 자동 조정 (화면 밖 방지). 최대 너비 250px.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '프로그레스 트래커 (배송/처리)', category: 'etc', rating: 4,
|
||
tags: ['프로그레스', '배송', '처리상태', '추적'],
|
||
memo: 'Amazon 배송 추적, Coupang, DHL 등. 주문/처리 단계별 진행 상황을 시각화.',
|
||
components: [
|
||
{ name: '단계 노드 (원/체크)', required: true },
|
||
{ name: '연결선 (완료/진행/대기)', required: true },
|
||
{ name: '단계 라벨 + 시간', required: true },
|
||
{ name: '현재 단계 강조', required: true },
|
||
{ name: '상세 정보 (각 단계 설명)', required: false },
|
||
],
|
||
usedIn: ['Amazon', 'Coupang', 'DHL', 'SAM 수주 상태 추적'],
|
||
guidelines: '완료=초록 체크, 현재=파란 점+애니메이션, 대기=회색. 수평 또는 수직 배치. 각 단계에 시간/날짜 표시.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '데이터 임포트 위자드', category: 'etc', rating: 4,
|
||
tags: ['임포트', '가져오기', 'CSV', '엑셀'],
|
||
memo: 'Airtable Import, HubSpot Import, Notion Import 등. CSV/엑셀 파일을 업로드하고 필드를 매핑하는 3단계 플로우.',
|
||
components: [
|
||
{ name: '파일 업로드 (드래그 앤 드롭)', required: true },
|
||
{ name: '미리보기 (첫 5행)', required: true },
|
||
{ name: '필드 매핑 (소스→대상)', required: true },
|
||
{ name: '유효성 검사 결과', required: true },
|
||
{ name: '임포트 진행률', required: true },
|
||
{ name: '결과 요약 (성공/실패 건수)', required: true },
|
||
],
|
||
usedIn: ['Airtable', 'HubSpot', 'Notion', 'SAM 데이터 마이그레이션'],
|
||
guidelines: '3단계: 업로드→매핑→확인. 미리보기에서 문제 행 빨간 강조. 매핑: 드롭다운으로 대상 필드 선택. 자동 감지 (컬럼명 유사도).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '랜딩 페이지 히어로 섹션', category: 'etc', rating: 4,
|
||
tags: ['랜딩', '히어로', 'CTA', '메인배너'],
|
||
memo: 'Stripe, Linear, Vercel 등 SaaS. 메인 페이지 첫 화면. 헤드라인 + 서브텍스트 + CTA 버튼의 표준 구조.',
|
||
components: [
|
||
{ name: '헤드라인 (큰 텍스트)', required: true },
|
||
{ name: '서브 텍스트 (설명)', required: true },
|
||
{ name: 'CTA 버튼 (1~2개)', required: true },
|
||
{ name: '히어로 이미지/영상/일러스트', required: false },
|
||
{ name: '소셜 프루프 (고객 로고/통계)', required: false },
|
||
],
|
||
usedIn: ['Stripe', 'Linear', 'Vercel', 'SAM 소개 페이지'],
|
||
guidelines: '헤드라인: 32~56px, 한 줄 권장. CTA 버튼: 명확한 액션 (예: "무료로 시작하기"). 서브 텍스트: 18~20px, 2줄 이내.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '코드 에디터/뷰어', category: 'etc', rating: 3,
|
||
tags: ['코드', '에디터', '구문강조', '개발'],
|
||
memo: 'VS Code, GitHub Gist, CodePen 등. 코드를 구문 강조하여 보여주거나 편집하는 UI.',
|
||
components: [
|
||
{ name: '코드 영역 (모노스페이스 폰트)', required: true },
|
||
{ name: '줄 번호', required: true },
|
||
{ name: '구문 강조 (Syntax Highlighting)', required: true },
|
||
{ name: '복사 버튼', required: true },
|
||
{ name: '언어 선택 드롭다운', required: false },
|
||
{ name: '다크/라이트 테마', required: false },
|
||
],
|
||
usedIn: ['VS Code', 'GitHub', 'CodePen', 'SAM API 문서'],
|
||
guidelines: '폰트: JetBrains Mono 또는 Fira Code. 다크 배경 (#1e1e1e). 복사 시 "복사됨" 피드백. 줄 번호 회색, 코드 색상은 테마에 따름.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '키보드 단축키 도움말', category: 'etc', rating: 3,
|
||
tags: ['단축키', '키보드', '도움말', '핫키'],
|
||
memo: 'GitHub (? 키), Notion (/), Gmail (?) 등. 키보드 단축키 목록을 보여주는 오버레이.',
|
||
components: [
|
||
{ name: '키 조합 표시 (Kbd 스타일)', required: true },
|
||
{ name: '액션 설명', required: true },
|
||
{ name: '카테고리 그룹핑', required: true },
|
||
{ name: '검색/필터', required: false },
|
||
{ name: 'ESC로 닫기', required: true },
|
||
],
|
||
usedIn: ['GitHub', 'Notion', 'Gmail', 'SAM 디자인 인사이트'],
|
||
guidelines: 'Kbd 스타일: 회색 배경 + 테두리 + 라운드. 그룹별 섹션 분리. 2~3열 레이아웃. ?키로 열기 관례.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '다크 모드 전환', category: 'etc', rating: 3,
|
||
tags: ['다크모드', '테마', '라이트', '전환'],
|
||
memo: 'macOS, Tailwind, GitHub, VS Code 등. 라이트/다크 테마를 전환하는 UI + 실제 적용.',
|
||
components: [
|
||
{ name: '토글 스위치/버튼', required: true },
|
||
{ name: '해/달 아이콘', required: true },
|
||
{ name: '시스템 설정 연동', required: false },
|
||
{ name: '전환 애니메이션', required: false },
|
||
{ name: 'CSS 변수 기반 테마', required: true },
|
||
],
|
||
usedIn: ['macOS', 'GitHub', 'Tailwind Docs', 'SAM (계획)'],
|
||
guidelines: '3옵션: 라이트/다크/시스템. 전환 시 부드러운 트랜지션 (200ms). prefers-color-scheme 미디어 쿼리. localStorage에 선호 저장.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '알림 설정 패널', category: 'etc', rating: 3,
|
||
tags: ['알림설정', '구독', '채널', '선호'],
|
||
memo: 'GitHub Notifications, Slack Preferences, Notion Settings 등. 알림 채널별/유형별 수신 여부를 설정.',
|
||
components: [
|
||
{ name: '알림 유형 목록 (수주, 승인 등)', required: true },
|
||
{ name: '채널별 토글 (이메일/푸시/슬랙)', required: true },
|
||
{ name: '방해 금지 시간 설정', required: false },
|
||
{ name: '전체 켜기/끄기', required: true },
|
||
{ name: '미리보기 (알림 샘플)', required: false },
|
||
],
|
||
usedIn: ['GitHub', 'Slack', 'Notion', 'SAM 알림 설정'],
|
||
guidelines: '매트릭스: 행=알림 유형, 열=채널. 토글 ON/OFF. 변경 즉시 저장. 필수 알림(보안)은 비활성화 불가.',
|
||
},
|
||
{
|
||
type: 'pattern', title: '파일 매니저/탐색기', category: 'etc', rating: 4,
|
||
tags: ['파일', '탐색기', '폴더', '관리'],
|
||
memo: 'Google Drive, Dropbox, macOS Finder 등. 폴더/파일을 계층적으로 탐색하고 관리하는 UI.',
|
||
components: [
|
||
{ name: '폴더 트리 (좌측)', required: true },
|
||
{ name: '파일 목록 (우측, 그리드/리스트)', required: true },
|
||
{ name: '경로 표시 (브레드크럼)', required: true },
|
||
{ name: '업로드/새 폴더 버튼', required: true },
|
||
{ name: '파일 미리보기', required: false },
|
||
{ name: '검색/정렬/필터', required: true },
|
||
],
|
||
usedIn: ['Google Drive', 'Dropbox', 'macOS Finder', 'SAM 문서 관리'],
|
||
guidelines: '더블클릭으로 폴더 진입. 파일 유형별 아이콘. 드래그 앤 드롭으로 이동. 우클릭 컨텍스트 메뉴. 다중 선택 (Ctrl+클릭).',
|
||
},
|
||
{
|
||
type: 'pattern', title: '소셜 공유 버튼', category: 'etc', rating: 3,
|
||
tags: ['공유', '소셜', 'SNS', '링크'],
|
||
memo: 'Medium, Product Hunt, 뉴스 사이트 등. 콘텐츠를 SNS에 공유하는 버튼 세트.',
|
||
components: [
|
||
{ name: '소셜 미디어 아이콘 (Twitter, LinkedIn 등)', required: true },
|
||
{ name: '링크 복사 버튼', required: true },
|
||
{ name: '공유 수 카운터', required: false },
|
||
{ name: '이메일 공유', required: false },
|
||
{ name: '플로팅/고정 위치 선택', required: true },
|
||
],
|
||
usedIn: ['Medium', 'Product Hunt', '뉴스 사이트', 'SAM 공유 기능'],
|
||
guidelines: '아이콘: 각 플랫폼 브랜드 컬러. 링크 복사 시 "복사됨" 토스트. 플로팅: 좌측 고정, 스크롤 따라감. 인라인: 콘텐츠 상/하단.',
|
||
},
|
||
];
|
||
|
||
const cards = presets.map(p => ({
|
||
id: mkId(),
|
||
type: p.type,
|
||
title: p.title,
|
||
image: '',
|
||
memo: p.memo,
|
||
source: '',
|
||
tags: p.tags,
|
||
category: p.category,
|
||
rating: p.rating,
|
||
pinned: false,
|
||
archived: false,
|
||
components: p.components || [],
|
||
usedIn: p.usedIn || [],
|
||
guidelines: p.guidelines || '',
|
||
frequency: 0,
|
||
principles: {},
|
||
suggestion: '',
|
||
severity: 'info',
|
||
beforeImage: '',
|
||
afterImage: '',
|
||
changes: [],
|
||
effect: '',
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
}));
|
||
|
||
if (!this.currentProject.cards) this.currentProject.cards = [];
|
||
this.currentProject.cards = [...cards, ...this.currentProject.cards];
|
||
this.saveProject();
|
||
this.categoryFilter = 'pattern';
|
||
this.toast('인기 UI 패턴 100종이 추가되었습니다');
|
||
},
|
||
};
|
||
}
|
||
</script>
|
||
@endpush
|