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